mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 16:57:08 +02:00
fix(editor): Missing duplicate workflow action on workflow list (#22230)
This commit is contained in:
parent
4832042b16
commit
000cccb627
|
|
@ -6,13 +6,16 @@ import { type MockedStore, mockedStore } from '@/__tests__/utils';
|
|||
import { MODAL_CONFIRM, VIEWS } from '@/app/constants';
|
||||
import WorkflowCard from '@/app/components/WorkflowCard.vue';
|
||||
import type { WorkflowResource } from '@/Interface';
|
||||
import type { IUser } from '@n8n/rest-api-client/api/users';
|
||||
import * as vueRouter from 'vue-router';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import type { ProjectListItem } from '@/features/collaboration/projects/projects.types';
|
||||
import { useMessage } from '@/app/composables/useMessage';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
|
|
@ -73,6 +76,7 @@ describe('WorkflowCard', () => {
|
|||
let projectsStore: MockedStore<typeof useProjectsStore>;
|
||||
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||
let workflowsStore: MockedStore<typeof useWorkflowsStore>;
|
||||
let usersStore: MockedStore<typeof useUsersStore>;
|
||||
let message: ReturnType<typeof useMessage>;
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
|
||||
|
|
@ -81,6 +85,7 @@ describe('WorkflowCard', () => {
|
|||
projectsStore = mockedStore(useProjectsStore);
|
||||
settingsStore = mockedStore(useSettingsStore);
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
usersStore = mockedStore(useUsersStore);
|
||||
message = useMessage();
|
||||
toast = useToast();
|
||||
|
||||
|
|
@ -631,4 +636,143 @@ describe('WorkflowCard', () => {
|
|||
expect(queryByTestId('workflow-card-archived')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('workflow-card-activator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'Duplicate' action when user has read permission and can create workflows", async () => {
|
||||
const data = createWorkflow({
|
||||
scopes: ['workflow:read'],
|
||||
isArchived: false,
|
||||
});
|
||||
|
||||
vi.spyOn(vueRouter, 'useRoute').mockReturnValueOnce({
|
||||
name: VIEWS.PROJECTS,
|
||||
} as vueRouter.RouteLocationNormalizedLoadedGeneric);
|
||||
|
||||
// Mock user with global workflow create permission
|
||||
usersStore.currentUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isDefaultUser: false,
|
||||
isPendingUser: false,
|
||||
mfaEnabled: false,
|
||||
globalScopes: ['workflow:create'],
|
||||
} as IUser;
|
||||
|
||||
const { getByTestId } = renderComponent({ props: { data } });
|
||||
const cardActions = getByTestId('workflow-card-actions');
|
||||
|
||||
expect(cardActions).toBeInTheDocument();
|
||||
|
||||
const cardActionsOpener = within(cardActions).getByRole('button');
|
||||
expect(cardActionsOpener).toBeInTheDocument();
|
||||
|
||||
const controllingId = cardActionsOpener.getAttribute('aria-controls');
|
||||
|
||||
await userEvent.click(cardActions);
|
||||
const actions = document.querySelector<HTMLElement>(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
expect(actions).toHaveTextContent('Duplicate');
|
||||
});
|
||||
|
||||
it("should show 'Duplicate' action when user has project-level create workflow permission", async () => {
|
||||
const projectId = 'project-123';
|
||||
const data = createWorkflow({
|
||||
scopes: ['workflow:read'],
|
||||
isArchived: false,
|
||||
homeProject: {
|
||||
id: projectId,
|
||||
name: 'Test Project',
|
||||
},
|
||||
});
|
||||
|
||||
vi.spyOn(vueRouter, 'useRoute').mockReturnValueOnce({
|
||||
name: VIEWS.PROJECTS,
|
||||
} as vueRouter.RouteLocationNormalizedLoadedGeneric);
|
||||
|
||||
// Mock user without global workflow create permission
|
||||
usersStore.currentUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isDefaultUser: false,
|
||||
isPendingUser: false,
|
||||
mfaEnabled: false,
|
||||
globalScopes: [],
|
||||
} as IUser;
|
||||
|
||||
// Mock project with workflow create permission
|
||||
projectsStore.myProjects = [
|
||||
{
|
||||
id: projectId,
|
||||
name: 'Test Project',
|
||||
icon: null,
|
||||
type: 'team',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
role: 'project:editor',
|
||||
scopes: ['workflow:create'],
|
||||
} as ProjectListItem,
|
||||
];
|
||||
|
||||
const { getByTestId } = renderComponent({ props: { data } });
|
||||
const cardActions = getByTestId('workflow-card-actions');
|
||||
|
||||
expect(cardActions).toBeInTheDocument();
|
||||
|
||||
const cardActionsOpener = within(cardActions).getByRole('button');
|
||||
expect(cardActionsOpener).toBeInTheDocument();
|
||||
|
||||
const controllingId = cardActionsOpener.getAttribute('aria-controls');
|
||||
|
||||
await userEvent.click(cardActions);
|
||||
const actions = document.querySelector<HTMLElement>(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
expect(actions).toHaveTextContent('Duplicate');
|
||||
});
|
||||
|
||||
it("should not show 'Duplicate' action when user does not have create workflow permission", async () => {
|
||||
const data = createWorkflow({
|
||||
scopes: ['workflow:read'],
|
||||
isArchived: false,
|
||||
});
|
||||
|
||||
vi.spyOn(vueRouter, 'useRoute').mockReturnValueOnce({
|
||||
name: VIEWS.PROJECTS,
|
||||
} as vueRouter.RouteLocationNormalizedLoadedGeneric);
|
||||
|
||||
// Mock user without workflow create permission
|
||||
usersStore.currentUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isDefaultUser: false,
|
||||
isPendingUser: false,
|
||||
mfaEnabled: false,
|
||||
globalScopes: [],
|
||||
} as IUser;
|
||||
|
||||
const { getByTestId } = renderComponent({ props: { data } });
|
||||
const cardActions = getByTestId('workflow-card-actions');
|
||||
|
||||
expect(cardActions).toBeInTheDocument();
|
||||
|
||||
const cardActionsOpener = within(cardActions).getByRole('button');
|
||||
expect(cardActionsOpener).toBeInTheDocument();
|
||||
|
||||
const controllingId = cardActionsOpener.getAttribute('aria-controls');
|
||||
|
||||
await userEvent.click(cardActions);
|
||||
const actions = document.querySelector<HTMLElement>(`#${controllingId}`);
|
||||
if (!actions) {
|
||||
throw new Error('Actions menu not found');
|
||||
}
|
||||
expect(actions).not.toHaveTextContent('Duplicate');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -121,6 +121,20 @@ const resourceTypeLabel = computed(() => locale.baseText('generic.workflow').toL
|
|||
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||
const workflowPermissions = computed(() => getResourcePermissions(props.data.scopes).workflow);
|
||||
|
||||
const globalPermissions = computed(
|
||||
() => getResourcePermissions(usersStore.currentUser?.globalScopes).workflow,
|
||||
);
|
||||
const projectPermissions = computed(
|
||||
() =>
|
||||
getResourcePermissions(
|
||||
projectsStore.myProjects?.find((p) => props.data.homeProject?.id === p.id)?.scopes,
|
||||
).workflow,
|
||||
);
|
||||
|
||||
const canCreateWorkflow = computed(
|
||||
() => globalPermissions.value.create ?? projectPermissions.value.create,
|
||||
);
|
||||
|
||||
const showFolders = computed(() => {
|
||||
return props.areFoldersEnabled && route.name !== VIEWS.WORKFLOWS;
|
||||
});
|
||||
|
|
@ -168,7 +182,12 @@ const actions = computed(() => {
|
|||
},
|
||||
];
|
||||
|
||||
if (workflowPermissions.value.create && !props.readOnly && !props.data.isArchived) {
|
||||
if (
|
||||
workflowPermissions.value.read &&
|
||||
canCreateWorkflow.value &&
|
||||
!props.readOnly &&
|
||||
!props.data.isArchived
|
||||
) {
|
||||
items.push({
|
||||
label: locale.baseText('workflows.item.duplicate'),
|
||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user