diff --git a/packages/@n8n/permissions/src/utilities/get-resource-permissions.ee.ts b/packages/@n8n/permissions/src/utilities/get-resource-permissions.ee.ts index 6c434a97a57..298b7d8fcbb 100644 --- a/packages/@n8n/permissions/src/utilities/get-resource-permissions.ee.ts +++ b/packages/@n8n/permissions/src/utilities/get-resource-permissions.ee.ts @@ -11,7 +11,7 @@ export type PermissionsRecord = { [K in keyof typeof RESOURCES]: ActionBooleans<(typeof RESOURCES)[K]>; }; -export const getResourcePermissions = (resourceScopes: Scope[] = []): PermissionsRecord => +export const getResourcePermissions = (resourceScopes: readonly Scope[] = []): PermissionsRecord => Object.keys(RESOURCES).reduce( (permissions, key) => ({ ...permissions, diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue b/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue index cd84892a86c..425463c76d0 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/MainHeader.vue @@ -288,7 +288,6 @@ async function onWorkflowDeactivated() { :id="workflow.id" :tags="workflowTags" :name="workflow.name" - :scopes="workflow.scopes" :current-folder="parentFolderForBreadcrumbs" :is-archived="workflow.isArchived" :description="workflow.description" diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.test.ts b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.test.ts index 2a9f4e7f8a9..f22c88ef269 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.test.ts +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.test.ts @@ -170,7 +170,6 @@ const defaultProps = { id: workflow.id, tags: ['1', '2'] as readonly string[], name: workflow.name, - scopes: workflow.scopes, isArchived: workflow.isArchived, description: workflow.description, }; @@ -181,6 +180,7 @@ describe('WorkflowDetails', () => { setActivePinia(docPinia); const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)); workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null }); + workflowDocumentStore.setScopes(workflow.scopes ?? []); workflowDocumentStoreRef.value = workflowDocumentStore; setActivePinia(pinia); @@ -262,11 +262,11 @@ describe('WorkflowDetails', () => { it('should not have workflow duplicate and import when branch is read-only', async () => { sourceControlStore.preferences.branchReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:read']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:read'], }, }); @@ -280,11 +280,11 @@ describe('WorkflowDetails', () => { it('should not have workflow duplicate and import when collaboration is read-only', async () => { collaborationStore.shouldBeReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:read']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:read'], }, }); @@ -296,11 +296,11 @@ describe('WorkflowDetails', () => { }); it('should have workflow duplicate and import options if permission update is true', async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:update']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:update'], }, }); @@ -326,12 +326,12 @@ describe('WorkflowDetails', () => { params: { name: 'test' }, } as unknown as ReturnType); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, id: 'new', isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -343,11 +343,11 @@ describe('WorkflowDetails', () => { }); it("should have 'Archive' option on non archived workflow", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -361,11 +361,11 @@ describe('WorkflowDetails', () => { it("should not have 'Archive' option on non archived workflow when branch is read-only", async () => { sourceControlStore.preferences.branchReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -378,11 +378,11 @@ describe('WorkflowDetails', () => { it("should not have 'Archive' option on non archived workflow when collaboration is read-only", async () => { collaborationStore.shouldBeReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -393,11 +393,11 @@ describe('WorkflowDetails', () => { }); it("should not have 'Archive' option on non archived workflow without permission", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:update']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:update'], }, }); @@ -408,11 +408,11 @@ describe('WorkflowDetails', () => { }); it("should have 'Unarchive' and 'Delete' options on archived workflow", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -427,11 +427,11 @@ describe('WorkflowDetails', () => { it("should not have 'Unarchive' or 'Delete' options on archived workflow when branch is read-only", async () => { sourceControlStore.preferences.branchReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -444,11 +444,11 @@ describe('WorkflowDetails', () => { it("should not have 'Unarchive' or 'Delete' options on archived workflow when collaboration is read-only", async () => { collaborationStore.shouldBeReadOnly = true; + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -459,11 +459,11 @@ describe('WorkflowDetails', () => { }); it("should not have 'Unarchive' or 'Delete' options on archived workflow without permission", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:update']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:update'], }, }); @@ -474,11 +474,11 @@ describe('WorkflowDetails', () => { }); it('should not have edit actions on archived workflow even with update permission', async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:update', 'workflow:delete']); const { getByTestId, queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:update', 'workflow:delete'], }, }); @@ -489,11 +489,11 @@ describe('WorkflowDetails', () => { }); it("should call onWorkflowMenuSelect on 'Archive' option click on nonactive workflow", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -527,11 +527,11 @@ describe('WorkflowDetails', () => { workflowsListStore.getWorkflowById.mockReturnValue(teamWorkflow as IWorkflowDb); workflowsStore.archiveWorkflow.mockResolvedValue(undefined); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -558,11 +558,11 @@ describe('WorkflowDetails', () => { workflowsListStore.getWorkflowById.mockReturnValue(personalWorkflow as IWorkflowDb); workflowsStore.archiveWorkflow.mockResolvedValue(undefined); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -583,11 +583,11 @@ describe('WorkflowDetails', () => { activeVersionId: 'v1', activeVersion: null, }); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); @@ -609,11 +609,11 @@ describe('WorkflowDetails', () => { }); it("should call onWorkflowMenuSelect on 'Unarchive' option click", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -627,11 +627,11 @@ describe('WorkflowDetails', () => { }); it("should call onWorkflowMenuSelect on 'Delete' option click", async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -664,11 +664,11 @@ describe('WorkflowDetails', () => { workflowsListStore.getWorkflowById.mockReturnValue(teamWorkflow as IWorkflowDb); workflowsListStore.deleteWorkflow.mockResolvedValue(undefined); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -696,11 +696,11 @@ describe('WorkflowDetails', () => { workflowsListStore.getWorkflowById.mockReturnValue(personalWorkflow as IWorkflowDb); workflowsListStore.deleteWorkflow.mockResolvedValue(undefined); + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -718,10 +718,10 @@ describe('WorkflowDetails', () => { workflowsListStore.workflowsById = { [workflow.id]: workflow }; + workflowDocumentStoreRef.value?.setScopes(['workflow:move']); const { getByTestId } = renderComponent({ props: { ...defaultProps, - scopes: ['workflow:move'], }, }); @@ -737,11 +737,11 @@ describe('WorkflowDetails', () => { describe('Archived badge', () => { it('should show badge on archived workflow', async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { getByTestId } = renderComponent({ props: { ...defaultProps, isArchived: true, - scopes: ['workflow:delete'], }, }); @@ -749,11 +749,11 @@ describe('WorkflowDetails', () => { }); it('should not show badge on non archived workflow', async () => { + workflowDocumentStoreRef.value?.setScopes(['workflow:delete']); const { queryByTestId } = renderComponent({ props: { ...defaultProps, isArchived: false, - scopes: ['workflow:delete'], }, }); diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.vue b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.vue index 833ea13b219..65cde8b86a8 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowDetails.vue @@ -56,7 +56,6 @@ const props = defineProps<{ id: IWorkflowDb['id']; tags: readonly string[]; name: IWorkflowDb['name']; - scopes: IWorkflowDb['scopes']; currentFolder?: FolderShortInfo; isArchived: IWorkflowDb['isArchived']; description?: IWorkflowDb['description']; @@ -104,7 +103,9 @@ const isNewWorkflow = computed(() => { return !workflowsStore.isWorkflowSaved[props.id]; }); -const workflowPermissions = computed(() => getResourcePermissions(props.scopes).workflow); +const workflowPermissions = computed( + () => getResourcePermissions(workflowDocumentStore?.value?.scopes).workflow, +); const readOnly = computed( () => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly, diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue index af6ebb49dc0..2f9fa25e229 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowShareModal.ee.vue @@ -24,6 +24,10 @@ import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHe import { useI18n } from '@n8n/i18n'; import { telemetry } from '@/app/plugins/telemetry'; import { useWorkflowSaving } from '@/app/composables/useWorkflowSaving'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, +} from '@/app/stores/workflowDocument.store'; import { I18nT } from 'vue-i18n'; import { N8nButton, N8nInfoTip, N8nText } from '@n8n/design-system'; @@ -87,7 +91,14 @@ const modalTitle = computed(() => { ); }); -const workflowPermissions = computed(() => getResourcePermissions(workflow.value?.scopes).workflow); +const workflowPermissions = computed(() => { + // For existing workflows, scopes come from the API response on the workflow object. + // For new unsaved workflows, scopes are only in the workflowDocument store. + const scopes = + workflow.value?.scopes ?? + useWorkflowDocumentStore(createWorkflowDocumentId(workflow.value.id)).scopes; + return getResourcePermissions(scopes).workflow; +}); const isPersonalSpaceRestricted = computed( () => diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index 2f5a100667c..54d1a506063 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -4848,10 +4848,7 @@ describe('useCanvasOperations', () => { }, }; - const getNewWorkflowDataAndMakeShareable = vi.spyOn( - workflowState, - 'getNewWorkflowDataAndMakeShareable', - ); + const getNewWorkflowData = vi.spyOn(workflowState, 'getNewWorkflowData'); const { importTemplate } = useCanvasOperations(); @@ -4874,10 +4871,7 @@ describe('useCanvasOperations', () => { disabled: false, }); expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(nodeB.name, true); - expect(getNewWorkflowDataAndMakeShareable).toHaveBeenCalledWith( - templateName, - projectsStore.currentProjectId, - ); + expect(getNewWorkflowData).toHaveBeenCalledWith(templateName, projectsStore.currentProjectId); }); }); describe('replaceNodeParameters', () => { @@ -5521,19 +5515,13 @@ describe('useCanvasOperations', () => { workflow: { nodes: [], connections: {} }, }); - const getNewWorkflowDataAndMakeShareable = vi.spyOn( - workflowState, - 'getNewWorkflowDataAndMakeShareable', - ); + const getNewWorkflowData = vi.spyOn(workflowState, 'getNewWorkflowData'); const { openWorkflowTemplate } = useCanvasOperations(); await openWorkflowTemplate('template-id'); expect(templatesStore.getFixedWorkflowTemplate).toHaveBeenCalledWith('template-id'); - expect(getNewWorkflowDataAndMakeShareable).toHaveBeenCalledWith( - 'Template Name', - 'test-project-id', - ); + expect(getNewWorkflowData).toHaveBeenCalledWith('Template Name', 'test-project-id'); expect(telemetry.track).toHaveBeenCalledWith('User inserted workflow template', { source: 'workflow', @@ -5589,18 +5577,12 @@ describe('useCanvasOperations', () => { meta: { templateId: 'template-id' }, }; - const getNewWorkflowDataAndMakeShareable = vi.spyOn( - workflowState, - 'getNewWorkflowDataAndMakeShareable', - ); + const getNewWorkflowData = vi.spyOn(workflowState, 'getNewWorkflowData'); const { openWorkflowTemplateFromJSON } = useCanvasOperations(); await openWorkflowTemplateFromJSON(template); - expect(getNewWorkflowDataAndMakeShareable).toHaveBeenCalledWith( - 'Template Name', - 'test-project-id', - ); + expect(getNewWorkflowData).toHaveBeenCalledWith('Template Name', 'test-project-id'); expect(router.replace).toHaveBeenCalledWith({ name: VIEWS.NEW_WORKFLOW, diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index fa405be17c6..b67d8b2ada6 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -2975,7 +2975,7 @@ export function useCanvasOperations() { workflowsStore.setConnections(workflow.connections); } await addNodes(convertedNodes ?? [], { keepPristine: true }); - await workflowState.getNewWorkflowDataAndMakeShareable(name, projectsStore.currentProjectId); + await workflowState.getNewWorkflowData(name, projectsStore.currentProjectId); } function tryToOpenSubworkflowInNewTab(nodeId: string): boolean { diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts index 2c7cd4dd83a..a60da474d55 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.test.ts @@ -249,7 +249,6 @@ describe('useWorkflowHelpers', () => { const setWorkflowIdSpy = vi.spyOn(workflowState, 'setWorkflowId'); const setWorkflowNameSpy = vi.spyOn(workflowState, 'setWorkflowName'); const setWorkflowVersionDataSpy = vi.spyOn(workflowsStore, 'setWorkflowVersionData'); - const setWorkflowScopesSpy = vi.spyOn(workflowState, 'setWorkflowScopes'); const setUsedCredentialsSpy = vi.spyOn(workflowsStore, 'setUsedCredentials'); const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith'); const upsertTagsSpy = vi.spyOn(tagsStore, 'upsertTags'); @@ -267,7 +266,6 @@ describe('useWorkflowHelpers', () => { name: null, description: null, }); - expect(setWorkflowScopesSpy).toHaveBeenCalledWith(['workflow:create']); expect(setUsedCredentialsSpy).toHaveBeenCalledWith([]); expect(setWorkflowSharedWithSpy).toHaveBeenCalledWith({ workflowId: '1', diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index 80b7938dd43..7a3fa957f6f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -982,7 +982,6 @@ export function useWorkflowHelpers() { name: null, description: null, }); - ws.setWorkflowScopes(workflowData.scopes); if ('activeVersion' in workflowData) { workflowsStore.setWorkflowActiveVersion(workflowData.activeVersion ?? null); @@ -1043,6 +1042,7 @@ export function useWorkflowHelpers() { workflowDocumentStore.setChecksum(workflowData.checksum); } workflowDocumentStore.setMeta(workflowData.meta); + workflowDocumentStore.setScopes(workflowData.scopes ?? []); tagsStore.upsertTags(tags); return { workflowDocumentStore }; diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts index 69c83740311..72a0de8f0b2 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts @@ -265,7 +265,7 @@ export function useWorkflowInitialization(workflowState: WorkflowState) { const parentFolderId = route.query.parentFolderId as string | undefined; - await workflowState.getNewWorkflowDataAndMakeShareable( + await workflowState.getNewWorkflowData( undefined, projectsStore.currentProjectId, parentFolderId, @@ -280,6 +280,12 @@ export function useWorkflowInitialization(workflowState: WorkflowState) { currentWorkflowDocumentStore.value.setHomeProject(homeProject); await projectsStore.refreshCurrentProject(); + + const { currentProject, personalProject } = projectsStore; + currentWorkflowDocumentStore.value.setScopes( + currentProject?.scopes ?? personalProject?.scopes ?? [], + ); + await fetchAndSetParentFolder(parentFolderId); uiStore.nodeViewInitialized = true; diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts index 943d553c973..c2b8b3bc020 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts @@ -79,10 +79,13 @@ export function useWorkflowSaving({ cancel?: () => Promise; } = {}, ) { + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); if ( !uiStore.stateIsDirty || workflowsStore.workflow.isArchived || - !getResourcePermissions(workflowsStore.workflow.scopes).workflow.update + !getResourcePermissions(workflowDocumentStore.scopes).workflow.update ) { next(); return; diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts index a73808cd86f..2be7f4f3ca6 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowState.ts @@ -27,7 +27,6 @@ import { inject } from 'vue'; import * as workflowsApi from '@/app/api/workflows'; import { useRootStore } from '@n8n/stores/useRootStore'; import { isEmpty } from '@/app/utils/typesUtils'; -import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; import { clearPopupWindowState } from '@/features/execution/executions/executions.utils'; import { DEFAULT_SETTINGS } from '@/app/stores/workflowDocument/useWorkflowDocumentSettings'; import { useDocumentTitle } from './useDocumentTitle'; @@ -164,27 +163,6 @@ export function useWorkflowState() { return workflowData; } - function makeNewWorkflowShareable() { - const { currentProject, personalProject } = useProjectsStore(); - const scopes = currentProject?.scopes ?? personalProject?.scopes ?? []; - - ws.workflow.scopes = scopes; - } - - async function getNewWorkflowDataAndMakeShareable( - name?: string, - projectId?: string, - parentFolderId?: string, - ): Promise { - const workflowData = await getNewWorkflowData(name, projectId, parentFolderId); - makeNewWorkflowShareable(); - return workflowData; - } - - function setWorkflowScopes(scopes: IWorkflowDb['scopes']): void { - ws.workflow.scopes = scopes; - } - //// // Execution //// @@ -437,8 +415,7 @@ export function useWorkflowState() { setWorkflowName, setWorkflowProperty, setActiveExecutionId, - getNewWorkflowDataAndMakeShareable, - setWorkflowScopes, + getNewWorkflowData, // Execution markExecutionAsStopped, diff --git a/packages/frontend/editor-ui/src/app/layouts/WorkflowLayout.test.ts b/packages/frontend/editor-ui/src/app/layouts/WorkflowLayout.test.ts index 377a15cb091..e54c4a8b1f0 100644 --- a/packages/frontend/editor-ui/src/app/layouts/WorkflowLayout.test.ts +++ b/packages/frontend/editor-ui/src/app/layouts/WorkflowLayout.test.ts @@ -35,7 +35,7 @@ vi.mock('@/features/ai/assistant/assistant.store', () => ({ vi.mock('@/app/composables/useWorkflowState', () => ({ useWorkflowState: vi.fn(() => ({ - getNewWorkflowDataAndMakeShareable: vi.fn(), + getNewWorkflowData: vi.fn(), setWorkflowId: vi.fn(), resetState: vi.fn(), })), diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts index 658803d917d..5350053736b 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts @@ -7,6 +7,7 @@ import { useWorkflowDocumentHomeProject } from './workflowDocument/useWorkflowDo import { useWorkflowDocumentChecksum } from './workflowDocument/useWorkflowDocumentChecksum'; import { useWorkflowDocumentMeta } from './workflowDocument/useWorkflowDocumentMeta'; import { useWorkflowDocumentPinData } from './workflowDocument/useWorkflowDocumentPinData'; +import { useWorkflowDocumentScopes } from './workflowDocument/useWorkflowDocumentScopes'; import { useWorkflowDocumentSettings } from './workflowDocument/useWorkflowDocumentSettings'; import { useWorkflowDocumentTags } from './workflowDocument/useWorkflowDocumentTags'; import { useWorkflowDocumentTimestamps } from './workflowDocument/useWorkflowDocumentTimestamps'; @@ -57,6 +58,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { const workflowDocumentMeta = useWorkflowDocumentMeta(); const workflowDocumentTags = useWorkflowDocumentTags(); const workflowDocumentPinData = useWorkflowDocumentPinData(); + const workflowDocumentScopes = useWorkflowDocumentScopes(); const workflowDocumentTimestamps = useWorkflowDocumentTimestamps(); const workflowDocumentSettings = useWorkflowDocumentSettings(); @@ -70,6 +72,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { ...workflowDocumentSettings, ...workflowDocumentTags, ...workflowDocumentPinData, + ...workflowDocumentScopes, ...workflowDocumentTimestamps, }; })(); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.test.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.test.ts new file mode 100644 index 00000000000..16b0041b6aa --- /dev/null +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, vi } from 'vitest'; +import { useWorkflowDocumentScopes } from './useWorkflowDocumentScopes'; + +function createScopes() { + return useWorkflowDocumentScopes(); +} + +describe('useWorkflowDocumentScopes', () => { + describe('initial state', () => { + it('should start with empty scopes', () => { + const { scopes } = createScopes(); + expect(scopes.value).toEqual([]); + }); + }); + + describe('setScopes', () => { + it('should set scopes and fire event hook', () => { + const { scopes, setScopes, onScopesChange } = createScopes(); + const hookSpy = vi.fn(); + onScopesChange(hookSpy); + + setScopes(['workflow:read', 'workflow:update']); + + expect(scopes.value).toEqual(['workflow:read', 'workflow:update']); + expect(hookSpy).toHaveBeenCalledWith({ + action: 'update', + payload: { scopes: ['workflow:read', 'workflow:update'] }, + }); + }); + + it('should clear scopes with empty array', () => { + const { scopes, setScopes } = createScopes(); + setScopes(['workflow:read']); + + setScopes([]); + + expect(scopes.value).toEqual([]); + }); + + it('should replace existing scopes', () => { + const { scopes, setScopes } = createScopes(); + setScopes(['workflow:read']); + + setScopes(['workflow:update', 'workflow:delete']); + + expect(scopes.value).toEqual(['workflow:update', 'workflow:delete']); + }); + + it('should fire event hook on every call', () => { + const { setScopes, onScopesChange } = createScopes(); + const hookSpy = vi.fn(); + onScopesChange(hookSpy); + + setScopes(['workflow:read']); + setScopes([]); + + expect(hookSpy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.ts new file mode 100644 index 00000000000..896ae4beb90 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentScopes.ts @@ -0,0 +1,32 @@ +import { readonly, ref } from 'vue'; +import { createEventHook } from '@vueuse/core'; +import type { Scope } from '@n8n/permissions'; +import { CHANGE_ACTION } from './types'; +import type { ChangeAction, ChangeEvent } from './types'; + +export type ScopesPayload = { + scopes: Scope[]; +}; + +export type ScopesChangeEvent = ChangeEvent; + +export function useWorkflowDocumentScopes() { + const scopes = ref([]); + + const onScopesChange = createEventHook(); + + function applyScopes(newScopes: Scope[], action: ChangeAction = CHANGE_ACTION.UPDATE) { + scopes.value = newScopes; + void onScopesChange.trigger({ action, payload: { scopes: newScopes } }); + } + + function setScopes(newScopes: Scope[]) { + applyScopes(newScopes); + } + + return { + scopes: readonly(scopes), + setScopes, + onScopesChange: onScopesChange.on, + }; +} diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts index 34b88d60534..13dbeb3e578 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.test.ts @@ -2219,8 +2219,12 @@ describe('useWorkflowsStore', () => { // Add workflow to workflowsById to simulate it being loaded from backend workflowsListStore.addWorkflow(testWorkflow); - // Verify the mock is set up correctly - expect(workflowsStore.workflow.scopes).toContain('workflow:update'); + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(testWorkflow.id), + ); + workflowDocumentStore.setScopes(testWorkflow.scopes ?? []); + + expect(workflowDocumentStore.scopes).toContain('workflow:update'); expect(workflowsStore.workflow.id).toBe('workflow-123'); expect(workflowsStore.workflow.isArchived).toBe(false); @@ -2248,6 +2252,11 @@ describe('useWorkflowsStore', () => { // Add workflow to workflowsById to simulate it being loaded from backend workflowsListStore.addWorkflow(testWorkflow); + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(testWorkflow.id), + ); + workflowDocumentStore.setScopes(testWorkflow.scopes ?? []); + vi.mocked(workflowsApi).getLastSuccessfulExecution.mockResolvedValue(null); await workflowsStore.fetchLastSuccessfulExecution(); @@ -2272,6 +2281,11 @@ describe('useWorkflowsStore', () => { // Add workflow to workflowsById to simulate it being loaded from backend workflowsListStore.addWorkflow(testWorkflow); + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(testWorkflow.id), + ); + workflowDocumentStore.setScopes(testWorkflow.scopes ?? []); + const error = new Error('API Error'); vi.mocked(workflowsApi).getLastSuccessfulExecution.mockRejectedValue(error); diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts index dbbf07f05a4..c642287b802 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -599,7 +599,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { } async function fetchLastSuccessfulExecution() { - const workflowPermissions = getResourcePermissions(workflow.value.scopes).workflow; + const workflowDocumentStore = workflowId.value + ? useWorkflowDocumentStore(createWorkflowDocumentId(workflowId.value)) + : undefined; + const workflowPermissions = getResourcePermissions(workflowDocumentStore?.scopes).workflow; try { if ( diff --git a/packages/frontend/editor-ui/src/app/views/NodeView.vue b/packages/frontend/editor-ui/src/app/views/NodeView.vue index c7539919f11..1ff7be17f21 100644 --- a/packages/frontend/editor-ui/src/app/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/app/views/NodeView.vue @@ -1640,7 +1640,7 @@ onBeforeRouteLeave(async (to, from, next) => { return; } - await useWorkflowSaving({ router }).promptSaveUnsavedWorkflowChanges(next, { + await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { async confirm() { if (from.name === VIEWS.NEW_WORKFLOW) { const savedWorkflowId = workflowsStore.workflowId; diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.test.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.test.ts index 4e139f459f0..7fad0b7097f 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.test.ts @@ -36,6 +36,11 @@ vi.mock('@/features/workflows/canvas/canvas.eventBus', () => ({ }, })); +vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({ + ...(await importOriginal()), + injectWorkflowDocumentStore: vi.fn().mockReturnValue(null), +})); + const mockGenerateMergedNodesAndActionsFn = vi.fn().mockReturnValue({ mergedNodes: [] }); vi.mock('@/features/shared/nodeCreator/composables/useActionsGeneration', () => ({ diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts index 24609862627..1ff1bfc4026 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useNodeCommands.ts @@ -10,6 +10,10 @@ import { type CommandBarItem } from '@n8n/design-system/components/N8nCommandBar import type { CommandGroup } from '../types'; import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, +} from '@/app/stores/workflowDocument.store'; import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store'; import { getResourcePermissions } from '@n8n/permissions'; import NodeIcon from '@/app/components/NodeIcon.vue'; @@ -37,13 +41,17 @@ export function useNodeCommands(options: { const collaborationStore = useCollaborationStore(); const { generateMergedNodesAndActions } = useActionsGenerator(); + const workflowDocumentStore = computed(() => + useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), + ); + const isReadOnly = computed( () => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly, ); const isArchived = computed(() => workflowsStore.workflow.isArchived); const workflowPermissions = computed( - () => getResourcePermissions(workflowsStore.workflow.scopes).workflow, + () => getResourcePermissions(workflowDocumentStore.value.scopes).workflow, ); const hasPermission = (permission: keyof typeof workflowPermissions.value) => diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.test.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.test.ts index b1601f55bbf..53ae655a774 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.test.ts @@ -7,16 +7,25 @@ import { useUIStore } from '@/app/stores/ui.store'; import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, + injectWorkflowDocumentStore, +} from '@/app/stores/workflowDocument.store'; import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; import { nodeViewEventBus } from '@/app/event-bus'; import { createTestWorkflow } from '@/__tests__/mocks'; import type { IWorkflowDb, INodeUi } from '@/Interface'; -import type { Ref } from 'vue'; +import { shallowRef, type Ref } from 'vue'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; vi.mock('@/app/composables/useCanvasOperations'); vi.mock('@/app/composables/useWorkflowHelpers'); +vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({ + ...(await importOriginal()), + injectWorkflowDocumentStore: vi.fn(), +})); vi.mock('@/app/composables/useTelemetry'); vi.mock('@/app/composables/useWorkflowSaving'); vi.mock('@/app/composables/useRunWorkflow'); @@ -61,6 +70,7 @@ vi.mock('@/app/composables/useWorkflowSaving', () => ({ describe('useWorkflowCommands', () => { let mockWorkflow: Ref; + let mockWorkflowDocumentStore: ReturnType; let mockUIStore: ReturnType; let mockTagsStore: ReturnType; let mockWorkflowsStore: ReturnType; @@ -68,7 +78,7 @@ describe('useWorkflowCommands', () => { let mockSourceControlStore: ReturnType; beforeEach(() => { - setActivePinia(createTestingPinia()); + setActivePinia(createTestingPinia({ stubActions: false })); mockWorkflow = ref( createTestWorkflow({ @@ -95,6 +105,12 @@ describe('useWorkflowCommands', () => { // Mark workflow as existing by adding it to workflowsById mockWorkflowsListStore.workflowsById = { [mockWorkflow.value.id]: mockWorkflow.value }; + mockWorkflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(mockWorkflow.value.id), + ); + mockWorkflowDocumentStore.setScopes(mockWorkflow.value.scopes ?? []); + vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(mockWorkflowDocumentStore)); + Object.defineProperty(mockUIStore, 'isActionActive', { value: { workflowSaving: false } as unknown as typeof mockUIStore.isActionActive, }); @@ -230,7 +246,7 @@ describe('useWorkflowCommands', () => { describe('duplicate workflow', () => { it('should not include duplicate command when user lacks create permission', () => { - mockWorkflowsStore.workflow.scopes = ['workflow:read', 'workflow:update']; + mockWorkflowDocumentStore.setScopes(['workflow:read', 'workflow:update']); const { commands } = useWorkflowCommands(); const duplicateCommand = commands.value.find((cmd) => cmd.id === 'duplicate-workflow'); @@ -402,7 +418,7 @@ describe('useWorkflowCommands', () => { }); it('should not show lifecycle commands without delete permission', () => { - mockWorkflowsStore.workflow.scopes = ['workflow:read', 'workflow:update']; + mockWorkflowDocumentStore.setScopes(['workflow:read', 'workflow:update']); const { commands } = useWorkflowCommands(); const archiveCommand = commands.value.find((cmd) => cmd.id === 'archive-workflow'); @@ -457,7 +473,7 @@ describe('useWorkflowCommands', () => { it('should allow actions for new workflows regardless of permissions', () => { // For new workflows, remove from workflowsById so isNewWorkflow returns true mockWorkflowsListStore.workflowsById = {}; - mockWorkflowsStore.workflow.scopes = ['workflow:read']; + mockWorkflowDocumentStore.setScopes(['workflow:read']); const { commands } = useWorkflowCommands(); const saveCommand = commands.value.find((cmd) => cmd.id === 'rename-workflow'); diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts index b386823456a..8467f8fd944 100644 --- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts +++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useWorkflowCommands.ts @@ -24,6 +24,10 @@ import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; import type { IWorkflowToShare } from '@/Interface'; import { saveAs } from 'file-saver'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, +} from '@/app/stores/workflowDocument.store'; import type { CommandGroup, CommandBarItem } from '../types'; import uniqBy from 'lodash/uniqBy'; import { nodeViewEventBus } from '@/app/event-bus'; @@ -59,6 +63,10 @@ export function useWorkflowCommands(): CommandGroup { const sourceControlStore = useSourceControlStore(); const collaborationStore = useCollaborationStore(); + const workflowDocumentStore = computed(() => + useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)), + ); + const router = useRouter(); const runWorkflow = useRunWorkflow({ router }); @@ -72,7 +80,7 @@ export function useWorkflowCommands(): CommandGroup { const isArchived = computed(() => workflowsStore.workflow.isArchived); const workflowPermissions = computed( - () => getResourcePermissions(workflowsStore.workflow.scopes).workflow, + () => getResourcePermissions(workflowDocumentStore.value.scopes).workflow, ); const hasPermission = (permission: keyof typeof workflowPermissions.value) => diff --git a/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenu.test.ts b/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenu.test.ts index 7f6249c623b..540c9a25875 100644 --- a/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenu.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenu.test.ts @@ -44,7 +44,7 @@ describe('useContextMenu', () => { let sourceControlStore: ReturnType; let uiStore: ReturnType; let workflowsStore: ReturnType; - let documentStore: ReturnType; + let workflowDocumentStore: ReturnType; const nodes = [nodeFactory(), nodeFactory(), nodeFactory()]; const selectedNodes = nodes.slice(0, 2); const testWorkflowId = 'test-workflow-id'; @@ -62,9 +62,9 @@ describe('useContextMenu', () => { workflowsStore = useWorkflowsStore(); workflowsStore.workflow.id = testWorkflowId; workflowsStore.workflow.nodes = nodes; - workflowsStore.workflow.scopes = ['workflow:update']; - documentStore = useWorkflowDocumentStore(createWorkflowDocumentId(testWorkflowId)); - vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(documentStore)); + workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(testWorkflowId)); + workflowDocumentStore.setScopes(['workflow:update']); + vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocumentStore)); workflowsStore.workflowObject = { nodes, getNode: (_: string) => { @@ -235,7 +235,7 @@ describe('useContextMenu', () => { const { open, isOpen, actions, targetNodeIds } = useContextMenu(); const webhookNode = nodeFactory({ type: WEBHOOK_NODE_TYPE, webhookId: 'test-webhook' }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(webhookNode); - documentStore.setActiveState({ activeVersionId: null, activeVersion: null }); + workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null }); open(mockEvent, { source: 'node-right-click', nodeId: webhookNode.id }); @@ -255,7 +255,7 @@ describe('useContextMenu', () => { const { open, isOpen, actions, targetNodeIds } = useContextMenu(); const webhookNode = nodeFactory({ type: WEBHOOK_NODE_TYPE, webhookId: 'test-webhook' }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(webhookNode); - documentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); + workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); open(mockEvent, { source: 'node-right-click', nodeId: webhookNode.id }); @@ -280,7 +280,7 @@ describe('useContextMenu', () => { webhookId: 'chat-webhook', }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(chatTriggerNode); - documentStore.setActiveState({ activeVersionId: null, activeVersion: null }); + workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null }); open(mockEvent, { source: 'node-right-click', nodeId: chatTriggerNode.id }); @@ -301,7 +301,7 @@ describe('useContextMenu', () => { webhookId: 'chat-webhook', }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(chatTriggerNode); - documentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); + workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); open(mockEvent, { source: 'node-right-click', nodeId: chatTriggerNode.id }); @@ -321,7 +321,7 @@ describe('useContextMenu', () => { const { open, isOpen, actions, targetNodeIds } = useContextMenu(); const regularNode = nodeFactory({ type: NO_OP_NODE_TYPE }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(regularNode); - documentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); + workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null }); open(mockEvent, { source: 'node-right-click', nodeId: regularNode.id }); @@ -350,7 +350,7 @@ describe('useContextMenu', () => { describe('Read-only mode', () => { it('should return the correct actions when right clicking a sticky', () => { vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(true); - workflowsStore.workflow.scopes = ['workflow:read']; + workflowDocumentStore.setScopes(['workflow:read']); const { open, isOpen, actions, targetNodeIds } = useContextMenu(); const sticky = nodeFactory({ type: STICKY_NODE_TYPE }); vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(sticky); diff --git a/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts b/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts index 136805f709b..328ccacd8dc 100644 --- a/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts +++ b/packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts @@ -51,7 +51,7 @@ export function useContextMenuItems(targetNodeIds: ComputedRef): Compu const uiStore = useUIStore(); const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); - const documentStore = injectWorkflowDocumentStore(); + const workflowDocumentStore = injectWorkflowDocumentStore(); const sourceControlStore = useSourceControlStore(); const collaborationStore = useCollaborationStore(); const focusedNodesStore = useFocusedNodesStore(); @@ -60,7 +60,7 @@ export function useContextMenuItems(targetNodeIds: ComputedRef): Compu const workflowObject = computed(() => workflowsStore.workflowObject as Workflow); const workflowPermissions = computed( - () => getResourcePermissions(workflowsStore.workflow.scopes).workflow, + () => getResourcePermissions(workflowDocumentStore?.value?.scopes).workflow, ); const isReadOnly = computed( @@ -252,7 +252,7 @@ export function useContextMenuItems(targetNodeIds: ComputedRef): Compu if (isWebhookNode(nodes[0])) { const isProductionOnly = PRODUCTION_ONLY_TRIGGER_NODE_TYPES.includes(nodes[0].type); - const isWorkflowActive = documentStore?.value?.active ?? false; + const isWorkflowActive = workflowDocumentStore?.value?.active ?? false; if (!isProductionOnly) { copyWebhookActions.push({ divided: true,