mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
fix(editor): Fix wf context for ai assistant setup node validations (#30664)
This commit is contained in:
+8
@@ -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: '<span><slot /></span>' },
|
||||
N8nTooltip: {
|
||||
|
||||
+1
@@ -104,6 +104,7 @@ function makeContext(isComplete: Ref<boolean>, 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),
|
||||
|
||||
+1
@@ -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),
|
||||
|
||||
+1
@@ -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),
|
||||
|
||||
+48
-7
@@ -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' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-25
@@ -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<INodeUi>(() => {
|
||||
} as INodeUi;
|
||||
});
|
||||
|
||||
const expressionContext = computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||
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<ExpressionLocalResolveContext | undefined>(() => ({
|
||||
localResolve: true,
|
||||
nodeName: displayNode.value.name,
|
||||
additionalKeys: {},
|
||||
}));
|
||||
|
||||
provide(ExpressionLocalResolveContextSymbol, expressionContext);
|
||||
provide(WorkflowDocumentStoreKey, workflowDocumentStore);
|
||||
|
||||
function onCredentialSelected(update: INodeUpdatePropertiesInformation) {
|
||||
if (!credentialType.value) return;
|
||||
|
||||
+3
@@ -36,6 +36,7 @@ export interface WorkflowSetupContext {
|
||||
credentialSelections: Ref<CredentialSelectionsMap>;
|
||||
terminalState: Ref<TerminalState | null>;
|
||||
isReady: Ref<boolean>;
|
||||
workflowId: ComputedRef<string | undefined>;
|
||||
projectId: ComputedRef<string | undefined>;
|
||||
credentialFlow: ComputedRef<InstanceAiCredentialFlow | undefined>;
|
||||
isActionPending: Ref<boolean>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user