feat: RBAC (#8922)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Valya Bullions <valya@n8n.io> Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: Danny Martini <despair.blue@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: oleg <me@olegivaniv.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Elias Meire <elias@meire.dev> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Ayato Hayashi <go12limchangyong@gmail.com>
This commit is contained in:
@@ -10,6 +10,22 @@
|
||||
@click:add="addCredential"
|
||||
@update:filters="filters = $event"
|
||||
>
|
||||
<template #header>
|
||||
<ProjectTabs />
|
||||
</template>
|
||||
<template #add-button="{ disabled }">
|
||||
<div>
|
||||
<n8n-button
|
||||
size="large"
|
||||
block
|
||||
:disabled="disabled"
|
||||
data-test-id="resources-list-add"
|
||||
@click="addCredential"
|
||||
>
|
||||
{{ addCredentialButtonText }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ data }">
|
||||
<CredentialCard data-test-id="resources-list-item" class="mb-2xs" :data="data" />
|
||||
</template>
|
||||
@@ -53,11 +69,12 @@ import type { ICredentialType } from 'n8n-workflow';
|
||||
import { CREDENTIAL_SELECT_MODAL_KEY, EnterpriseEditionFeature } from '@/constants';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
import ProjectTabs from '@/features/projects/components/ProjectTabs.vue';
|
||||
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
@@ -68,13 +85,13 @@ export default defineComponent({
|
||||
components: {
|
||||
ResourcesListLayout,
|
||||
CredentialCard,
|
||||
ProjectTabs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: {
|
||||
search: '',
|
||||
ownedBy: '',
|
||||
sharedWith: '',
|
||||
homeProject: '',
|
||||
type: '',
|
||||
},
|
||||
sourceControlStoreUnsubscribe: () => {},
|
||||
@@ -85,9 +102,9 @@ export default defineComponent({
|
||||
useCredentialsStore,
|
||||
useNodeTypesStore,
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useSourceControlStore,
|
||||
useExternalSecretsStore,
|
||||
useProjectsStore,
|
||||
),
|
||||
allCredentials(): ICredentialsResponse[] {
|
||||
return this.credentialsStore.allCredentials;
|
||||
@@ -98,11 +115,19 @@ export default defineComponent({
|
||||
credentialTypesById(): ICredentialTypeMap {
|
||||
return this.credentialsStore.credentialTypesById;
|
||||
},
|
||||
addCredentialButtonText() {
|
||||
return this.projectsStore.currentProject
|
||||
? this.$locale.baseText('credentials.project.add')
|
||||
: this.$locale.baseText('credentials.add');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'filters.type'() {
|
||||
this.sendFiltersTelemetry('type');
|
||||
},
|
||||
'$route.params.projectId'() {
|
||||
void this.initialize();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
|
||||
@@ -130,7 +155,9 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
const loadPromises = [
|
||||
this.credentialsStore.fetchAllCredentials(),
|
||||
this.credentialsStore.fetchAllCredentials(
|
||||
this.$route?.params?.projectId as string | undefined,
|
||||
),
|
||||
this.credentialsStore.fetchCredentialTypes(false),
|
||||
this.externalSecretsStore.fetchAllSecrets(),
|
||||
this.nodeTypesStore.loadNodeTypesIfNotLoaded(),
|
||||
@@ -138,8 +165,6 @@ export default defineComponent({
|
||||
];
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
await this.usersStore.fetchUsers(); // Can be loaded in the background, used for filtering
|
||||
},
|
||||
onFilter(
|
||||
resource: ICredentialsResponse,
|
||||
|
||||
@@ -290,6 +290,7 @@ import type {
|
||||
Workflow,
|
||||
ConnectionTypes,
|
||||
INodeOutputConfiguration,
|
||||
IRun,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
deepCopy,
|
||||
@@ -317,6 +318,7 @@ import type {
|
||||
NodeCreatorOpenSource,
|
||||
AddedNodesAndConnections,
|
||||
ToggleNodeCreatorOptions,
|
||||
IPushDataExecutionFinished,
|
||||
AIAssistantConnectionInfo,
|
||||
} from '@/Interface';
|
||||
|
||||
@@ -388,6 +390,8 @@ import { useCanvasPanning } from '@/composables/useCanvasPanning';
|
||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
import type { ProjectSharingData } from '@/features/projects/projects.types';
|
||||
import { useAIStore } from '@/stores/ai.store';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { isJSPlumbEndpointElement } from '@/utils/typeGuards';
|
||||
@@ -555,7 +559,7 @@ export default defineComponent({
|
||||
this.resetWorkspace();
|
||||
this.uiStore.stateIsDirty = previousDirtyState;
|
||||
}
|
||||
await Promise.all([this.loadCredentials(), this.initView()]);
|
||||
await this.initView();
|
||||
this.canvasStore.stopLoading();
|
||||
if (this.blankRedirect) {
|
||||
this.blankRedirect = false;
|
||||
@@ -617,6 +621,7 @@ export default defineComponent({
|
||||
usePushConnectionStore,
|
||||
useSourceControlStore,
|
||||
useExecutionsStore,
|
||||
useProjectsStore,
|
||||
useAIStore,
|
||||
),
|
||||
nativelyNumberSuffixedDefaults(): string[] {
|
||||
@@ -837,11 +842,7 @@ export default defineComponent({
|
||||
|
||||
const loadPromises = (() => {
|
||||
if (this.settingsStore.isPreviewMode && this.isDemo) return [];
|
||||
const promises = [
|
||||
this.loadActiveWorkflows(),
|
||||
this.loadCredentials(),
|
||||
this.loadCredentialTypes(),
|
||||
];
|
||||
const promises = [this.loadActiveWorkflows(), this.loadCredentialTypes()];
|
||||
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables)) {
|
||||
promises.push(this.loadVariables());
|
||||
}
|
||||
@@ -1282,17 +1283,10 @@ export default defineComponent({
|
||||
this.workflowsStore.setWorkflowPinData(data.workflowData.pinData);
|
||||
}
|
||||
|
||||
if (data.workflowData.ownedBy) {
|
||||
this.workflowsEEStore.setWorkflowOwnedBy({
|
||||
workflowId: data.workflowData.id,
|
||||
ownedBy: data.workflowData.ownedBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.workflowData.sharedWith) {
|
||||
if (data.workflowData.sharedWithProjects) {
|
||||
this.workflowsEEStore.setWorkflowSharedWith({
|
||||
workflowId: data.workflowData.id,
|
||||
sharedWith: data.workflowData.sharedWith,
|
||||
sharedWithProjects: data.workflowData.sharedWithProjects,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1418,7 +1412,11 @@ export default defineComponent({
|
||||
await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
||||
|
||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||
this.workflowData = (await this.workflowsStore.getNewWorkflowData(data.name)) || {};
|
||||
this.workflowData =
|
||||
(await this.workflowsStore.getNewWorkflowData(
|
||||
data.name,
|
||||
this.projectsStore.currentProjectId,
|
||||
)) || {};
|
||||
this.workflowsStore.addToWorkflowMetadata({ templateId });
|
||||
await this.$nextTick();
|
||||
this.canvasStore.zoomToFit();
|
||||
@@ -1447,17 +1445,10 @@ export default defineComponent({
|
||||
this.workflowsStore.setWorkflowVersionId(workflow.versionId);
|
||||
this.workflowsStore.setWorkflowMetadata(workflow.meta);
|
||||
|
||||
if (workflow.ownedBy) {
|
||||
this.workflowsEEStore.setWorkflowOwnedBy({
|
||||
workflowId: workflow.id,
|
||||
ownedBy: workflow.ownedBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.sharedWith) {
|
||||
if (workflow.sharedWithProjects) {
|
||||
this.workflowsEEStore.setWorkflowSharedWith({
|
||||
workflowId: workflow.id,
|
||||
sharedWith: workflow.sharedWith,
|
||||
sharedWithProjects: workflow.sharedWithProjects,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3647,10 +3638,21 @@ export default defineComponent({
|
||||
// Clear the interval to prevent the notification from being sent
|
||||
clearTimeout(this.unloadTimeout);
|
||||
},
|
||||
makeNewWorkflowShareable() {
|
||||
const { currentProject, personalProject } = this.projectsStore;
|
||||
const homeProject = currentProject ?? personalProject ?? {};
|
||||
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
||||
|
||||
this.workflowsStore.workflow.homeProject = homeProject as ProjectSharingData;
|
||||
this.workflowsStore.workflow.scopes = scopes;
|
||||
},
|
||||
async newWorkflow(): Promise<void> {
|
||||
this.canvasStore.startLoading();
|
||||
this.resetWorkspace();
|
||||
this.workflowData = await this.workflowsStore.getNewWorkflowData();
|
||||
this.workflowData = await this.workflowsStore.getNewWorkflowData(
|
||||
undefined,
|
||||
this.projectsStore.currentProjectId,
|
||||
);
|
||||
this.workflowsStore.currentWorkflowExecutions = [];
|
||||
this.executionsStore.activeExecution = null;
|
||||
|
||||
@@ -3660,6 +3662,7 @@ export default defineComponent({
|
||||
this.uiStore.nodeViewInitialized = true;
|
||||
this.historyStore.reset();
|
||||
this.executionsStore.activeExecution = null;
|
||||
this.makeNewWorkflowShareable();
|
||||
this.canvasStore.stopLoading();
|
||||
},
|
||||
async tryToAddWelcomeSticky(): Promise<void> {
|
||||
@@ -3700,6 +3703,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.loadCredentials();
|
||||
// Load a workflow
|
||||
let workflowId = null as string | null;
|
||||
if (this.$route.params.name) {
|
||||
@@ -4664,7 +4668,12 @@ export default defineComponent({
|
||||
await this.credentialsStore.fetchCredentialTypes(true);
|
||||
},
|
||||
async loadCredentials(): Promise<void> {
|
||||
await this.credentialsStore.fetchAllCredentials();
|
||||
const workflow = this.workflowsStore.getWorkflowById(this.currentWorkflow);
|
||||
const projectId =
|
||||
workflow?.homeProject?.type === 'personal'
|
||||
? this.projectsStore.personalProject?.id
|
||||
: workflow?.homeProject?.id;
|
||||
await this.credentialsStore.fetchAllCredentials(projectId);
|
||||
},
|
||||
async loadVariables(): Promise<void> {
|
||||
await this.environmentsStore.fetchAllVariables();
|
||||
@@ -4968,7 +4977,7 @@ export default defineComponent({
|
||||
this.resetWorkspace();
|
||||
this.uiStore.stateIsDirty = false;
|
||||
|
||||
await this.$router.replace({ name: VIEWS.WORKFLOWS });
|
||||
await this.$router.replace({ name: VIEWS.HOMEPAGE });
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -5032,7 +5041,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([this.loadCredentials(), this.loadVariables(), this.tagsStore.fetchAll()]);
|
||||
await Promise.all([this.loadVariables(), this.tagsStore.fetchAll(), this.loadCredentials()]);
|
||||
|
||||
if (workflowId !== null && !this.uiStore.stateIsDirty) {
|
||||
const workflow: IWorkflowDb | undefined =
|
||||
|
||||
@@ -110,11 +110,6 @@ export default defineComponent({
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.showUMSetupWarning) {
|
||||
await this.usersStore.fetchUsers();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useUIStore, useUsersStore, useUsageStore, useSSOStore),
|
||||
isSharingEnabled() {
|
||||
@@ -191,6 +186,11 @@ export default defineComponent({
|
||||
return hasPermission(['rbac'], { rbac: { scope: ['user:update', 'user:changeRole'] } });
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.showUMSetupWarning) {
|
||||
await this.usersStore.fetchUsers();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
redirectToSetup() {
|
||||
void this.$router.push({ name: VIEWS.SETUP });
|
||||
|
||||
@@ -66,7 +66,7 @@ export const credentialsTelegram1: ICredentialsResponse = {
|
||||
firstName: 'Player',
|
||||
lastName: 'One',
|
||||
},
|
||||
sharedWith: [],
|
||||
sharedWithProjects: [],
|
||||
};
|
||||
|
||||
export const credentialsTelegram2: ICredentialsResponse = {
|
||||
@@ -81,7 +81,7 @@ export const credentialsTelegram2: ICredentialsResponse = {
|
||||
firstName: 'Player',
|
||||
lastName: 'One',
|
||||
},
|
||||
sharedWith: [],
|
||||
sharedWithProjects: [],
|
||||
};
|
||||
|
||||
export {
|
||||
|
||||
@@ -246,6 +246,11 @@ onBeforeUnmount(() => {
|
||||
@sort="resetNewVariablesList"
|
||||
@click:add="addTemporaryVariable"
|
||||
>
|
||||
<template #header>
|
||||
<n8n-heading size="2xlarge" class="mb-m">
|
||||
{{ i18n.baseText('variables.heading') }}
|
||||
</n8n-heading>
|
||||
</template>
|
||||
<template #add-button>
|
||||
<n8n-tooltip placement="top" :disabled="canCreateVariables">
|
||||
<div>
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
:filters="filters"
|
||||
:additional-filters-handler="onFilter"
|
||||
:type-props="{ itemSize: 80 }"
|
||||
:show-aside="allWorkflows.length > 0"
|
||||
:shareable="isShareable"
|
||||
:initialize="initialize"
|
||||
:disabled="readOnlyEnv"
|
||||
@click:add="addWorkflow"
|
||||
@update:filters="onFiltersUpdated"
|
||||
>
|
||||
<template #header>
|
||||
<ProjectTabs v-if="showProjectTabs" />
|
||||
</template>
|
||||
<template #add-button="{ disabled }">
|
||||
<n8n-tooltip :disabled="!readOnlyEnv">
|
||||
<div>
|
||||
@@ -23,7 +25,7 @@
|
||||
data-test-id="resources-list-add"
|
||||
@click="addWorkflow"
|
||||
>
|
||||
{{ $locale.baseText(`workflows.add`) }}
|
||||
{{ addWorkflowButtonText }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<template #content>
|
||||
@@ -171,11 +173,11 @@ import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
import ProjectTabs from '@/features/projects/components/ProjectTabs.vue';
|
||||
|
||||
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
|
||||
|
||||
@@ -201,13 +203,13 @@ const WorkflowsView = defineComponent({
|
||||
TagsDropdown,
|
||||
SuggestedTemplatesPage,
|
||||
SuggestedTemplatesSection,
|
||||
ProjectTabs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: {
|
||||
search: '',
|
||||
ownedBy: '',
|
||||
sharedWith: '',
|
||||
homeProject: '',
|
||||
status: StatusFilter.ALL as string | boolean,
|
||||
tags: [] as string[],
|
||||
},
|
||||
@@ -220,11 +222,9 @@ const WorkflowsView = defineComponent({
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useWorkflowsStore,
|
||||
useCredentialsStore,
|
||||
useSourceControlStore,
|
||||
useTagsStore,
|
||||
useTemplatesStore,
|
||||
useUsersStore,
|
||||
useProjectsStore,
|
||||
),
|
||||
readOnlyEnv(): boolean {
|
||||
return this.sourceControlStore.preferences.branchReadOnly;
|
||||
@@ -269,14 +269,29 @@ const WorkflowsView = defineComponent({
|
||||
}
|
||||
return ['Sales', 'sales-and-marketing'].includes(this.userRole);
|
||||
},
|
||||
showProjectTabs() {
|
||||
return (
|
||||
!!this.$route.params.projectId ||
|
||||
!!this.allWorkflows.length ||
|
||||
this.projectsStore.myProjects.length > 1
|
||||
);
|
||||
},
|
||||
addWorkflowButtonText() {
|
||||
return this.projectsStore.currentProject
|
||||
? this.$locale.baseText('workflows.project.add')
|
||||
: this.$locale.baseText('workflows.add');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'filters.tags'() {
|
||||
this.sendFiltersTelemetry('tags');
|
||||
},
|
||||
'$route.params.projectId'() {
|
||||
void this.initialize();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFiltersFromQueryString();
|
||||
async mounted() {
|
||||
await this.setFiltersFromQueryString();
|
||||
|
||||
void this.usersStore.showPersonalizationSurvey();
|
||||
|
||||
@@ -298,7 +313,10 @@ const WorkflowsView = defineComponent({
|
||||
},
|
||||
addWorkflow() {
|
||||
this.uiStore.nodeViewInitialized = false;
|
||||
void this.$router.push({ name: VIEWS.NEW_WORKFLOW });
|
||||
void this.$router.push({
|
||||
name: VIEWS.NEW_WORKFLOW,
|
||||
query: { projectId: this.$route?.params?.projectId },
|
||||
});
|
||||
|
||||
this.$telemetry.track('User clicked add workflow button', {
|
||||
source: 'Workflows list',
|
||||
@@ -316,9 +334,8 @@ const WorkflowsView = defineComponent({
|
||||
async initialize() {
|
||||
await Promise.all([
|
||||
this.usersStore.fetchUsers(),
|
||||
this.workflowsStore.fetchAllWorkflows(),
|
||||
this.workflowsStore.fetchAllWorkflows(this.$route?.params?.projectId as string | undefined),
|
||||
this.workflowsStore.fetchActiveWorkflows(),
|
||||
this.credentialsStore.fetchAllCredentials(),
|
||||
]);
|
||||
},
|
||||
onClickTag(tagId: string, event: PointerEvent) {
|
||||
@@ -367,32 +384,27 @@ const WorkflowsView = defineComponent({
|
||||
query.tags = this.filters.tags.join(',');
|
||||
}
|
||||
|
||||
if (this.filters.ownedBy) {
|
||||
query.ownedBy = this.filters.ownedBy;
|
||||
}
|
||||
|
||||
if (this.filters.sharedWith) {
|
||||
query.sharedWith = this.filters.sharedWith;
|
||||
if (this.filters.homeProject) {
|
||||
query.homeProject = this.filters.homeProject;
|
||||
}
|
||||
|
||||
void this.$router.replace({
|
||||
query: Object.keys(query).length ? query : undefined,
|
||||
});
|
||||
},
|
||||
isValidUserId(userId: string) {
|
||||
return Object.keys(this.usersStore.users).includes(userId);
|
||||
isValidProjectId(projectId: string) {
|
||||
return this.projectsStore.projects.some((project) => project.id === projectId);
|
||||
},
|
||||
setFiltersFromQueryString() {
|
||||
const { tags, status, search, ownedBy, sharedWith } = this.$route.query;
|
||||
async setFiltersFromQueryString() {
|
||||
const { tags, status, search, homeProject } = this.$route.query;
|
||||
|
||||
const filtersToApply: { [key: string]: string | string[] | boolean } = {};
|
||||
|
||||
if (ownedBy && typeof ownedBy === 'string' && this.isValidUserId(ownedBy)) {
|
||||
filtersToApply.ownedBy = ownedBy;
|
||||
}
|
||||
|
||||
if (sharedWith && typeof sharedWith === 'string' && this.isValidUserId(sharedWith)) {
|
||||
filtersToApply.sharedWith = sharedWith;
|
||||
if (homeProject && typeof homeProject === 'string') {
|
||||
await this.projectsStore.getAllProjects();
|
||||
if (this.isValidProjectId(homeProject)) {
|
||||
filtersToApply.homeProject = homeProject;
|
||||
}
|
||||
}
|
||||
|
||||
if (search && typeof search === 'string') {
|
||||
|
||||
137
packages/editor-ui/src/views/__tests__/SettingsUsersView.test.ts
Normal file
137
packages/editor-ui/src/views/__tests__/SettingsUsersView.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { within } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { getDropdownItems } from '@/__tests__/utils';
|
||||
import ModalRoot from '@/components/ModalRoot.vue';
|
||||
import DeleteUserModal from '@/components/DeleteUserModal.vue';
|
||||
import SettingsUsersView from '@/views/SettingsUsersView.vue';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { createUser } from '@/__tests__/data/users';
|
||||
import { createProjectListItem } from '@/__tests__/data/projects';
|
||||
import { useRBACStore } from '@/stores/rbac.store';
|
||||
import { DELETE_USER_MODAL_KEY } from '@/constants';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
const wrapperComponentWithModal = {
|
||||
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
|
||||
template: `
|
||||
<div>
|
||||
<SettingsUsersView />
|
||||
<ModalRoot name="${DELETE_USER_MODAL_KEY}">
|
||||
<template #default="{ modalName, activeId }">
|
||||
<DeleteUserModal :modal-name="modalName" :active-id="activeId" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(wrapperComponentWithModal);
|
||||
|
||||
const loggedInUser = createUser();
|
||||
const users = Array.from({ length: 3 }, createUser);
|
||||
const personalProjects = Array.from({ length: 3 }, createProjectListItem);
|
||||
|
||||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let rbacStore: ReturnType<typeof useRBACStore>;
|
||||
|
||||
describe('SettingsUsersView', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
projectsStore = useProjectsStore();
|
||||
usersStore = useUsersStore();
|
||||
rbacStore = useRBACStore();
|
||||
|
||||
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
|
||||
vi.spyOn(usersStore, 'fetchUsers').mockImplementation(async () => await Promise.resolve());
|
||||
vi.spyOn(usersStore, 'allUsers', 'get').mockReturnValue(users);
|
||||
vi.spyOn(usersStore, 'getUserById', 'get').mockReturnValue(() => loggedInUser);
|
||||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
||||
async () => await Promise.resolve(),
|
||||
);
|
||||
vi.spyOn(projectsStore, 'personalProjects', 'get').mockReturnValue(personalProjects);
|
||||
});
|
||||
|
||||
it('should show confirmation modal before deleting user and delete with transfer', async () => {
|
||||
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
const userListItem = getByTestId(`user-list-item-${users[0].email}`);
|
||||
expect(userListItem).toBeInTheDocument();
|
||||
|
||||
const actionToggle = within(userListItem).getByTestId('action-toggle');
|
||||
const actionToggleButton = within(actionToggle).getByRole('button');
|
||||
expect(actionToggleButton).toBeVisible();
|
||||
|
||||
await userEvent.click(actionToggle);
|
||||
const actionToggleId = actionToggleButton.getAttribute('aria-controls');
|
||||
|
||||
const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement;
|
||||
const actionDelete = within(actionDropdown).getByTestId('action-delete');
|
||||
await userEvent.click(actionDelete);
|
||||
|
||||
const modal = getByTestId('deleteUser-modal');
|
||||
expect(modal).toBeVisible();
|
||||
const confirmButton = within(modal).getByTestId('confirm-delete-user-button');
|
||||
expect(confirmButton).toBeDisabled();
|
||||
|
||||
await userEvent.click(within(modal).getAllByRole('radio')[0]);
|
||||
|
||||
const projectSelect = getByTestId('project-sharing-select');
|
||||
expect(projectSelect).toBeVisible();
|
||||
|
||||
const projectSelectDropdownItems = await getDropdownItems(projectSelect);
|
||||
await userEvent.click(projectSelectDropdownItems[0]);
|
||||
|
||||
expect(confirmButton).toBeEnabled();
|
||||
await userEvent.click(confirmButton);
|
||||
expect(deleteUserSpy).toHaveBeenCalledWith({
|
||||
id: users[0].id,
|
||||
transferId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should show confirmation modal before deleting user and delete without transfer', async () => {
|
||||
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
const userListItem = getByTestId(`user-list-item-${users[0].email}`);
|
||||
expect(userListItem).toBeInTheDocument();
|
||||
|
||||
const actionToggle = within(userListItem).getByTestId('action-toggle');
|
||||
const actionToggleButton = within(actionToggle).getByRole('button');
|
||||
expect(actionToggleButton).toBeVisible();
|
||||
|
||||
await userEvent.click(actionToggle);
|
||||
const actionToggleId = actionToggleButton.getAttribute('aria-controls');
|
||||
|
||||
const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement;
|
||||
const actionDelete = within(actionDropdown).getByTestId('action-delete');
|
||||
await userEvent.click(actionDelete);
|
||||
|
||||
const modal = getByTestId('deleteUser-modal');
|
||||
expect(modal).toBeVisible();
|
||||
const confirmButton = within(modal).getByTestId('confirm-delete-user-button');
|
||||
expect(confirmButton).toBeDisabled();
|
||||
|
||||
await userEvent.click(within(modal).getAllByRole('radio')[1]);
|
||||
|
||||
const input = within(modal).getByRole('textbox');
|
||||
|
||||
await userEvent.type(input, 'delete all ');
|
||||
expect(confirmButton).toBeDisabled();
|
||||
|
||||
await userEvent.type(input, 'data');
|
||||
expect(confirmButton).toBeEnabled();
|
||||
|
||||
await userEvent.click(confirmButton);
|
||||
expect(deleteUserSpy).toHaveBeenCalledWith({
|
||||
id: users[0].id,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import WorkflowsView from '@/views/WorkflowsView.vue';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useProjectsStore } from '@/features/projects/projects.store';
|
||||
|
||||
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
@@ -18,12 +19,14 @@ describe('WorkflowsView', () => {
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowsView, {
|
||||
global: {
|
||||
mocks: {
|
||||
$route: {
|
||||
query: {},
|
||||
params: {},
|
||||
},
|
||||
$router: {
|
||||
replace: vi.fn(),
|
||||
@@ -54,6 +57,10 @@ describe('WorkflowsView', () => {
|
||||
|
||||
settingsStore = useSettingsStore();
|
||||
usersStore = useUsersStore();
|
||||
projectsStore = useProjectsStore();
|
||||
|
||||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(async () => {});
|
||||
|
||||
await settingsStore.getSettings();
|
||||
await usersStore.fetchUsers();
|
||||
await usersStore.loginWithCookie();
|
||||
|
||||
Reference in New Issue
Block a user