From 0f91fd2b2e9cba9864c5955414ef1f4ca6bbd215 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 4 Sep 2024 13:18:20 +0300 Subject: [PATCH] fix(editor): Fix workflow loading after switching to executions view in new canvas (no-changelog) (#10655) --- packages/editor-ui/src/router.ts | 1 + .../editor-ui/src/stores/workflows.store.ts | 6 +- packages/editor-ui/src/views/NodeView.v2.vue | 116 +++++++++++------- .../src/views/WorkflowExecutionsView.vue | 58 +++++---- 4 files changed, 107 insertions(+), 74 deletions(-) diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 6c6a68b22d..96a189071b 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -328,6 +328,7 @@ export const routes: RouteRecordRaw[] = [ default: NodeView, }, meta: { + nodeView: true, middleware: ['authenticated'], middlewareOptions: { authenticated: { diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 3256c5670f..06e93b8aaa 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -428,9 +428,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { async function fetchWorkflow(id: string): Promise { const rootStore = useRootStore(); - const workflow = await workflowsApi.getWorkflow(rootStore.restApiContext, id); - addWorkflow(workflow); - return workflow; + const workflowData = await workflowsApi.getWorkflow(rootStore.restApiContext, id); + addWorkflow(workflowData); + return workflowData; } async function getNewWorkflowData(name?: string, projectId?: string): Promise { diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 4f5f16caa5..f69d9b7eb9 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -5,13 +5,13 @@ import { nextTick, onActivated, onBeforeMount, - onBeforeUnmount, onDeactivated, onMounted, ref, useCssModule, watch, h, + onBeforeUnmount, } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue'; @@ -53,7 +53,9 @@ import { MAIN_HEADER_TABS, MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM, + NEW_WORKFLOW_ID, NODE_CREATOR_OPEN_SOURCES, + PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, STICKY_NODE_TYPE, VALID_WORKFLOW_IMPORT_URL_REGEX, @@ -152,7 +154,7 @@ const canvasEventBus = createEventBus(); const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({ route, }); -const { registerCustomAction } = useGlobalLinkActions(); +const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions(); const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router }); const { updateNodePosition, @@ -201,12 +203,18 @@ const isExecutionPreview = ref(false); const canOpenNDV = ref(true); const hideNodeIssues = ref(false); -const workflowId = computed(() => route.params.name as string); -const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]); +const initializedWorkflowId = ref(); +const workflowId = computed(() => { + const workflowIdParam = route.params.name as string; + return [PLACEHOLDER_EMPTY_WORKFLOW_ID, NEW_WORKFLOW_ID].includes(workflowIdParam) + ? undefined + : workflowIdParam; +}); -const isNewWorkflowRoute = computed(() => route.name === VIEWS.NEW_WORKFLOW); +const isNewWorkflowRoute = computed(() => route.name === VIEWS.NEW_WORKFLOW || !workflowId.value); +const isWorkflowRoute = computed(() => !!route?.meta?.nodeView); const isDemoRoute = computed(() => route.name === VIEWS.DEMO); -const isReadOnlyRoute = computed(() => route?.meta?.readOnlyCanvas === true); +const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas); const isReadOnlyEnvironment = computed(() => { return sourceControlStore.preferences.branchReadOnly; }); @@ -287,6 +295,10 @@ async function initializeRoute() { return; } + const isAlreadyInitialized = + initializedWorkflowId.value && + [NEW_WORKFLOW_ID, workflowId.value].includes(initializedWorkflowId.value); + // This function is called on route change as well, so we need to do the following: // - if the redirect is blank, then do nothing // - if the route is the template import view, then open the template @@ -296,11 +308,11 @@ async function initializeRoute() { } else if (route.name === VIEWS.TEMPLATE_IMPORT) { const templateId = route.params.id; await openWorkflowTemplate(templateId.toString()); - } else { + } else if (isWorkflowRoute.value && !isAlreadyInitialized) { historyStore.reset(); // If there is no workflow id, treat it as a new workflow - if (!workflowId.value || isNewWorkflowRoute.value) { + if (isNewWorkflowRoute.value || !workflowId.value) { if (route.meta?.nodeView === true) { await initializeWorkspaceForNewWorkflow(); } @@ -308,14 +320,14 @@ async function initializeRoute() { } await initializeWorkspaceForExistingWorkflow(workflowId.value); + + nodeHelpers.updateNodesInputIssues(); + nodeHelpers.updateNodesCredentialsIssues(); + nodeHelpers.updateNodesParameterIssues(); + + await loadCredentials(); + await initializeDebugMode(); } - - nodeHelpers.updateNodesInputIssues(); - nodeHelpers.updateNodesCredentialsIssues(); - nodeHelpers.updateNodesParameterIssues(); - - await loadCredentials(); - await initializeDebugMode(); } async function initializeWorkspaceForNewWorkflow() { @@ -325,11 +337,10 @@ async function initializeWorkspaceForNewWorkflow() { workflowsStore.makeNewWorkflowShareable(); uiStore.nodeViewInitialized = true; + initializedWorkflowId.value = NEW_WORKFLOW_ID; } async function initializeWorkspaceForExistingWorkflow(id: string) { - resetWorkspace(); - try { const workflowData = await workflowsStore.fetchWorkflow(id); @@ -339,7 +350,9 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { trackOpenWorkflowFromOnboardingTemplate(); } - await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject); + await projectsStore.setProjectNavActiveIdByWorkflowHomeProject( + editableWorkflow.value.homeProject, + ); collaborationStore.notifyWorkflowOpened(id); } catch (error) { @@ -350,6 +363,7 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { }); } finally { uiStore.nodeViewInitialized = true; + initializedWorkflowId.value = workflowId.value; } } @@ -359,7 +373,7 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { async function openWorkflow(data: IWorkflowDb) { resetWorkspace(); - titleSet(workflow.value.name, 'IDLE'); + titleSet(editableWorkflow.value.name, 'IDLE'); await initializeWorkspace(data); @@ -382,7 +396,7 @@ async function openWorkflow(data: IWorkflowDb) { function trackOpenWorkflowFromOnboardingTemplate() { telemetry.track( - `User opened workflow from onboarding template with ID ${workflow.value.meta?.onboardingId}`, + `User opened workflow from onboarding template with ID ${editableWorkflow.value.meta?.onboardingId}`, { workflow_id: workflowId.value, }, @@ -716,8 +730,8 @@ function onClickNodeAdd(source: string, sourceHandle: string) { async function loadCredentials() { let options: { workflowId: string } | { projectId: string }; - if (workflow.value) { - options = { workflowId: workflow.value.id }; + if (editableWorkflow.value) { + options = { workflowId: editableWorkflow.value.id }; } else { const queryParam = typeof route.query?.projectId === 'string' ? route.query?.projectId : undefined; @@ -917,7 +931,9 @@ function onClickConnectionAdd(connection: Connection) { */ const workflowPermissions = computed(() => { - return getResourcePermissions(workflowsStore.getWorkflowById(workflowId.value)?.scopes).workflow; + return workflowId.value + ? getResourcePermissions(workflowsStore.getWorkflowById(workflowId.value)?.scopes).workflow + : {}; }); const projectPermissions = computed(() => { @@ -1200,7 +1216,7 @@ async function onSourceControlPull() { loadCredentials(), ]); - if (workflowId.value !== null && !uiStore.stateIsDirty) { + if (workflowId.value && !uiStore.stateIsDirty) { const workflowData = await workflowsStore.fetchWorkflow(workflowId.value); if (workflowData) { titleSet(workflowData.name, 'IDLE'); @@ -1306,6 +1322,10 @@ async function onPostMessageReceived(messageEvent: MessageEvent) { */ function checkIfEditingIsAllowed(): boolean { + if (!initializedWorkflowId.value) { + return true; + } + if (readOnlyNotification.value?.visible) { return false; } @@ -1438,6 +1458,12 @@ function registerCustomActions() { }); } +function unregisterCustomActions() { + unregisterCustomAction('openNodeDetail'); + unregisterCustomAction('openSelectiveNodeCreator'); + unregisterCustomAction('showNodeCreator'); +} + /** * Routing */ @@ -1445,10 +1471,6 @@ function registerCustomActions() { watch( () => route.name, async () => { - if (!checkIfEditingIsAllowed()) { - return; - } - await initializeRoute(); }, ); @@ -1464,8 +1486,9 @@ onBeforeMount(() => { } }); -onMounted(async () => { +onMounted(() => { canvasStore.startLoading(); + titleReset(); resetWorkspace(); @@ -1479,6 +1502,8 @@ onMounted(async () => { .finally(() => { isLoading.value = false; canvasStore.stopLoading(); + + void externalHooks.run('nodeView.mount').catch(() => {}); }); void usersStore.showPersonalizationSurvey(); @@ -1486,34 +1511,31 @@ onMounted(async () => { checkIfRouteIsAllowed(); }); - addUndoRedoEventBindings(); - addPostMessageEventBindings(); addSourceControlEventBindings(); + addPostMessageEventBindings(); + addWorkflowSavedEventBindings(); + addBeforeUnloadEventBindings(); addImportEventBindings(); addExecutionOpenedEventBindings(); - addWorkflowSavedEventBindings(); - registerCustomActions(); - - // @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store - void externalHooks.run('nodeView.mount').catch(() => {}); }); -onActivated(() => { - addBeforeUnloadEventBindings(); -}); - -onBeforeUnmount(() => { - removeUndoRedoEventBindings(); - removePostMessageEventBindings(); - removeSourceControlEventBindings(); - removeImportEventBindings(); - removeExecutionOpenedEventBindings(); - removeWorkflowSavedEventBindings(); +onActivated(async () => { + addUndoRedoEventBindings(); }); onDeactivated(() => { + removeUndoRedoEventBindings(); +}); + +onBeforeUnmount(() => { + removeSourceControlEventBindings(); + removePostMessageEventBindings(); + removeWorkflowSavedEventBindings(); removeBeforeUnloadEventBindings(); + removeImportEventBindings(); + removeExecutionOpenedEventBindings(); + unregisterCustomActions(); collaborationStore.terminate(); }); diff --git a/packages/editor-ui/src/views/WorkflowExecutionsView.vue b/packages/editor-ui/src/views/WorkflowExecutionsView.vue index f09619fdd7..a9abf67ee4 100644 --- a/packages/editor-ui/src/views/WorkflowExecutionsView.vue +++ b/packages/editor-ui/src/views/WorkflowExecutionsView.vue @@ -8,7 +8,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils'; import { useToast } from '@/composables/useToast'; -import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants'; +import { NEW_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants'; import { useRoute, useRouter } from 'vue-router'; import type { ExecutionSummary } from 'n8n-workflow'; import { useDebounce } from '@/composables/useDebounce'; @@ -34,15 +34,22 @@ const loadingMore = ref(false); const workflow = ref(); const workflowId = computed(() => { - return (route.params.name as string) || workflowsStore.workflowId; + const workflowIdParam = route.params.name as string; + return [PLACEHOLDER_EMPTY_WORKFLOW_ID, NEW_WORKFLOW_ID].includes(workflowIdParam) + ? undefined + : workflowIdParam; }); const executionId = computed(() => route.params.executionId as string); -const executions = computed(() => [ - ...(executionsStore.currentExecutionsByWorkflowId[workflowId.value] ?? []), - ...(executionsStore.executionsByWorkflowId[workflowId.value] ?? []), -]); +const executions = computed(() => + workflowId.value + ? [ + ...(executionsStore.currentExecutionsByWorkflowId[workflowId.value] ?? []), + ...(executionsStore.executionsByWorkflowId[workflowId.value] ?? []), + ] + : [], +); const execution = computed(() => { return executions.value.find((e) => e.id === executionId.value) ?? currentExecution.value; @@ -65,13 +72,12 @@ watch( ); onMounted(async () => { - await nodeTypesStore.loadNodeTypesIfNotLoaded(); - await Promise.all([ - nodeTypesStore.loadNodeTypesIfNotLoaded(), - fetchWorkflow(), - executionsStore.initialize(workflowId.value), - ]); - await fetchExecution(); + await Promise.all([nodeTypesStore.loadNodeTypesIfNotLoaded(), fetchWorkflow()]); + + if (workflowId.value) { + await Promise.all([executionsStore.initialize(workflowId.value), fetchExecution()]); + } + await initializeRoute(); document.addEventListener('visibilitychange', onDocumentVisibilityChange); }); @@ -116,19 +122,23 @@ async function initializeRoute() { } async function fetchWorkflow() { - // Check if the workflow already has an ID - // In other words: are we coming from the Editor tab or browser loaded the Executions tab directly - if (workflowsStore.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) { - try { - await workflowsStore.fetchActiveWorkflows(); - const data = await workflowsStore.fetchWorkflow(workflowId.value); - workflowHelpers.initState(data); - await nodeHelpers.addNodes(data.nodes, data.connections); - } catch (error) { - toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title')); + if (workflowId.value) { + // Check if we are loading the Executions tab directly, without having loaded the workflow + if (workflowsStore.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + try { + await workflowsStore.fetchActiveWorkflows(); + const data = await workflowsStore.fetchWorkflow(workflowId.value); + workflowHelpers.initState(data); + await nodeHelpers.addNodes(data.nodes, data.connections); + } catch (error) { + toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title')); + } } + + workflow.value = workflowsStore.getWorkflowById(workflowId.value); + } else { + workflow.value = workflowsStore.workflow; } - workflow.value = workflowsStore.getWorkflowById(workflowId.value); } async function onAutoRefreshToggle(value: boolean) {