From e1b73a2e364dfadb2b1fd67bffac8d8d1fec22f3 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 10 Sep 2026 16:01:37 -0400 Subject: [PATCH] refactor(editor): Unify template import initialization with the new workflow init path --- .../composables/useCanvasOperations.test.ts | 10 +- .../app/composables/useCanvasOperations.ts | 21 +- .../useNewWorkflowDocument.test.ts | 216 ++++++++++++++++++ .../app/composables/useNewWorkflowDocument.ts | 98 ++++++++ .../useWorkflowInitialization.test.ts | 92 +------- .../composables/useWorkflowInitialization.ts | 69 +----- 6 files changed, 329 insertions(+), 177 deletions(-) create mode 100644 packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.test.ts create mode 100644 packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.ts 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 d2d0a53c8fc..78054d761ee 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -6858,14 +6858,11 @@ describe('useCanvasOperations', () => { }, }; - const getNewWorkflowData = vi.mocked(workflowsApi.getNewWorkflowData); const setConnectionsSpy = vi.spyOn(workflowDocumentStoreInstance, 'setConnections'); const { importTemplate } = useCanvasOperations(); - const templateName = 'template name'; await importTemplate({ - name: templateName, workflow, }); @@ -6882,11 +6879,6 @@ describe('useCanvasOperations', () => { disabled: false, }); expect(workflowDocumentStoreInstance.setNodePristine).toHaveBeenCalledWith(nodeB.name, true); - expect(getNewWorkflowData).toHaveBeenCalledWith( - expect.anything(), - templateName, - projectsStore.currentProjectId, - ); }); }); describe('replaceNodeParameters', () => { @@ -7785,6 +7777,7 @@ describe('useCanvasOperations', () => { expect.anything(), 'Template Name', 'test-project-id', + undefined, ); expect(telemetry.track).toHaveBeenCalledWith('User inserted workflow template', { @@ -7850,6 +7843,7 @@ describe('useCanvasOperations', () => { expect.anything(), 'Template Name', 'test-project-id', + undefined, ); expect(router.replace).toHaveBeenCalledWith({ diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index 0686abaec48..9bcd71bea5a 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -34,6 +34,7 @@ import { getN8nAgentsNodeName } from '@/experiments/inlineAgents/useInlineAgents import { type PinDataSource, usePinnedData } from '@/app/composables/usePinnedData'; import { useTelemetry } from '@n8n/composables/useTelemetry'; import { useToast } from '@n8n/composables/useToast'; +import { useNewWorkflowDocument } from '@/app/composables/useNewWorkflowDocument'; import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers'; import { useWorkflowNormalization } from '@/app/composables/useWorkflowNormalization'; import { getExecutionErrorToastConfiguration } from '@/features/execution/executions/executions.utils'; @@ -58,7 +59,6 @@ import { ReplaceNodeParametersCommand, UpdateNodeGroupCommand, } from '@/app/models/history'; -import * as workflowsApi from '@/app/api/workflows'; import { useCanvasStore } from '@/app/stores/canvas.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { getAutoSelectedCredential } from '@/features/credentials/credentials.utils'; @@ -261,6 +261,7 @@ export function useCanvasOperations() { const i18n = useI18n(); const toast = useToast(); const workflowHelpers = useWorkflowHelpers(); + const { initializeNewWorkflowDocument } = useNewWorkflowDocument(); const nodeHelpers = useNodeHelpers(); const aiSimulatedDataGuard = useAiSimulatedDataGuard(); const { @@ -3585,10 +3586,8 @@ export function useCanvasOperations() { } async function importTemplate({ - name, workflow, }: { - name?: string; workflow: IWorkflowTemplate['workflow'] | WorkflowDataWithTemplateId; }) { let convertedNodes = workflow.nodes?.map(workflowsStore.convertTemplateNodeToNodeUi); @@ -3601,13 +3600,6 @@ export function useCanvasOperations() { workflowDocumentStore.value.setConnections(workflow.connections); } await addNodes(convertedNodes ?? [], { keepPristine: true }); - const workflowData = await workflowsApi.getNewWorkflowData( - rootStore.restApiContext, - name, - projectsStore.currentProjectId, - ); - workflowDocumentStore.value.setName(workflowData.name); - workflowDocumentStore.value.setHydrated(true); } async function tryToOpenSubworkflowInNewTab(nodeId: string): Promise { @@ -3817,7 +3809,8 @@ export function useCanvasOperations() { workflowsStore.setWorkflowId(route.params.workflowId); } - await importTemplate({ name: data.name, workflow: data.workflow }); + await initializeNewWorkflowDocument({ name: data.name }); + await importTemplate({ workflow: data.workflow }); workflowDocumentStore.value.addToMeta({ templateId: `${templateId}` }); @@ -3870,10 +3863,8 @@ export function useCanvasOperations() { workflowsStore.setWorkflowId(route.params.workflowId); } - await importTemplate({ - name: workflow.name, - workflow, - }); + await initializeNewWorkflowDocument({ name: workflow.name, parentFolderId }); + await importTemplate({ workflow }); workflowDocumentStore.value.addToMeta({ templateId: `${templateId}` }); diff --git a/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.test.ts b/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.test.ts new file mode 100644 index 00000000000..15a77a33ad1 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.test.ts @@ -0,0 +1,216 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { shallowRef, defineComponent, h } from 'vue'; +import { setActivePinia } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { render } from '@testing-library/vue'; + +import { useNewWorkflowDocument } from './useNewWorkflowDocument'; +import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; +import { useUIStore } from '@/app/stores/ui.store'; +import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; +import type { Project } from '@/features/collaboration/projects/projects.types'; +import { mockedStore } from '@/__tests__/utils'; + +const mockSetDocumentTitle = vi.hoisted(() => vi.fn()); +vi.mock('@/app/composables/useDocumentTitle', () => ({ + useDocumentTitle: vi.fn(() => ({ + setDocumentTitle: mockSetDocumentTitle, + })), +})); + +const mockGetNewWorkflowData = vi.hoisted(() => + vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }), +); +vi.mock('@/app/api/workflows', async (importOriginal) => ({ + ...(await importOriginal()), + getNewWorkflowData: mockGetNewWorkflowData, +})); + +const mockFetchParentFolder = vi.hoisted(() => vi.fn().mockResolvedValue(null)); +vi.mock('@/features/core/folders/composables/useParentFolder', () => ({ + useParentFolder: vi.fn(() => ({ + fetchParentFolder: mockFetchParentFolder, + })), +})); + +vi.mock('@/app/composables/useWorkflowId', async (importOriginal) => ({ + ...(await importOriginal()), + useWorkflowId: vi.fn(() => shallowRef('wf-1')), +})); + +const mockRoute = vi.hoisted(() => ({ + name: 'workflow' as string, + params: {} as Record, + query: {} as Record, + meta: {} as Record, +})); +vi.mock('vue-router', async (importOriginal) => { + const actual = (await importOriginal()) as object; + return { + ...actual, + useRoute: vi.fn(() => mockRoute), + useRouter: vi.fn(() => ({ + replace: vi.fn().mockResolvedValue(undefined), + push: vi.fn().mockResolvedValue(undefined), + })), + }; +}); + +const mockWorkflowDocumentStore = vi.hoisted(() => ({ + workflowId: 'wf-1', + name: 'New Workflow', + setName: vi.fn(), + setHomeProject: vi.fn(), + setScopes: vi.fn(), + setParentFolder: vi.fn(), + setHydrated: vi.fn(), + onNameChange: vi.fn(), +})); +vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({ + ...(await importOriginal()), + useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore), + createWorkflowDocumentId: vi.fn((id: string) => id), +})); + +function renderWithComposable( + callback: (init: ReturnType) => void, + workflowDocumentStoreRef = shallowRef( + mockWorkflowDocumentStore, + ), +) { + const TestComponent = defineComponent({ + setup() { + callback(useNewWorkflowDocument()); + return () => h('div'); + }, + }); + + return render(TestComponent, { + global: { + provide: { + [WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef, + }, + }, + }); +} + +describe('useNewWorkflowDocument', () => { + beforeEach(() => { + setActivePinia(createTestingPinia()); + vi.clearAllMocks(); + mockGetNewWorkflowData.mockResolvedValue({ name: 'New Workflow', settings: {} }); + }); + + function initialize(options?: { name?: string; parentFolderId?: string }) { + let initializeNewWorkflowDocument!: ReturnType< + typeof useNewWorkflowDocument + >['initializeNewWorkflowDocument']; + renderWithComposable((composable) => { + initializeNewWorkflowDocument = composable.initializeNewWorkflowDocument; + }); + return initializeNewWorkflowDocument(options); + } + + it('sets the name from getNewWorkflowData and passes the requested name through', async () => { + mockGetNewWorkflowData.mockResolvedValue({ name: 'Template Name 2', settings: {} }); + + await initialize({ name: 'Template Name' }); + + expect(mockGetNewWorkflowData).toHaveBeenCalledWith( + expect.anything(), + 'Template Name', + undefined, + undefined, + ); + expect(mockWorkflowDocumentStore.setName).toHaveBeenCalledWith('Template Name 2'); + expect(mockSetDocumentTitle).toHaveBeenCalledWith('Template Name 2', 'IDLE'); + }); + + it('populates scopes and home project from the personal project', async () => { + const projectsStore = mockedStore(useProjectsStore); + projectsStore.personalProject = { + id: 'personal-project-id', + scopes: ['workflow:update', 'workflow:publish'], + } as Project; + projectsStore.currentProject = null; + + await initialize(); + + expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith( + expect.objectContaining({ id: 'personal-project-id' }), + ); + expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith([ + 'workflow:update', + 'workflow:publish', + ]); + expect(mockWorkflowDocumentStore.setHydrated).toHaveBeenCalledWith(true); + }); + + it('prefers the project that refreshCurrentProject resolves', async () => { + const projectsStore = mockedStore(useProjectsStore); + projectsStore.personalProject = { + id: 'personal-project-id', + scopes: ['workflow:read'], + } as Project; + projectsStore.currentProject = null; + projectsStore.refreshCurrentProject.mockImplementation(async () => { + projectsStore.currentProject = { + id: 'team-project-id', + scopes: ['workflow:update'], + } as Project; + }); + + await initialize(); + + expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith( + expect.objectContaining({ id: 'team-project-id' }), + ); + expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']); + }); + + it('falls back to the loaded projects when the project refresh fails', async () => { + const projectsStore = mockedStore(useProjectsStore); + projectsStore.personalProject = { + id: 'personal-project-id', + scopes: ['workflow:update'], + } as Project; + projectsStore.currentProject = null; + projectsStore.refreshCurrentProject.mockRejectedValue(new Error('network error')); + + await initialize(); + + expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']); + }); + + it('does not populate a store that was replaced during the project refresh', async () => { + const projectsStore = mockedStore(useProjectsStore); + projectsStore.personalProject = { + id: 'personal-project-id', + scopes: ['workflow:update'], + } as Project; + const storeRef = shallowRef(mockWorkflowDocumentStore); + projectsStore.refreshCurrentProject.mockImplementation(async () => { + storeRef.value = null; + }); + + let initializeNewWorkflowDocument!: ReturnType< + typeof useNewWorkflowDocument + >['initializeNewWorkflowDocument']; + renderWithComposable((composable) => { + initializeNewWorkflowDocument = composable.initializeNewWorkflowDocument; + }, storeRef); + await initializeNewWorkflowDocument(); + + expect(mockWorkflowDocumentStore.setHomeProject).not.toHaveBeenCalled(); + expect(mockWorkflowDocumentStore.setScopes).not.toHaveBeenCalled(); + }); + + it('marks the node view initialized', async () => { + const uiStore = mockedStore(useUIStore); + uiStore.nodeViewInitialized = false; + + await initialize(); + + expect(uiStore.nodeViewInitialized).toBe(true); + }); +}); diff --git a/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.ts b/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.ts new file mode 100644 index 00000000000..d68205502f0 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/composables/useNewWorkflowDocument.ts @@ -0,0 +1,98 @@ +import { hasInjectionContext, inject } from 'vue'; +import * as workflowsApi from '@/app/api/workflows'; +import { useDocumentTitle } from '@/app/composables/useDocumentTitle'; +import { useWorkflowId } from '@/app/composables/useWorkflowId'; +import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, + type WorkflowDocumentStore, +} from '@/app/stores/workflowDocument.store'; +import { useUIStore } from '@/app/stores/ui.store'; +import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; +import { useParentFolder } from '@/features/core/folders/composables/useParentFolder'; +import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; +import { useRootStore } from '@n8n/stores/useRootStore'; + +/** + * Populates a freshly created workflow document store with everything the + * editor derives state from: name, home project, scopes, parent folder, + * hydration, and the name → list cache sync. + * + * The new-workflow path and the template import path must both run this, so + * a side effect added here reaches both. ADO-5844 came from the template + * path hand-copying only part of this list. + */ +export function useNewWorkflowDocument() { + const documentTitle = useDocumentTitle(); + const rootStore = useRootStore(); + const uiStore = useUIStore(); + const workflowsListStore = useWorkflowsListStore(); + const projectsStore = useProjectsStore(); + const workflowId = useWorkflowId(); + const { fetchParentFolder } = useParentFolder(); + // The app-level writable ref (provided by App.vue). Layouts gate rendering + // on it, so it must point at the store this initialization creates. + const currentWorkflowDocumentStore = hasInjectionContext() + ? inject(WorkflowDocumentStoreKey, null) + : null; + + async function initializeNewWorkflowDocument({ + name, + parentFolderId, + }: { name?: string; parentFolderId?: string } = {}): Promise { + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowId.value), + ); + if (currentWorkflowDocumentStore) { + currentWorkflowDocumentStore.value = workflowDocumentStore; + } + + // Sync document store name → list cache (mirrors initializeWorkflowDocument) + workflowDocumentStore.onNameChange(({ payload }) => { + workflowsListStore.updateWorkflowInCache(workflowId.value, { name: payload.name }); + }); + + const workflowData = await workflowsApi.getNewWorkflowData( + rootStore.restApiContext, + name, + projectsStore.currentProjectId, + parentFolderId, + ); + workflowDocumentStore.setName(workflowData.name); + documentTitle.setDocumentTitle(workflowData.name, 'IDLE'); + + try { + await projectsStore.refreshCurrentProject(); + } catch (error) { + // A stale project is recoverable; a rejection here would strand the + // caller's loading state. + console.error('Failed to refresh current project for a new workflow document', { error }); + } + + // Navigation during the async work can dispose or replace the store. + // Only populate the store this call created. + if ( + currentWorkflowDocumentStore && + currentWorkflowDocumentStore.value !== workflowDocumentStore + ) { + return workflowDocumentStore; + } + + const { currentProject, personalProject } = projectsStore; + // Must read the project after the refresh: a `?projectId=` deep link isn't fetched + // into the store until then, so an earlier read stamps personal as the owner. + workflowDocumentStore.setHomeProject(currentProject ?? personalProject ?? null); + workflowDocumentStore.setScopes(currentProject?.scopes ?? personalProject?.scopes ?? []); + + const parentFolder = await fetchParentFolder(parentFolderId); + workflowDocumentStore.setParentFolder(parentFolder); + workflowDocumentStore.setHydrated(true); + + uiStore.nodeViewInitialized = true; + + return workflowDocumentStore; + } + + return { initializeNewWorkflowDocument }; +} diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts index 427d899d6f8..445a9cdc245 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.test.ts @@ -143,9 +143,6 @@ vi.mock('vue-router', async (importOriginal) => { function renderWithComposable( callback: (init: ReturnType) => void, - workflowDocumentStoreRef = shallowRef( - mockWorkflowDocumentStore, - ), ) { const TestComponent = defineComponent({ setup() { @@ -158,7 +155,7 @@ function renderWithComposable( return render(TestComponent, { global: { provide: { - [WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef, + [WorkflowDocumentStoreKey as symbol]: shallowRef(mockWorkflowDocumentStore), }, }, }); @@ -262,8 +259,8 @@ describe('useWorkflowInitialization', () => { }); }); - describe('template import permissions', () => { - function importTemplate() { + describe('template import route', () => { + it('delegates the import to openWorkflowTemplate and reports it handled', async () => { mockRoute.name = VIEWS.TEMPLATE_IMPORT; mockRoute.params = { id: '11204' }; @@ -271,91 +268,10 @@ describe('useWorkflowInitialization', () => { renderWithComposable((init) => { handleTemplateImportRoute = init.handleTemplateImportRoute; }); - return handleTemplateImportRoute(); - } - - it('populates scopes and home project on the document store', async () => { - const projectsStore = mockedStore(useProjectsStore); - projectsStore.personalProject = { - id: 'personal-project-id', - scopes: ['workflow:update', 'workflow:publish'], - } as Project; - projectsStore.currentProject = null; - - const handled = await importTemplate(); + const handled = await handleTemplateImportRoute(); expect(handled).toBe(true); expect(mockOpenWorkflowTemplate).toHaveBeenCalledWith('11204'); - expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith( - expect.objectContaining({ id: 'personal-project-id' }), - ); - expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith([ - 'workflow:update', - 'workflow:publish', - ]); - }); - - it('prefers the project that refreshCurrentProject resolves', async () => { - const projectsStore = mockedStore(useProjectsStore); - projectsStore.personalProject = { - id: 'personal-project-id', - scopes: ['workflow:read'], - } as Project; - projectsStore.currentProject = null; - projectsStore.refreshCurrentProject.mockImplementation(async () => { - projectsStore.currentProject = { - id: 'team-project-id', - scopes: ['workflow:update'], - } as Project; - }); - - await importTemplate(); - - expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith( - expect.objectContaining({ id: 'team-project-id' }), - ); - expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']); - }); - - it('falls back to the loaded projects when the project refresh fails', async () => { - const projectsStore = mockedStore(useProjectsStore); - projectsStore.personalProject = { - id: 'personal-project-id', - scopes: ['workflow:update'], - } as Project; - projectsStore.currentProject = null; - projectsStore.refreshCurrentProject.mockRejectedValue(new Error('network error')); - - const handled = await importTemplate(); - - expect(handled).toBe(true); - expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']); - }); - - it('does not stamp a store that was replaced during the project refresh', async () => { - mockRoute.name = VIEWS.TEMPLATE_IMPORT; - mockRoute.params = { id: '11204' }; - - const projectsStore = mockedStore(useProjectsStore); - projectsStore.personalProject = { - id: 'personal-project-id', - scopes: ['workflow:update'], - } as Project; - const storeRef = shallowRef( - mockWorkflowDocumentStore, - ); - projectsStore.refreshCurrentProject.mockImplementation(async () => { - storeRef.value = null; - }); - - let handleTemplateImportRoute!: () => Promise; - renderWithComposable((init) => { - handleTemplateImportRoute = init.handleTemplateImportRoute; - }, storeRef); - await handleTemplateImportRoute(); - - expect(mockWorkflowDocumentStore.setHomeProject).not.toHaveBeenCalled(); - expect(mockWorkflowDocumentStore.setScopes).not.toHaveBeenCalled(); }); }); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts index 428f6e5193c..779ab8cbfa5 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowInitialization.ts @@ -6,9 +6,7 @@ import { useToast } from '@n8n/composables/useToast'; import { useDocumentTitle } from '@/app/composables/useDocumentTitle'; import { useExternalHooks } from '@/app/composables/useExternalHooks'; import { useCanvasOperations } from '@/app/composables/useCanvasOperations'; -import { useParentFolder } from '@/features/core/folders/composables/useParentFolder'; -import * as workflowsApi from '@/app/api/workflows'; -import { useRootStore } from '@n8n/stores/useRootStore'; +import { useNewWorkflowDocument } from '@/app/composables/useNewWorkflowDocument'; import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; import { useWorkflowsListStore } from '@/app/stores/workflowsList.store'; @@ -46,7 +44,6 @@ export function useWorkflowInitialization() { const documentTitle = useDocumentTitle(); const externalHooks = useExternalHooks(); - const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); const workflowsListStore = useWorkflowsListStore(); const uiStore = useUIStore(); @@ -72,13 +69,12 @@ export function useWorkflowInitialization() { openWorkflowTemplate, openWorkflowTemplateFromJSON, } = useCanvasOperations(); + const { initializeNewWorkflowDocument } = useNewWorkflowDocument(); const { applyExecutionData } = useExecutionDebugging(); const isLoading = ref(true); const initializedWorkflowId = ref(); - const { fetchParentFolder } = useParentFolder(); - function disposeCurrentWorkflowDocumentStore() { const workflowDocumentStore = currentWorkflowDocumentStore.value; @@ -170,35 +166,6 @@ export function useWorkflowInitialization() { await openWorkflowTemplate(templateId.toString()); } - // Create document store for template workflow (empty tags initially) - // The workflow ID was set during the template import - const currentWorkflowId = workflowId.value; - if (currentWorkflowId) { - const workflowDocumentId = createWorkflowDocumentId(currentWorkflowId); - const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId); - currentWorkflowDocumentStore.value = workflowDocumentStore; - documentTitle.setDocumentTitle(workflowDocumentStore.name, 'IDLE'); - - // The header derives every permission from the document store scopes. - // Without them, the first save turns the whole header read-only - // (Publish and Save hidden, actions menu disabled). - try { - await projectsStore.refreshCurrentProject(); - } catch (error) { - // A stale project is recoverable; a rejection here would leave the - // route handler's caller with isLoading stuck on the loading view. - console.error('Failed to refresh current project during template import', { error }); - } - - // Navigation during the refresh can dispose or replace the store. - // Only stamp the store this import created. - if (currentWorkflowDocumentStore.value === workflowDocumentStore) { - const { currentProject, personalProject } = projectsStore; - workflowDocumentStore.setHomeProject(currentProject ?? personalProject ?? null); - workflowDocumentStore.setScopes(currentProject?.scopes ?? personalProject?.scopes ?? []); - } - } - return true; } @@ -327,38 +294,8 @@ export function useWorkflowInitialization() { workflowsStore.setWorkflowId(workflowId.value); - const workflowDocumentId = createWorkflowDocumentId(workflowId.value); - currentWorkflowDocumentStore.value = useWorkflowDocumentStore(workflowDocumentId); + await initializeNewWorkflowDocument({ parentFolderId }); - // Sync document store name → list cache (mirrors initializeWorkflowDocument) - currentWorkflowDocumentStore.value.onNameChange(({ payload }) => { - workflowsListStore.updateWorkflowInCache(workflowId.value, { name: payload.name }); - }); - - const workflowData = await workflowsApi.getNewWorkflowData( - rootStore.restApiContext, - undefined, - projectsStore.currentProjectId, - parentFolderId, - ); - currentWorkflowDocumentStore.value.setName(workflowData.name); - documentTitle.setDocumentTitle(workflowData.name, 'IDLE'); - - await projectsStore.refreshCurrentProject(); - - const { currentProject, personalProject } = projectsStore; - // Must read the project after the refresh: a `?projectId=` deep link isn't fetched - // into the store until then, so an earlier read stamps personal as the owner. - currentWorkflowDocumentStore.value.setHomeProject(currentProject ?? personalProject ?? null); - currentWorkflowDocumentStore.value.setScopes( - currentProject?.scopes ?? personalProject?.scopes ?? [], - ); - - const parentFolder = await fetchParentFolder(parentFolderId); - currentWorkflowDocumentStore.value?.setParentFolder(parentFolder); - currentWorkflowDocumentStore.value.setHydrated(true); - - uiStore.nodeViewInitialized = true; initializedWorkflowId.value = workflowId.value; fitView();