From 48df1de8c93fdcd2bda58ba09323a61587ba159d Mon Sep 17 00:00:00 2001 From: Daria Date: Tue, 9 Sep 2025 10:03:10 +0300 Subject: [PATCH] fix(editor): Respect project id and parent folder for callouts (no-changelog) (#19318) --- .../src/composables/useCalloutHelpers.test.ts | 59 ++++++++++++++++++- .../src/composables/useCalloutHelpers.ts | 8 ++- .../frontend/editor-ui/src/views/NodeView.vue | 10 +++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/frontend/editor-ui/src/composables/useCalloutHelpers.test.ts b/packages/frontend/editor-ui/src/composables/useCalloutHelpers.test.ts index 10364f084b..947e05bd71 100644 --- a/packages/frontend/editor-ui/src/composables/useCalloutHelpers.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCalloutHelpers.test.ts @@ -4,7 +4,7 @@ import { createTestingPinia } from '@pinia/testing'; import { PrebuiltAgentTemplates, SampleTemplates } from '@/utils/templates/workflowSamples'; import { useNDVStore } from '@/stores/ndv.store'; import { mockedStore } from '@/__tests__/utils'; -import { NODE_CREATOR_OPEN_SOURCES } from '@/constants'; +import { NODE_CREATOR_OPEN_SOURCES, VIEWS } from '@/constants'; import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks'; @@ -64,6 +64,12 @@ vi.mock('@n8n/rest-api-client/api/users', () => ({ updateCurrentUserSettings: vi.fn(), })); +vi.mock('@/stores/projects.store', () => ({ + useProjectsStore: () => ({ + currentProjectId: 'test-project-id', + }), +})); + let ndvStore: ReturnType>; let nodeCreatorStore: ReturnType>; let viewStacks: ReturnType>; @@ -134,6 +140,57 @@ describe('useCalloutHelpers()', () => { expect(mocks.track).not.toHaveBeenCalled(); }); + it('includes project ID in template URL when opening template', () => { + vi.spyOn(window, 'open').mockImplementation(() => null); + mocks.resolve.mockReturnValue({ href: 'n8n.io/template/test' }); + + const { openSampleWorkflowTemplate } = useCalloutHelpers(); + + openSampleWorkflowTemplate(SampleTemplates.RagStarterTemplate, { + telemetry: { + source: 'ndv', + nodeType: 'testNode', + }, + }); + + expect(mocks.resolve).toHaveBeenCalledWith({ + name: VIEWS.TEMPLATE_IMPORT, + params: { id: SampleTemplates.RagStarterTemplate }, + query: { + fromJson: 'true', + projectId: 'test-project-id', + }, + }); + }); + + it('includes folder ID in template URL when opening template', () => { + vi.spyOn(window, 'open').mockImplementation(() => null); + mocks.resolve.mockReturnValue({ href: 'n8n.io/template/test' }); + mocks.useRoute.mockReturnValueOnce({ + query: {}, + params: { folderId: 'my-folder-id' }, + }); + + const { openSampleWorkflowTemplate } = useCalloutHelpers(); + + openSampleWorkflowTemplate(SampleTemplates.EasyAiTemplate, { + telemetry: { + source: 'ndv', + nodeType: 'testNode', + }, + }); + + expect(mocks.resolve).toHaveBeenCalledWith({ + name: VIEWS.TEMPLATE_IMPORT, + params: { id: SampleTemplates.EasyAiTemplate }, + query: { + fromJson: 'true', + projectId: 'test-project-id', + parentFolderId: 'my-folder-id', + }, + }); + }); + it.each(Object.values(PrebuiltAgentTemplates))( 'opens pre-built agent template %s from NDV successfully', (templateId) => { diff --git a/packages/frontend/editor-ui/src/composables/useCalloutHelpers.ts b/packages/frontend/editor-ui/src/composables/useCalloutHelpers.ts index 4dbc68efc8..4c4d79ee81 100644 --- a/packages/frontend/editor-ui/src/composables/useCalloutHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useCalloutHelpers.ts @@ -29,6 +29,7 @@ import { } from '@/utils/templates/workflowSamples'; import type { INodeCreateElement, OpenTemplateElement } from '@/Interface'; import { useUIStore } from '@/stores/ui.store'; +import { useProjectsStore } from '@/stores/projects.store'; export function useCalloutHelpers() { const route = useRoute(); @@ -45,6 +46,7 @@ export function useCalloutHelpers() { const viewStacks = useViewStacks(); const nodeTypesStore = useNodeTypesStore(); const uiStore = useUIStore(); + const projectsStore = useProjectsStore(); const isRagStarterCalloutVisible = computed(() => { const template = getRagStarterWorkflowJson(); @@ -200,7 +202,11 @@ export function useCalloutHelpers() { const { href } = router.resolve({ name: VIEWS.TEMPLATE_IMPORT, params: { id: template.meta.templateId }, - query: { fromJson: 'true', parentFolderId: route.params.folderId }, + query: { + fromJson: 'true', + parentFolderId: route.params.folderId, + projectId: projectsStore.currentProjectId, + }, }); window.open(href, '_blank'); diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue index 52aaf7c82f..ed0ca0b844 100644 --- a/packages/frontend/editor-ui/src/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/views/NodeView.vue @@ -491,7 +491,7 @@ async function fetchAndSetParentFolder(folderId?: string) { } async function fetchAndSetProject(projectId: string) { - if (!projectsStore.currentProject) { + if (projectsStore.currentProject?.id !== projectId) { const project = await projectsStore.fetchProject(projectId); projectsStore.setCurrentProject(project); } @@ -614,9 +614,15 @@ async function openTemplateFromWorkflowJSON(workflow: WorkflowDataWithTemplateId isBlankRedirect.value = true; const templateId = workflow.meta.templateId; const parentFolderId = route.query.parentFolderId as string | undefined; + + if (projectsStore.currentProjectId) { + await fetchAndSetProject(projectsStore.currentProjectId); + } + await fetchAndSetParentFolder(parentFolderId); + await router.replace({ name: VIEWS.NEW_WORKFLOW, - query: { templateId, parentFolderId }, + query: { templateId, parentFolderId, projectId: projectsStore.currentProjectId }, }); await importTemplate({