From f2a86e9a1d4eee825692f56be284e17f852ac5a7 Mon Sep 17 00:00:00 2001 From: Svetoslav Dekov Date: Mon, 18 May 2026 21:37:31 +0300 Subject: [PATCH] fix(editor): Fix wf context for ai assistant setup node validations (#30664) --- .../WorkflowSetupSectionBody.test.ts | 8 +++ .../__tests__/WorkflowSetupWizard.test.ts | 1 + .../components/WorkflowSetupCard.test.ts | 1 + .../components/WorkflowSetupGroupCard.test.ts | 1 + .../WorkflowSetupSectionBody.test.ts | 55 ++++++++++++++++--- .../components/WorkflowSetupSectionBody.vue | 53 +++++++++--------- .../composables/useWorkflowSetupContext.ts | 3 + .../instance-ai-workflow-setup.spec.ts | 4 -- 8 files changed, 90 insertions(+), 36 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupSectionBody.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupSectionBody.test.ts index 484d7b63a2f..cf2158795cd 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupSectionBody.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupSectionBody.test.ts @@ -1,3 +1,4 @@ +import { createTestingPinia } from '@pinia/testing'; import { computed, ref } from 'vue'; import { mount } from '@vue/test-utils'; import { describe, expect, it, vi } from 'vitest'; @@ -50,6 +51,11 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({ useNodeTypesStore: () => ({ getNodeType: () => null, communityNodeType: () => null, + getAllNodeTypes: () => ({ + nodeTypes: {}, + init: async () => {}, + getByNameAndVersion: () => undefined, + }), }), })); @@ -69,6 +75,7 @@ function makeContext(): WorkflowSetupContext { credentialSelections: ref({}), terminalState: ref(null), isReady: ref(true), + workflowId: computed(() => undefined), projectId: computed(() => undefined), credentialFlow: computed(() => undefined), isActionPending: ref(false), @@ -96,6 +103,7 @@ function renderComponent(section: WorkflowSetupSection) { return mount(WorkflowSetupSectionBody, { props: { section }, global: { + plugins: [createTestingPinia({ stubActions: false })], stubs: { N8nText: { template: '' }, N8nTooltip: { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupWizard.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupWizard.test.ts index 8d640defceb..0392142c370 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupWizard.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/WorkflowSetupWizard.test.ts @@ -104,6 +104,7 @@ function makeContext(isComplete: Ref, options: ContextOptions = {}): Wo credentialSelections: ref({}), terminalState: ref(null), isReady: ref(true), + workflowId: computed(() => undefined), projectId: computed(() => undefined), credentialFlow: computed(() => undefined), isActionPending: options.isActionPending ?? ref(false), diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupCard.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupCard.test.ts index 5714d86d19f..0abdcd94519 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupCard.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupCard.test.ts @@ -83,6 +83,7 @@ function makeContext(section: WorkflowSetupSection): WorkflowSetupContext { credentialSelections: ref({}), terminalState: ref(null), isReady: ref(true), + workflowId: computed(() => undefined), projectId: computed(() => undefined), credentialFlow: computed(() => undefined), isActionPending: ref(false), diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.test.ts index eeb4a28e166..89f922b5f3c 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.test.ts @@ -64,6 +64,7 @@ function makeContext(overrides: ContextOverrides = {}): WorkflowSetupContext { credentialSelections: ref({}), terminalState: ref(null), isReady: ref(true), + workflowId: computed(() => undefined), projectId: computed(() => undefined), credentialFlow: computed(() => undefined), isActionPending: ref(false), diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.test.ts index bba39a4f445..c192a00390b 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.test.ts @@ -1,7 +1,9 @@ +import { createTestingPinia } from '@pinia/testing'; import { computed, nextTick, ref } from 'vue'; import { fireEvent } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createComponentRenderer } from '@/__tests__/render'; +import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import type { INodeUi } from '@/Interface'; import WorkflowSetupSectionBody from './WorkflowSetupSectionBody.vue'; import { makeWorkflowSetupSection } from '../__tests__/factories'; @@ -22,6 +24,9 @@ const nodeTypesStore = vi.hoisted(() => ({ })); const renderedCredentials = vi.hoisted(() => [] as unknown[]); +const workflowDocumentStoreRef = vi.hoisted(() => ({ + current: null as WorkflowDocumentStore | null, +})); vi.mock('../composables/useWorkflowSetupContext', () => ({ useWorkflowSetupContext: () => workflowSetupContext.current, @@ -35,11 +40,6 @@ vi.mock('@/app/stores/nodeTypes.store', () => ({ useNodeTypesStore: () => nodeTypesStore, })); -vi.mock('@/features/settings/environments.ee/environments.store', () => ({ - default: () => ({ variablesAsObject: {} }), - useEnvironmentsStore: () => ({ variablesAsObject: {} }), -})); - vi.mock('@/features/credentials/components/NodeCredentials.vue', () => ({ default: { props: ['node'], @@ -52,13 +52,17 @@ vi.mock('@/app/components/FreeAiCreditsCallout.vue', () => ({ })); vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async () => { - const { defineComponent, h } = await import('vue'); + const { defineComponent, h, inject } = await import('vue'); + const { WorkflowDocumentStoreKey } = await import('@/app/constants/injectionKeys'); return { default: defineComponent({ props: ['node'], emits: ['valueChanged', 'parameterBlur'], setup(props, { emit }) { + const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null); + workflowDocumentStoreRef.current = workflowDocumentStore?.value ?? null; + return () => { renderedCredentials.push((props.node as INodeUi | undefined)?.credentials); @@ -80,7 +84,9 @@ vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', async () }; }); -const renderComponent = createComponentRenderer(WorkflowSetupSectionBody); +const renderComponent = createComponentRenderer(WorkflowSetupSectionBody, { + pinia: createTestingPinia({ stubActions: false }), +}); function makeContext(section: WorkflowSetupSection): WorkflowSetupContext { const parameters = ref({ formId: '' }); @@ -95,6 +101,7 @@ function makeContext(section: WorkflowSetupSection): WorkflowSetupContext { credentialSelections: ref({ [section.targetNodeName]: { typeformApi: 'cred-1' } }), terminalState: ref(null), isReady: ref(true), + workflowId: computed(() => 'workflow-1'), projectId: computed(() => 'project-1'), credentialFlow: computed(() => undefined), isActionPending: ref(false), @@ -126,6 +133,7 @@ describe('WorkflowSetupSectionBody', () => { beforeEach(() => { vi.clearAllMocks(); renderedCredentials.length = 0; + workflowDocumentStoreRef.current = null; credentialsStore.getCredentialById.mockReturnValue({ id: 'cred-1', name: 'Typeform account' }); nodeTypesStore.getNodeType.mockReturnValue({ name: 'n8n-nodes-base.typeformTrigger', @@ -170,4 +178,37 @@ describe('WorkflowSetupSectionBody', () => { expect(renderedCredentials.at(-1)).toBe(credentialsBeforeParameterChange); }); + + it('provides a scoped workflow document store with the display node', async () => { + const section = makeWorkflowSetupSection({ + id: 'Typeform Trigger:typeformApi', + targetNodeName: 'Typeform Trigger', + parameterNames: ['formId'], + node: { + id: 'typeform-trigger', + name: 'Typeform Trigger', + type: 'n8n-nodes-base.typeformTrigger', + typeVersion: 1, + parameters: { formId: '' }, + }, + }); + workflowSetupContext.current = makeContext(section); + + const { getByTestId } = renderComponent({ props: { section } }); + await nextTick(); + + expect(workflowDocumentStoreRef.current?.documentId).toBe( + 'workflow-1@Typeform Trigger:typeformApi', + ); + expect(workflowDocumentStoreRef.current?.getNodeByName('Typeform Trigger')?.parameters).toEqual( + { formId: '' }, + ); + + await fireEvent.click(getByTestId('change-parameter')); + await nextTick(); + + expect(workflowDocumentStoreRef.current?.getNodeByName('Typeform Trigger')?.parameters).toEqual( + { formId: 'form-1' }, + ); + }); }); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.vue index cc7c24ebfcc..78a7f5ddd34 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupSectionBody.vue @@ -7,9 +7,12 @@ import FreeAiCreditsCallout from '@/app/components/FreeAiCreditsCallout.vue'; import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store'; -import { ExpressionLocalResolveContextSymbol } from '@/app/constants'; -import { Workflow, type IConnections, type INodeProperties } from 'n8n-workflow'; +import { ExpressionLocalResolveContextSymbol, WorkflowDocumentStoreKey } from '@/app/constants'; +import { + createWorkflowDocumentId, + useWorkflowDocumentStore, +} from '@/app/stores/workflowDocument.store'; +import type { INodeProperties } from 'n8n-workflow'; import type { ExpressionLocalResolveContext } from '@/app/types/expressions'; import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface'; import type { WorkflowSetupSection } from '../workflowSetup.types'; @@ -23,7 +26,6 @@ const ctx = useWorkflowSetupContext(); const i18n = useI18n(); const credentialsStore = useCredentialsStore(); const nodeTypesStore = useNodeTypesStore(); -const environmentsStore = useEnvironmentsStore(); const credentialType = computed(() => props.section.credentialType); @@ -97,30 +99,31 @@ const displayNode = computed(() => { } as INodeUi; }); -const expressionContext = computed(() => { - const node = displayNode.value; - const connections: IConnections = {}; - const workflow = new Workflow({ - id: 'instance-ai-workflow-setup', - name: 'Instance AI workflow setup', - nodes: [node], - connections, - active: false, - nodeTypes: nodeTypesStore.getAllNodeTypes(), - }); +const workflowDocumentStore = computed(() => + useWorkflowDocumentStore( + createWorkflowDocumentId( + ctx.workflowId.value ?? 'instance-ai-workflow-setup', + props.section.id, + ), + ), +); - return { - localResolve: true, - envVars: environmentsStore.variablesAsObject, - workflow, - execution: null, - nodeName: node.name, - additionalKeys: {}, - connections, - }; -}); +watch( + displayNode, + (node) => { + workflowDocumentStore.value.setNodes([node]); + }, + { immediate: true, deep: true }, +); + +const expressionContext = computed(() => ({ + localResolve: true, + nodeName: displayNode.value.name, + additionalKeys: {}, +})); provide(ExpressionLocalResolveContextSymbol, expressionContext); +provide(WorkflowDocumentStoreKey, workflowDocumentStore); function onCredentialSelected(update: INodeUpdatePropertiesInformation) { if (!credentialType.value) return; diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/composables/useWorkflowSetupContext.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/composables/useWorkflowSetupContext.ts index da15c86534d..a76c3a91d46 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/composables/useWorkflowSetupContext.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/composables/useWorkflowSetupContext.ts @@ -36,6 +36,7 @@ export interface WorkflowSetupContext { credentialSelections: Ref; terminalState: Ref; isReady: Ref; + workflowId: ComputedRef; projectId: ComputedRef; credentialFlow: ComputedRef; isActionPending: Ref; @@ -92,6 +93,7 @@ export function provideWorkflowSetupContext(opts: ProvideOptions): WorkflowSetup const inputsState = useWorkflowSetupInputs({ sections }); + const workflowId = computed(() => opts.workflowId.value); const projectId = computed(() => opts.projectId.value); const credentialFlow = computed(() => opts.credentialFlow.value); @@ -171,6 +173,7 @@ export function provideWorkflowSetupContext(opts: ProvideOptions): WorkflowSetup credentialSelections: inputsState.credentialSelections, terminalState: applyMachine.terminalState, isReady: bootstrap.isReady, + workflowId, projectId, credentialFlow, isActionPending: actions.isActionPending, diff --git a/packages/testing/playwright/tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts b/packages/testing/playwright/tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts index 8eb82989cb7..2e30cd9c368 100644 --- a/packages/testing/playwright/tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts +++ b/packages/testing/playwright/tests/e2e/instance-ai/instance-ai-workflow-setup.spec.ts @@ -774,10 +774,6 @@ test.describe( test('should clear required parameter issue indicator when the field is filled', async ({ n8n, }) => { - test.fixme( - true, - 'Broken by ParameterInput node-resolution refactor in #30219 — issues are computed against the global workflow document store node instead of the wizard displayNode.', - ); await n8n.api.workflows.createWorkflow( createParameterOnlyWorkflow(PARAMETER_ISSUE_WORKFLOW_NAME), );