Skip to content

Commit 26805b6

Browse files
alexgrozavclaudecubic-dev-ai[bot]
authored
refactor(editor): Move initialization logic to WorkflowLayout (no-changelog) (#25288)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 045f5b0 commit 26805b6

8 files changed

Lines changed: 578 additions & 509 deletions

File tree

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
import { ref, computed } from 'vue';
2+
import { useRoute, useRouter } from 'vue-router';
3+
import { useI18n } from '@n8n/i18n';
4+
import { useToast } from '@/app/composables/useToast';
5+
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
6+
import { useExternalHooks } from '@/app/composables/useExternalHooks';
7+
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
8+
import { useParentFolder } from '@/features/core/folders/composables/useParentFolder';
9+
import { useWorkflowsStore } from '@/app/stores/workflows.store';
10+
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
11+
import { useUIStore } from '@/app/stores/ui.store';
12+
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
13+
import { useCredentialsStore } from '@/features/credentials/credentials.store';
14+
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
15+
import { useExternalSecretsStore } from '@/features/integrations/externalSecrets.ee/externalSecrets.ee.store';
16+
import { useSettingsStore } from '@/app/stores/settings.store';
17+
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
18+
import { useHistoryStore } from '@/app/stores/history.store';
19+
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
20+
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
21+
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
22+
import { useTelemetry } from '@/app/composables/useTelemetry';
23+
import { useExecutionDebugging } from '@/features/execution/executions/composables/useExecutionDebugging';
24+
import { getSampleWorkflowByTemplateId } from '@/features/workflows/templates/utils/workflowSamples';
25+
import { EnterpriseEditionFeature, VIEWS } from '@/app/constants';
26+
import type { WorkflowState } from '@/app/composables/useWorkflowState';
27+
import type { IWorkflowDb } from '@/Interface';
28+
29+
export function useWorkflowInitialization(workflowState: WorkflowState) {
30+
const route = useRoute();
31+
const router = useRouter();
32+
const i18n = useI18n();
33+
const toast = useToast();
34+
const documentTitle = useDocumentTitle();
35+
const externalHooks = useExternalHooks();
36+
37+
const workflowsStore = useWorkflowsStore();
38+
const workflowsListStore = useWorkflowsListStore();
39+
const uiStore = useUIStore();
40+
const nodeTypesStore = useNodeTypesStore();
41+
const credentialsStore = useCredentialsStore();
42+
const environmentsStore = useEnvironmentsStore();
43+
const externalSecretsStore = useExternalSecretsStore();
44+
const settingsStore = useSettingsStore();
45+
const projectsStore = useProjectsStore();
46+
const historyStore = useHistoryStore();
47+
const builderStore = useBuilderStore();
48+
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
49+
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
50+
const telemetry = useTelemetry();
51+
52+
const {
53+
resetWorkspace,
54+
initializeWorkspace,
55+
fitView,
56+
openWorkflowTemplate,
57+
openWorkflowTemplateFromJSON,
58+
} = useCanvasOperations();
59+
const { fetchAndSetParentFolder } = useParentFolder();
60+
// Pass workflowState to useExecutionDebugging since we're in the same component
61+
// that provides WorkflowStateKey (WorkflowLayout), so inject won't work
62+
const { applyExecutionData } = useExecutionDebugging(workflowState);
63+
64+
const isLoading = ref(true);
65+
const initializedWorkflowId = ref<string | undefined>();
66+
67+
const workflowId = computed(() => {
68+
const name = route.params.name;
69+
return (Array.isArray(name) ? name[0] : name) ?? '';
70+
});
71+
72+
const isNewWorkflowRoute = computed(() => route.query.new === 'true');
73+
const isDemoRoute = computed(() => route.name === VIEWS.DEMO);
74+
const isTemplateRoute = computed(() => route.name === VIEWS.TEMPLATE_IMPORT);
75+
const isOnboardingRoute = computed(() => route.name === VIEWS.WORKFLOW_ONBOARDING);
76+
const isDebugRoute = computed(() => route.name === VIEWS.EXECUTION_DEBUG);
77+
78+
async function loadCredentials() {
79+
let options: { workflowId: string } | { projectId: string };
80+
81+
if (workflowId.value && !isNewWorkflowRoute.value) {
82+
options = { workflowId: workflowId.value };
83+
} else {
84+
const queryParam =
85+
typeof route.query?.projectId === 'string' ? route.query?.projectId : undefined;
86+
const projectId = queryParam ?? projectsStore.personalProject?.id;
87+
if (projectId === undefined) {
88+
throw new Error(
89+
'Could not find projectId in the query nor could I find the personal project in the project store',
90+
);
91+
}
92+
93+
options = { projectId };
94+
}
95+
96+
await credentialsStore.fetchAllCredentialsForWorkflow(options);
97+
}
98+
99+
/**
100+
* Handle template import route (VIEWS.TEMPLATE_IMPORT)
101+
* Returns true if template was handled, false otherwise
102+
*/
103+
async function handleTemplateImportRoute(): Promise<boolean> {
104+
if (!isTemplateRoute.value) return false;
105+
106+
const templateId = route.params.id;
107+
if (!templateId) return false;
108+
109+
const loadWorkflowFromJSON = route.query.fromJson === 'true';
110+
111+
if (loadWorkflowFromJSON) {
112+
const workflow = getSampleWorkflowByTemplateId(templateId.toString());
113+
if (!workflow) {
114+
toast.showError(
115+
new Error(i18n.baseText('nodeView.couldntLoadWorkflow.invalidWorkflowObject')),
116+
i18n.baseText('nodeView.couldntImportWorkflow'),
117+
);
118+
await router.replace({ name: VIEWS.NEW_WORKFLOW });
119+
return true;
120+
}
121+
122+
await openWorkflowTemplateFromJSON(workflow);
123+
} else {
124+
await openWorkflowTemplate(templateId.toString());
125+
}
126+
127+
return true;
128+
}
129+
130+
/**
131+
* Handle debug mode route (VIEWS.EXECUTION_DEBUG)
132+
* Loads execution data for debugging
133+
*/
134+
async function handleDebugModeRoute() {
135+
if (!isDebugRoute.value) return;
136+
137+
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'DEBUG');
138+
139+
if (!workflowsStore.isInDebugMode) {
140+
const executionId = route.params.executionId;
141+
if (typeof executionId === 'string') {
142+
await applyExecutionData(executionId);
143+
workflowsStore.isInDebugMode = true;
144+
}
145+
}
146+
}
147+
148+
async function initializeData() {
149+
const loadPromises = (() => {
150+
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
151+
152+
const promises: Array<Promise<unknown>> = [
153+
workflowsListStore.fetchActiveWorkflows(),
154+
credentialsStore.fetchCredentialTypes(true),
155+
loadCredentials(),
156+
];
157+
158+
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables]) {
159+
promises.push(environmentsStore.fetchAllVariables());
160+
}
161+
162+
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets]) {
163+
promises.push(externalSecretsStore.fetchGlobalSecrets());
164+
const shouldFetchProjectSecrets =
165+
route?.params?.projectId !== projectsStore.personalProject?.id;
166+
if (shouldFetchProjectSecrets && typeof route?.params?.projectId === 'string') {
167+
promises.push(externalSecretsStore.fetchProjectSecrets(route.params.projectId));
168+
}
169+
}
170+
171+
return promises;
172+
})();
173+
174+
if (nodeTypesStore.allNodeTypes.length === 0) {
175+
loadPromises.push(nodeTypesStore.getNodeTypes());
176+
}
177+
178+
try {
179+
await Promise.all(loadPromises);
180+
void nodeTypesStore.fetchCommunityNodePreviews();
181+
} catch (error) {
182+
toast.showError(
183+
error,
184+
i18n.baseText('nodeView.showError.mounted1.title'),
185+
i18n.baseText('nodeView.showError.mounted1.message') + ':',
186+
);
187+
}
188+
}
189+
190+
async function openWorkflow(data: IWorkflowDb) {
191+
resetWorkspace();
192+
193+
if (builderStore.streaming) {
194+
documentTitle.setDocumentTitle(data.name, 'AI_BUILDING');
195+
} else {
196+
documentTitle.setDocumentTitle(data.name, 'IDLE');
197+
}
198+
199+
await initializeWorkspace(data);
200+
201+
void externalHooks.run('workflow.open', {
202+
workflowId: data.id,
203+
workflowName: data.name,
204+
});
205+
206+
fitView();
207+
}
208+
209+
async function initializeWorkspaceForNewWorkflow() {
210+
resetWorkspace();
211+
212+
const parentFolderId = route.query.parentFolderId as string | undefined;
213+
214+
await workflowState.getNewWorkflowDataAndMakeShareable(
215+
undefined,
216+
projectsStore.currentProjectId,
217+
parentFolderId,
218+
);
219+
220+
workflowState.setWorkflowId(workflowId.value);
221+
222+
await projectsStore.refreshCurrentProject();
223+
await fetchAndSetParentFolder(parentFolderId);
224+
225+
uiStore.nodeViewInitialized = true;
226+
initializedWorkflowId.value = workflowId.value;
227+
228+
fitView();
229+
}
230+
231+
async function initializeWorkspaceForExistingWorkflow(id: string) {
232+
try {
233+
const workflowData = await workflowsListStore.fetchWorkflow(id);
234+
235+
await openWorkflow(workflowData);
236+
237+
if (workflowData.parentFolder) {
238+
workflowsStore.setParentFolder(workflowData.parentFolder);
239+
}
240+
241+
// Track telemetry for onboarding and experiment workflows
242+
if (workflowData.meta?.onboardingId) {
243+
telemetry.track(
244+
`User opened workflow from onboarding template with ID ${workflowData.meta.onboardingId}`,
245+
{ workflow_id: id },
246+
);
247+
}
248+
249+
if (workflowData.meta?.templateId?.startsWith('035_template_onboarding')) {
250+
aiTemplatesStarterCollectionStore.trackUserOpenedWorkflow(
251+
workflowData.meta.templateId.split('-').pop() ?? '',
252+
);
253+
}
254+
255+
if (workflowData.meta?.templateId?.startsWith('37_onboarding_experiments_batch_aug11')) {
256+
readyToRunWorkflowsStore.trackOpenWorkflow(
257+
workflowData.meta.templateId.split('-').pop() ?? '',
258+
);
259+
}
260+
261+
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(
262+
workflowData.homeProject,
263+
workflowData.sharedWithProjects,
264+
);
265+
void workflowsStore.fetchLastSuccessfulExecution();
266+
} catch (error) {
267+
if ((error as { httpStatusCode?: number }).httpStatusCode === 404) {
268+
return await router.replace({
269+
name: VIEWS.ENTITY_NOT_FOUND,
270+
params: { entityType: 'workflow' },
271+
});
272+
}
273+
if ((error as { httpStatusCode?: number }).httpStatusCode === 403) {
274+
return await router.replace({
275+
name: VIEWS.ENTITY_UNAUTHORIZED,
276+
params: { entityType: 'workflow' },
277+
});
278+
}
279+
280+
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
281+
void router.push({
282+
name: VIEWS.NEW_WORKFLOW,
283+
});
284+
} finally {
285+
uiStore.nodeViewInitialized = true;
286+
initializedWorkflowId.value = workflowId.value;
287+
}
288+
}
289+
290+
async function initializeWorkflow(force = false) {
291+
// Handle blank redirect (used by template import to prevent double initialization)
292+
// The flag is set by openWorkflowTemplate before navigation. We clear it here
293+
// when detected, ensuring WorkflowLayout doesn't interfere with template import.
294+
if (uiStore.isBlankRedirect) {
295+
uiStore.isBlankRedirect = false;
296+
isLoading.value = false;
297+
return;
298+
}
299+
300+
// Handle template import route first - this has its own navigation flow
301+
// Must be checked before workflowId since template routes use route.params.id, not name
302+
const handledTemplate = await handleTemplateImportRoute();
303+
if (handledTemplate) {
304+
isLoading.value = false;
305+
return;
306+
}
307+
308+
// Skip if no workflowId (shouldn't happen for non-template routes in WorkflowLayout)
309+
if (!workflowId.value) {
310+
isLoading.value = false;
311+
return;
312+
}
313+
314+
const isAlreadyInitialized =
315+
!force && initializedWorkflowId.value && initializedWorkflowId.value === workflowId.value;
316+
317+
if (isAlreadyInitialized) {
318+
// Still need to handle debug mode when navigating to debug route
319+
// even if the workflow is already initialized (e.g., from executions tab)
320+
await handleDebugModeRoute();
321+
isLoading.value = false;
322+
return;
323+
}
324+
325+
isLoading.value = true;
326+
327+
try {
328+
historyStore.reset();
329+
330+
if (isDemoRoute.value) {
331+
await initializeWorkspaceForNewWorkflow();
332+
return;
333+
}
334+
335+
if (isNewWorkflowRoute.value) {
336+
const exists = await workflowsListStore.checkWorkflowExists(workflowId.value);
337+
if (!exists && route.meta?.nodeView === true) {
338+
await initializeWorkspaceForNewWorkflow();
339+
return;
340+
} else {
341+
await router.replace({
342+
...route,
343+
query: {
344+
...route.query,
345+
new: undefined,
346+
},
347+
});
348+
}
349+
}
350+
351+
await initializeWorkspaceForExistingWorkflow(workflowId.value);
352+
353+
// Handle debug mode after workflow is loaded
354+
await handleDebugModeRoute();
355+
} finally {
356+
isLoading.value = false;
357+
}
358+
}
359+
360+
function cleanup() {
361+
resetWorkspace();
362+
uiStore.nodeViewInitialized = false;
363+
}
364+
365+
return {
366+
isLoading,
367+
initializedWorkflowId,
368+
workflowId,
369+
isNewWorkflowRoute,
370+
isDemoRoute,
371+
isTemplateRoute,
372+
isOnboardingRoute,
373+
isDebugRoute,
374+
loadCredentials,
375+
initializeData,
376+
openWorkflow,
377+
initializeWorkspaceForNewWorkflow,
378+
initializeWorkspaceForExistingWorkflow,
379+
handleTemplateImportRoute,
380+
handleDebugModeRoute,
381+
initializeWorkflow,
382+
cleanup,
383+
};
384+
}

0 commit comments

Comments
 (0)