mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
Merge 26c512ac4a into d06110ba9d
This commit is contained in:
commit
aee89d51e1
|
|
@ -329,17 +329,11 @@ export function createTestWorkflowExecutionResponse(
|
|||
export function createTestExpressionLocalResolveContext(
|
||||
data: Partial<ExpressionLocalResolveContext> = {},
|
||||
): ExpressionLocalResolveContext {
|
||||
const workflow = data.workflow ?? createTestWorkflowObject();
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
workflow,
|
||||
nodeName: 'n0',
|
||||
inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
|
||||
envVars: {},
|
||||
additionalKeys: {},
|
||||
connections: workflow.connectionsBySourceNode,
|
||||
execution: null,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { useExposeCssVar } from '@/app/composables/useExposeCssVar';
|
|||
import { useFloatingUiOffsets } from '@/app/composables/useFloatingUiOffsets';
|
||||
import { useWorkflowId } from '@/app/composables/useWorkflowId';
|
||||
import { WorkflowDocumentStoreKey, WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
const route = useRoute();
|
||||
const rootStore = useRootStore();
|
||||
|
|
@ -53,9 +53,7 @@ const defaultLocale = computed(() => rootStore.defaultLocale);
|
|||
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
||||
const hasContentFooter = ref(false);
|
||||
const workflowId = useWorkflowId();
|
||||
const currentWorkflowDocumentStore = shallowRef<ReturnType<typeof useWorkflowDocumentStore> | null>(
|
||||
null,
|
||||
);
|
||||
const currentWorkflowDocumentStore = shallowRef<WorkflowDocumentStore | null>(null);
|
||||
|
||||
provide(WorkflowIdKey, workflowId);
|
||||
provide(WorkflowDocumentStoreKey, currentWorkflowDocumentStore);
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ export function useNodeExecution(
|
|||
const updateInformation = await generateCodeForAiTransform(
|
||||
prompt,
|
||||
`parameters.${AI_TRANSFORM_JS_CODE}`,
|
||||
workflowDocumentStore.value.documentId,
|
||||
5,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import type { IWorkflowDb } from '@/Interface';
|
|||
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import {
|
||||
type useWorkflowDocumentStore,
|
||||
useWorkflowDocumentStore as createWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
type WorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useWorkflowImport } from '@/app/composables/useWorkflowImport';
|
||||
|
|
@ -31,7 +31,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
|||
|
||||
interface PostMessageHandlerDeps {
|
||||
workflowState: WorkflowState;
|
||||
currentWorkflowDocumentStore: ShallowRef<ReturnType<typeof useWorkflowDocumentStore> | null>;
|
||||
currentWorkflowDocumentStore: ShallowRef<WorkflowDocumentStore | null>;
|
||||
currentNDVStore: ShallowRef<ReturnType<typeof useNDVStore> | null>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,11 @@ export function useResolvedExpression({
|
|||
if (currentInvocation !== updateExpressionInvocation) return;
|
||||
|
||||
resolvedExpression.value = resolved.ok ? resolved.result : null;
|
||||
resolvedExpressionString.value = stringifyExpressionResult(resolved, hasRunData.value);
|
||||
resolvedExpressionString.value = stringifyExpressionResult(
|
||||
resolved,
|
||||
workflowDocumentStore.value.getPinDataSnapshot(),
|
||||
hasRunData.value,
|
||||
);
|
||||
} else {
|
||||
resolvedExpression.value = null;
|
||||
resolvedExpressionString.value = '';
|
||||
|
|
|
|||
|
|
@ -8,28 +8,13 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
|||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { useTagsStore } from '@/features/shared/tags/tags.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import {
|
||||
createMockNodeTypes,
|
||||
createTestExpressionLocalResolveContext,
|
||||
createTestNode,
|
||||
createTestTaskData,
|
||||
createTestWorkflow,
|
||||
createTestWorkflowExecutionResponse,
|
||||
createTestWorkflowObject,
|
||||
mockLoadedNodeType,
|
||||
mockNodeTypeDescription,
|
||||
} from '@/__tests__/mocks';
|
||||
import { createTestNode, createTestWorkflow, mockNodeTypeDescription } from '@/__tests__/mocks';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
createRunExecutionData,
|
||||
NodeConnectionTypes,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
} from 'n8n-workflow';
|
||||
import { CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from 'n8n-workflow';
|
||||
import type { AssignmentCollectionValue, IConnections } from 'n8n-workflow';
|
||||
import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { SLACK_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '../constants';
|
||||
import { SET_NODE_TYPE, SLACK_TRIGGER_NODE_TYPE } from '../constants';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
|
|
@ -1190,94 +1175,54 @@ describe('useWorkflowHelpers', () => {
|
|||
});
|
||||
|
||||
describe(resolveParameter, () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
});
|
||||
|
||||
describe('with local resolve context', () => {
|
||||
it('should resolve parameter without execution data', async () => {
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'n0' })],
|
||||
});
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowData.id),
|
||||
);
|
||||
workflowDocumentStore.hydrate(workflowData);
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
f0: '={{ 2 + 2 }}',
|
||||
f1: '={{ $vars.foo }}',
|
||||
f2: '={{ String($exotic).toUpperCase() }}',
|
||||
},
|
||||
workflowDocumentStore.documentId,
|
||||
{
|
||||
localResolve: true,
|
||||
envVars: {
|
||||
foo: 'hello!',
|
||||
},
|
||||
additionalKeys: {
|
||||
$exotic: true,
|
||||
},
|
||||
workflow: createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'n0' })],
|
||||
}),
|
||||
execution: null,
|
||||
nodeName: 'n0',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({ f0: 4, f1: 'hello!', f2: 'TRUE' });
|
||||
});
|
||||
|
||||
it('should resolve parameter with execution data', async () => {
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'n0' }), createTestNode({ name: 'n1' })],
|
||||
connections: {
|
||||
n0: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ type: NodeConnectionTypes.Main, index: 0, node: 'n1' }],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
f0: '={{ $json }}',
|
||||
f1: '={{ $("n0").item.json }}',
|
||||
},
|
||||
createTestExpressionLocalResolveContext({
|
||||
workflow: createTestWorkflowObject(workflowData),
|
||||
execution: createTestWorkflowExecutionResponse({
|
||||
workflowData,
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
n0: [
|
||||
createTestTaskData({
|
||||
data: { [NodeConnectionTypes.Main]: [[{ json: { foo: 777 } }]] },
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
nodeName: 'n1',
|
||||
inputNode: { name: 'n0', branchIndex: 0, runIndex: 0 },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
f0: { foo: 777 },
|
||||
f1: { foo: 777 },
|
||||
});
|
||||
expect(result).toEqual({ f0: 4, f2: 'TRUE' });
|
||||
});
|
||||
|
||||
it('should include $tool in additionalKeys for hitl tool node types', async () => {
|
||||
const toolNodeType = 'n8n-nodes-base.someHitlTool';
|
||||
const toolNodeTypes = createMockNodeTypes({
|
||||
[toolNodeType]: mockLoadedNodeType(toolNodeType),
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'toolNode', type: toolNodeType })],
|
||||
});
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowData.id),
|
||||
);
|
||||
workflowDocumentStore.hydrate(workflowData);
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
toolName: '={{ $tool.name }}',
|
||||
toolParams: '={{ $tool.parameters }}',
|
||||
},
|
||||
workflowDocumentStore.documentId,
|
||||
{
|
||||
localResolve: true,
|
||||
workflow: createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'toolNode', type: toolNodeType })],
|
||||
nodeTypes: toolNodeTypes,
|
||||
}),
|
||||
execution: null,
|
||||
nodeName: 'toolNode',
|
||||
additionalKeys: {},
|
||||
},
|
||||
|
|
@ -1288,16 +1233,20 @@ describe(resolveParameter, () => {
|
|||
});
|
||||
|
||||
it('should not include $tool in additionalKeys for non-tool node types', async () => {
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'regularNode', type: SET_NODE_TYPE })],
|
||||
});
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowData.id),
|
||||
);
|
||||
workflowDocumentStore.hydrate(workflowData);
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
toolCheck: '={{ $tool }}',
|
||||
},
|
||||
workflowDocumentStore.documentId,
|
||||
{
|
||||
localResolve: true,
|
||||
workflow: createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'regularNode', type: SET_NODE_TYPE })],
|
||||
}),
|
||||
execution: null,
|
||||
nodeName: 'regularNode',
|
||||
additionalKeys: {},
|
||||
},
|
||||
|
|
@ -1308,21 +1257,21 @@ describe(resolveParameter, () => {
|
|||
|
||||
it('should resolve $tool.name expression for tool nodes', async () => {
|
||||
const toolNodeType = 'n8n-nodes-base.someHitlTool';
|
||||
const toolNodeTypes = createMockNodeTypes({
|
||||
[toolNodeType]: mockLoadedNodeType(toolNodeType),
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'hitlTool', type: toolNodeType })],
|
||||
});
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowData.id),
|
||||
);
|
||||
workflowDocumentStore.hydrate(workflowData);
|
||||
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
message: '={{ "The agent wants to call " + $tool.name }}',
|
||||
},
|
||||
workflowDocumentStore.documentId,
|
||||
{
|
||||
localResolve: true,
|
||||
workflow: createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'hitlTool', type: toolNodeType })],
|
||||
nodeTypes: toolNodeTypes,
|
||||
}),
|
||||
execution: null,
|
||||
nodeName: 'hitlTool',
|
||||
additionalKeys: {},
|
||||
},
|
||||
|
|
@ -1333,21 +1282,21 @@ describe(resolveParameter, () => {
|
|||
|
||||
it('should resolve $tool.parameters expression for hitl tool nodes', async () => {
|
||||
const toolNodeType = 'n8n-nodes-base.someHitlTool';
|
||||
const toolNodeTypes = createMockNodeTypes({
|
||||
[toolNodeType]: mockLoadedNodeType(toolNodeType),
|
||||
const workflowData = createTestWorkflow({
|
||||
nodes: [createTestNode({ name: 'someTool', type: toolNodeType })],
|
||||
});
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowData.id),
|
||||
);
|
||||
workflowDocumentStore.hydrate(workflowData);
|
||||
|
||||
const result = await resolveParameter(
|
||||
{
|
||||
params: '={{ $tool.parameters }}',
|
||||
},
|
||||
workflowDocumentStore.documentId,
|
||||
{
|
||||
localResolve: true,
|
||||
workflow: createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'someTool', type: toolNodeType })],
|
||||
nodeTypes: toolNodeTypes,
|
||||
}),
|
||||
execution: null,
|
||||
nodeName: 'someTool',
|
||||
additionalKeys: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
import * as workflowUtils from 'n8n-workflow/common';
|
||||
|
||||
import type { INodeTypesMaxCount, INodeUi, IWorkflowDb, TargetItem, XYPosition } from '@/Interface';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type { INodeTypesMaxCount, IWorkflowDb, TargetItem, XYPosition } from '@/Interface';
|
||||
import type { ICredentialsResponse } from '@/features/credentials/credentials.types';
|
||||
import type { ITag } from '@n8n/rest-api-client/api/tags';
|
||||
import type { WorkflowData, WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
|
||||
|
|
@ -55,8 +54,8 @@ import {
|
|||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
injectWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import type { WorkflowObjectAccessors } from '../types';
|
||||
|
||||
export type ResolveParameterOptions = {
|
||||
targetItem?: TargetItem;
|
||||
|
|
@ -71,54 +70,31 @@ export type ResolveParameterOptions = {
|
|||
|
||||
export async function resolveParameter<T = IDataObject>(
|
||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||
opts: ResolveParameterOptions | ExpressionLocalResolveContext = {},
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
opts_: ResolveParameterOptions | ExpressionLocalResolveContext = {},
|
||||
): Promise<T | null> {
|
||||
if ('localResolve' in opts && opts.localResolve) {
|
||||
return await resolveParameterImpl(
|
||||
parameter,
|
||||
opts.workflow,
|
||||
opts.connections,
|
||||
opts.envVars,
|
||||
opts.workflow.getNode(opts.nodeName),
|
||||
opts.execution,
|
||||
opts.workflow.pinData,
|
||||
{
|
||||
inputNodeName: opts.inputNode?.name,
|
||||
inputRunIndex: opts.inputNode?.runIndex,
|
||||
inputBranchIndex: opts.inputNode?.branchIndex,
|
||||
additionalKeys: opts.additionalKeys,
|
||||
},
|
||||
);
|
||||
}
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
const envVars = useEnvironmentsStore().variablesAsObject;
|
||||
const ndvActiveNode =
|
||||
'localResolve' in opts_ && opts_.localResolve
|
||||
? workflowDocumentStore.getNodeByName(opts_.nodeName)
|
||||
: useNDVStore().activeNode;
|
||||
const opts: ResolveParameterOptions =
|
||||
'localResolve' in opts_ && opts_.localResolve
|
||||
? {
|
||||
inputNodeName: opts_.inputNode?.name,
|
||||
inputRunIndex: opts_.inputNode?.runIndex,
|
||||
inputBranchIndex: opts_.inputNode?.branchIndex,
|
||||
additionalKeys: opts_.additionalKeys,
|
||||
}
|
||||
: opts_;
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const workflowObject = workflowDocumentStore.getWorkflowObjectAccessorSnapshot();
|
||||
const connections = workflowDocumentStore.connectionsBySourceNode;
|
||||
const executionData = workflowsStore.workflowExecutionData;
|
||||
const pinData = workflowDocumentStore.getPinDataSnapshot();
|
||||
|
||||
return await resolveParameterImpl(
|
||||
parameter,
|
||||
workflowDocumentStore.getWorkflowObjectAccessorSnapshot(),
|
||||
workflowDocumentStore.connectionsBySourceNode,
|
||||
useEnvironmentsStore().variablesAsObject,
|
||||
useNDVStore().activeNode,
|
||||
workflowsStore.workflowExecutionData,
|
||||
workflowDocumentStore.getPinDataSnapshot(),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: move to separate file
|
||||
function resolveParameterImpl<T = IDataObject>(
|
||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||
workflowObject: WorkflowObjectAccessors,
|
||||
connections: IConnections,
|
||||
envVars: Record<string, string | boolean | number>,
|
||||
ndvActiveNode: INodeUi | null,
|
||||
executionData: IExecutionResponse | null,
|
||||
pinData: IPinData | undefined,
|
||||
opts: ResolveParameterOptions = {},
|
||||
): T | null {
|
||||
let itemIndex = opts?.targetItem?.itemIndex || 0;
|
||||
const activeNode = ndvActiveNode ?? workflowObject.getNode(opts.contextNodeName || '');
|
||||
|
||||
|
|
@ -299,6 +275,7 @@ function resolveParameterImpl<T = IDataObject>(
|
|||
export async function resolveRequiredParameters(
|
||||
currentParameter: INodeProperties,
|
||||
parameters: INodeParameters,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
opts: ResolveParameterOptions | ExpressionLocalResolveContext = {},
|
||||
): Promise<IDataObject | null> {
|
||||
const loadOptionsDependsOn = new Set(currentParameter?.typeOptions?.loadOptionsDependsOn ?? []);
|
||||
|
|
@ -309,10 +286,16 @@ export async function resolveRequiredParameters(
|
|||
const required = loadOptionsDependsOn.has(name);
|
||||
|
||||
if (required) {
|
||||
return [name, await resolveParameter(parameter as NodeParameterValue, opts)];
|
||||
return [
|
||||
name,
|
||||
await resolveParameter(parameter as NodeParameterValue, workflowDocumentId, opts),
|
||||
];
|
||||
} else {
|
||||
try {
|
||||
return [name, await resolveParameter(parameter as NodeParameterValue, opts)];
|
||||
return [
|
||||
name,
|
||||
await resolveParameter(parameter as NodeParameterValue, workflowDocumentId, opts),
|
||||
];
|
||||
} catch (error) {
|
||||
// ignore any expressions errors for non required parameters
|
||||
return [name, null];
|
||||
|
|
@ -651,7 +634,11 @@ export function useWorkflowHelpers() {
|
|||
__xxxxxxx__: expression,
|
||||
...siblingParameters,
|
||||
};
|
||||
const returnData: IDataObject | null = await resolveParameter(parameters, opts);
|
||||
const returnData: IDataObject | null = await resolveParameter(
|
||||
parameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
opts,
|
||||
);
|
||||
if (!returnData) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import type { INodeUi, IWorkflowDb } from '@/Interface';
|
|||
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
|
||||
import { getNodesWithNormalizedPosition } from '@/app/utils/nodeViewUtils';
|
||||
import {
|
||||
type useWorkflowDocumentStore,
|
||||
type WorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
|
||||
|
||||
export function useWorkflowImport(
|
||||
currentWorkflowDocumentStore: ShallowRef<ReturnType<typeof useWorkflowDocumentStore> | null>,
|
||||
currentWorkflowDocumentStore: ShallowRef<WorkflowDocumentStore | null>,
|
||||
currentNDVStore: ShallowRef<ReturnType<typeof useNDVStore> | null>,
|
||||
) {
|
||||
const route = useRoute();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
|||
import type { TelemetryContext } from '@/app/types/telemetry';
|
||||
import type { WorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import type { useExecutionDataStore } from '@/app/stores/executionData.store';
|
||||
import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
|
||||
import type { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
|
||||
|
|
@ -23,9 +23,8 @@ export const ExpressionLocalResolveContextSymbol: InjectionKey<
|
|||
> = Symbol('ExpressionLocalResolveContext');
|
||||
export const TelemetryContextSymbol: InjectionKey<TelemetryContext> = Symbol('TelemetryContext');
|
||||
export const WorkflowStateKey: InjectionKey<WorkflowState> = Symbol('WorkflowState');
|
||||
export const WorkflowDocumentStoreKey: InjectionKey<
|
||||
ShallowRef<ReturnType<typeof useWorkflowDocumentStore> | null>
|
||||
> = Symbol('WorkflowDocumentStore');
|
||||
export const WorkflowDocumentStoreKey: InjectionKey<ShallowRef<WorkflowDocumentStore | null>> =
|
||||
Symbol('WorkflowDocumentStore');
|
||||
export const ExecutionDataStoreKey: InjectionKey<
|
||||
ShallowRef<ReturnType<typeof useExecutionDataStore> | null>
|
||||
> = Symbol('ExecutionDataStore');
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
|||
}
|
||||
|
||||
return {
|
||||
documentId: id,
|
||||
workflowId,
|
||||
workflowVersion,
|
||||
...workflowDocumentName,
|
||||
|
|
@ -406,6 +407,8 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
|||
})();
|
||||
}
|
||||
|
||||
export type WorkflowDocumentStore = ReturnType<typeof useWorkflowDocumentStore>;
|
||||
|
||||
/**
|
||||
* Disposes a workflow document store instance.
|
||||
* Call this when a workflow document is unloaded (e.g., when navigating away from NodeView).
|
||||
|
|
@ -413,7 +416,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
|||
* Pinia's $dispose removes the store from its registry, but not from pinia.state.
|
||||
* Remove the state entry as well so recreating this scoped store starts clean.
|
||||
*/
|
||||
export function disposeWorkflowDocumentStore(store: ReturnType<typeof useWorkflowDocumentStore>) {
|
||||
export function disposeWorkflowDocumentStore(store: WorkflowDocumentStore) {
|
||||
const pinia = getActivePinia();
|
||||
store.$dispose();
|
||||
|
||||
|
|
@ -429,9 +432,7 @@ export function disposeWorkflowDocumentStore(store: ReturnType<typeof useWorkflo
|
|||
* Use this in composables/stores that need to interact with the current workflow's
|
||||
* document store and avoid calling this outside a component tree.
|
||||
*/
|
||||
export function injectWorkflowDocumentStore(): ShallowRef<
|
||||
ReturnType<typeof useWorkflowDocumentStore>
|
||||
> {
|
||||
export function injectWorkflowDocumentStore(): ShallowRef<WorkflowDocumentStore> {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const fallback = computed(() => {
|
||||
// TODO: once usages outside of a component tree is eliminated,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import type { Basic } from '@/Interface';
|
||||
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
|
||||
import type { IConnections, IWorkflowDataProxyAdditionalKeys } from 'n8n-workflow';
|
||||
import type { WorkflowObjectAccessors } from './workflow';
|
||||
import type { IWorkflowDataProxyAdditionalKeys } from 'n8n-workflow';
|
||||
|
||||
type Range = { from: number; to: number };
|
||||
|
||||
|
|
@ -39,11 +36,7 @@ export namespace ColoringStateEffect {
|
|||
*/
|
||||
export interface ExpressionLocalResolveContext {
|
||||
localResolve: true;
|
||||
envVars: Record<string, Basic>;
|
||||
additionalKeys: IWorkflowDataProxyAdditionalKeys;
|
||||
workflow: WorkflowObjectAccessors;
|
||||
connections: IConnections;
|
||||
execution: IExecutionResponse | null;
|
||||
nodeName: string;
|
||||
/**
|
||||
* Allowed to be undefined (e.g., trigger node, partial execution)
|
||||
|
|
|
|||
|
|
@ -12,36 +12,42 @@ describe('Utils: Expressions', () => {
|
|||
describe('stringifyExpressionResult()', () => {
|
||||
it('should return empty string for non-critical errors', () => {
|
||||
expect(
|
||||
stringifyExpressionResult({
|
||||
ok: false,
|
||||
error: new ExpressionError('error message', { type: 'no_execution_data' }),
|
||||
}),
|
||||
stringifyExpressionResult(
|
||||
{
|
||||
ok: false,
|
||||
error: new ExpressionError('error message', { type: 'no_execution_data' }),
|
||||
},
|
||||
{},
|
||||
),
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('should return an error message for critical errors', () => {
|
||||
expect(
|
||||
stringifyExpressionResult({
|
||||
ok: false,
|
||||
error: new ExpressionError('error message', { type: 'no_input_connection' }),
|
||||
}),
|
||||
stringifyExpressionResult(
|
||||
{
|
||||
ok: false,
|
||||
error: new ExpressionError('error message', { type: 'no_input_connection' }),
|
||||
},
|
||||
{},
|
||||
),
|
||||
).toEqual('[ERROR: No input connected]');
|
||||
});
|
||||
|
||||
it('should return empty string when result is null', () => {
|
||||
expect(stringifyExpressionResult({ ok: true, result: null })).toEqual('');
|
||||
expect(stringifyExpressionResult({ ok: true, result: null }, {})).toEqual('');
|
||||
});
|
||||
|
||||
it('should return NaN when result is NaN', () => {
|
||||
expect(stringifyExpressionResult({ ok: true, result: NaN })).toEqual('NaN');
|
||||
expect(stringifyExpressionResult({ ok: true, result: NaN }, {})).toEqual('NaN');
|
||||
});
|
||||
|
||||
it('should return [empty] message when result is empty string', () => {
|
||||
expect(stringifyExpressionResult({ ok: true, result: '' })).toEqual('[empty]');
|
||||
expect(stringifyExpressionResult({ ok: true, result: '' }, {})).toEqual('[empty]');
|
||||
});
|
||||
|
||||
it('should return the result when it is a string', () => {
|
||||
expect(stringifyExpressionResult({ ok: true, result: 'foo' })).toEqual('foo');
|
||||
expect(stringifyExpressionResult({ ok: true, result: 'foo' }, {})).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { i18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import type { ResolvableState } from '@/app/types/expressions';
|
||||
import { ExpressionError, ExpressionParser, isExpression, type Result } from 'n8n-workflow';
|
||||
import {
|
||||
ExpressionError,
|
||||
ExpressionParser,
|
||||
isExpression,
|
||||
type IPinData,
|
||||
type Result,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export { isExpression };
|
||||
|
||||
|
|
@ -79,7 +80,11 @@ export const getResolvableState = (error: unknown, ignoreError = false): Resolva
|
|||
return 'invalid';
|
||||
};
|
||||
|
||||
export const getExpressionErrorMessage = (error: Error, nodeHasRunData = false): string => {
|
||||
export const getExpressionErrorMessage = (
|
||||
error: Error,
|
||||
pinData: IPinData,
|
||||
nodeHasRunData = false,
|
||||
): string => {
|
||||
if (isNoExecDataExpressionError(error) || isPairedItemIntermediateNodesError(error)) {
|
||||
return i18n.baseText('expressionModalInput.noExecutionData');
|
||||
}
|
||||
|
|
@ -100,11 +105,7 @@ export const getExpressionErrorMessage = (error: Error, nodeHasRunData = false):
|
|||
|
||||
if (isInvalidPairedItemError(error) || isNoPairedItemError(error)) {
|
||||
const nodeCause = error.context.nodeCause as string;
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
const isPinned = !!workflowDocumentStore.pinData?.[nodeCause];
|
||||
const isPinned = !!pinData[nodeCause];
|
||||
|
||||
if (isPinned) {
|
||||
return i18n.baseText('expressionModalInput.pairedItemInvalidPinnedError', {
|
||||
|
|
@ -124,6 +125,7 @@ export const getExpressionErrorMessage = (error: Error, nodeHasRunData = false):
|
|||
|
||||
export const stringifyExpressionResult = (
|
||||
result: Result<unknown, Error>,
|
||||
pinData: IPinData,
|
||||
nodeHasRunData = false,
|
||||
): string => {
|
||||
if (!result.ok) {
|
||||
|
|
@ -131,7 +133,7 @@ export const stringifyExpressionResult = (
|
|||
return '';
|
||||
}
|
||||
|
||||
return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error, nodeHasRunData)}]`;
|
||||
return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error, pinData, nodeHasRunData)}]`;
|
||||
}
|
||||
|
||||
if (result.result === null) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createTestNode, createTestWorkflow, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import InputPanel, { type Props } from './InputPanel.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
|
@ -103,18 +103,12 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
|||
});
|
||||
}
|
||||
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
return createComponentRenderer(InputPanel, {
|
||||
props: {
|
||||
pushRef: 'pushRef',
|
||||
runIndex: 0,
|
||||
currentNodeName: nodes[0].name,
|
||||
activeNodeName: nodes[1].name,
|
||||
workflowObject,
|
||||
displayMode: 'schema',
|
||||
focusedMappableInput: '',
|
||||
isMappingOnboarded: false,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {
|
|||
NodeConnectionTypes,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types/workflow';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import InputNodeSelect from './InputNodeSelect.vue';
|
||||
import NodeExecuteButton from '@/app/components/NodeExecuteButton.vue';
|
||||
|
|
@ -42,7 +41,6 @@ type MappingMode = 'debugging' | 'mapping';
|
|||
|
||||
export type Props = {
|
||||
runIndex: number;
|
||||
workflowObject: WorkflowObjectAccessors;
|
||||
pushRef: string;
|
||||
activeNodeName: string;
|
||||
currentNodeName?: string;
|
||||
|
|
@ -116,6 +114,10 @@ const { runWorkflow } = useRunWorkflow({ router });
|
|||
const { canReveal, isDynamicCredentials, revealData } = useExecutionRedaction();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const workflowObject = computed(() =>
|
||||
workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const openWorkflowSettings = () => {
|
||||
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
||||
};
|
||||
|
|
@ -158,12 +160,12 @@ const isActiveNodeConfig = computed(() => {
|
|||
let inputs = activeNodeType.value?.inputs ?? [];
|
||||
let outputs = activeNodeType.value?.outputs ?? [];
|
||||
|
||||
if (props.workflowObject && activeNode.value) {
|
||||
const node = props.workflowObject.getNode(activeNode.value.name);
|
||||
if (activeNode.value) {
|
||||
const node = workflowDocumentStore.value.getNodeByName(activeNode.value.name);
|
||||
|
||||
if (node && activeNodeType.value) {
|
||||
inputs = NodeHelpers.getNodeInputs(props.workflowObject, node, activeNodeType.value);
|
||||
outputs = NodeHelpers.getNodeOutputs(props.workflowObject, node, activeNodeType.value);
|
||||
inputs = NodeHelpers.getNodeInputs(workflowObject.value, node, activeNodeType.value);
|
||||
outputs = NodeHelpers.getNodeOutputs(workflowObject.value, node, activeNodeType.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +219,7 @@ const isExecutingPrevious = computed(() => {
|
|||
|
||||
const rootNodesParents = computed(() => {
|
||||
if (!rootNode.value) return [];
|
||||
return props.workflowObject.getParentNodesByDepth(rootNode.value);
|
||||
return workflowObject.value.getParentNodesByDepth(rootNode.value);
|
||||
});
|
||||
|
||||
const currentNode = computed(() => {
|
||||
|
|
@ -244,7 +246,7 @@ const parentNodes = computed(() => {
|
|||
return [];
|
||||
}
|
||||
|
||||
const parents = props.workflowObject
|
||||
const parents = workflowObject.value
|
||||
.getParentNodesByDepth(activeNode.value.name)
|
||||
.filter((parent) => parent.name !== activeNode.value?.name);
|
||||
return uniqBy(parents, (parent) => parent.name);
|
||||
|
|
@ -271,7 +273,7 @@ const waitingMessage = computed(() => {
|
|||
|
||||
return waitingNodeTooltip(
|
||||
workflowDocumentStore?.value?.getNodeByName(parentNode.name) ?? null,
|
||||
props.workflowObject,
|
||||
workflowObject.value,
|
||||
parentRunData?.metadata,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import RunInfo from '@/features/ndv/runData/components/RunInfo.vue';
|
|||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types/workflow';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import RunDataAi from '@/features/ndv/runData/components/ai/RunDataAi.vue';
|
||||
import { useNodeType } from '@/app/composables/useNodeType';
|
||||
|
|
@ -29,6 +28,7 @@ import { N8nIcon, N8nRadioButtons, N8nSpinner, N8nText } from '@n8n/design-syste
|
|||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/app/constants';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
// Types
|
||||
|
||||
type RunDataRef = InstanceType<typeof RunData>;
|
||||
|
|
@ -43,7 +43,6 @@ type OutputTypeKey = keyof typeof OUTPUT_TYPE;
|
|||
type OutputType = (typeof OUTPUT_TYPE)[OutputTypeKey];
|
||||
|
||||
type Props = {
|
||||
workflowObject: WorkflowObjectAccessors;
|
||||
runIndex: number;
|
||||
isReadOnly?: boolean;
|
||||
linkedRuns?: boolean;
|
||||
|
|
@ -83,6 +82,7 @@ const ndvStore = injectNDVStore();
|
|||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const telemetry = useTelemetry();
|
||||
const i18n = useI18n();
|
||||
const { activeNode } = storeToRefs(ndvStore);
|
||||
|
|
@ -111,6 +111,10 @@ const collapsingColumnName = ref<string | null>(null);
|
|||
|
||||
// Computed
|
||||
|
||||
const workflowObject = computed(() =>
|
||||
workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
);
|
||||
|
||||
const node = computed(() => {
|
||||
return ndvStore.activeNode ?? undefined;
|
||||
});
|
||||
|
|
@ -127,7 +131,10 @@ const hasAiMetadata = computed(() => {
|
|||
}
|
||||
|
||||
if (node.value) {
|
||||
const connectedSubNodes = props.workflowObject.getParentNodes(node.value.name, 'ALL_NON_MAIN');
|
||||
const connectedSubNodes = workflowDocumentStore.value.getParentNodes(
|
||||
node.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
);
|
||||
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
||||
|
||||
return resultData && Array.isArray(resultData) && resultData.length > 0;
|
||||
|
|
@ -218,7 +225,7 @@ const allToolsWereUnusedNotice = computed(() => {
|
|||
// as it likely ends up unactionable noise to the user
|
||||
if (pinnedData.hasData.value) return undefined;
|
||||
|
||||
const toolsAvailable = props.workflowObject.getParentNodes(
|
||||
const toolsAvailable = workflowDocumentStore.value.getParentNodes(
|
||||
node.value.name,
|
||||
NodeConnectionTypes.AiTool,
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { removeExpressionPrefix } from '@/app/utils/expressions';
|
|||
import { propertyNameFromExpression } from '@/app/utils/mappingUtils';
|
||||
import { N8nIconButton, N8nTooltip } from '@n8n/design-system';
|
||||
import { typeFromExpression } from '../../utils/assignmentCollection.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
|
|
@ -48,6 +49,7 @@ const i18n = useI18n();
|
|||
const ndvStore = injectNDVStore();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
const { binaryDataAccessTooltip } = useBinaryDataAccessTooltip();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const assignmentTypeToNodeProperty = (
|
||||
type: string,
|
||||
|
|
@ -141,7 +143,10 @@ const onValueDrop = async (droppedExpression: string) => {
|
|||
}
|
||||
|
||||
const droppedValue = removeExpressionPrefix(droppedExpression);
|
||||
assignment.value.type = await typeFromExpression(droppedValue);
|
||||
assignment.value.type = await typeFromExpression(
|
||||
droppedValue,
|
||||
workflowDocumentStore.value.documentId,
|
||||
);
|
||||
|
||||
if (!assignment.value.name) {
|
||||
assignment.value.name = propertyNameFromExpression(droppedValue);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
|||
import { useExperimentalNdvStore } from '@/features/workflows/canvas/experimental/experimentalNdv.store';
|
||||
|
||||
import { N8nInputLabel } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
interface Props {
|
||||
parameter: INodeProperties;
|
||||
value: AssignmentCollectionValue;
|
||||
|
|
@ -60,6 +61,7 @@ const state = reactive<{ paramValue: AssignmentCollectionValue }>({
|
|||
paramValue: createParamValue(props.value),
|
||||
});
|
||||
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const ndvStore = injectNDVStore();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
const { callDebounced } = useDebounce();
|
||||
|
|
@ -119,7 +121,9 @@ function addAssignment(): void {
|
|||
}
|
||||
|
||||
async function dropAssignment(expression: string): Promise<void> {
|
||||
const type = props.defaultType ?? (await typeFromExpression(expression));
|
||||
const type =
|
||||
props.defaultType ??
|
||||
(await typeFromExpression(expression, workflowDocumentStore.value.documentId));
|
||||
state.paramValue.assignments.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: propertyNameFromExpression(expression),
|
||||
|
|
@ -181,7 +185,6 @@ function optionSelected(action: string) {
|
|||
node &&
|
||||
expressionLocalResolveCtx?.inputNode
|
||||
"
|
||||
:workflow="expressionLocalResolveCtx.workflow"
|
||||
:node="node"
|
||||
:input-node-name="expressionLocalResolveCtx.inputNode.name"
|
||||
:reference="dropAreaContainer?.$el"
|
||||
|
|
|
|||
|
|
@ -17,12 +17,16 @@ vi.mock('@n8n/stores/useRootStore');
|
|||
vi.mock('@/features/ai/assistant/assistant.api');
|
||||
vi.mock('@/app/stores/workflowDocument.store', async () => {
|
||||
const actual = await vi.importActual('@/app/stores/workflowDocument.store');
|
||||
const { shallowRef } = await import('vue');
|
||||
const mockStore = {
|
||||
getParentNodesByDepth: vi.fn().mockReturnValue([]),
|
||||
getNodeByName: vi.fn().mockReturnValue(null),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
useWorkflowDocumentStore: vi.fn(() => ({
|
||||
getParentNodesByDepth: vi.fn().mockReturnValue([]),
|
||||
})),
|
||||
useWorkflowDocumentStore: vi.fn(() => mockStore),
|
||||
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
|
||||
injectWorkflowDocumentStore: vi.fn(() => shallowRef(mockStore)),
|
||||
};
|
||||
});
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
|||
import DraggableTarget from '@/app/components/DraggableTarget.vue';
|
||||
|
||||
import { propertyNameFromExpression } from '@/app/utils/mappingUtils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
const AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT = 'codeGeneratedForPrompt';
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -32,6 +33,8 @@ export type Props = {
|
|||
const props = defineProps<Props>();
|
||||
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const activeNode = computed(() => ndvStore.activeNode);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
|
@ -106,6 +109,7 @@ async function onSubmit() {
|
|||
const updateInformation = await generateCodeForAiTransform(
|
||||
prompt.value,
|
||||
getPath(target as string),
|
||||
workflowDocumentStore.value.documentId,
|
||||
5,
|
||||
);
|
||||
if (!updateInformation) return;
|
||||
|
|
@ -158,7 +162,7 @@ function onPromptInput(inputValue: string) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
parentNodes.value = getParentNodes();
|
||||
parentNodes.value = getParentNodes(workflowDocumentStore.value.documentId);
|
||||
});
|
||||
|
||||
function cleanTextareaRowsData() {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import type { ConditionResult } from './types';
|
|||
import { useDebounce } from '@/app/composables/useDebounce';
|
||||
|
||||
import { N8nIcon, N8nIconButton, N8nTooltip } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
interface Props {
|
||||
path: string;
|
||||
condition: FilterConditionValue;
|
||||
|
|
@ -55,6 +56,7 @@ const emit = defineEmits<{
|
|||
|
||||
const i18n = useI18n();
|
||||
const { debounce } = useDebounce();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const condition = ref<FilterConditionValue>(props.condition);
|
||||
|
||||
|
|
@ -85,6 +87,7 @@ const conditionResult = computedAsync<ConditionResult>(
|
|||
return await resolveCondition({
|
||||
condition: currentCondition,
|
||||
options: currentOptions,
|
||||
workflowDocumentId: workflowDocumentStore.value.documentId,
|
||||
});
|
||||
},
|
||||
{ status: 'resolve_error' },
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { resolveParameter } from '@/app/composables/useWorkflowHelpers';
|
|||
import Draggable from 'vuedraggable';
|
||||
|
||||
import { N8nButton, N8nInputLabel } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
interface Props {
|
||||
parameter: INodeProperties;
|
||||
value: FilterValue;
|
||||
|
|
@ -53,6 +54,7 @@ const emit = defineEmits<{
|
|||
|
||||
const i18n = useI18n();
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const debouncedEmitChange = debounce(emitChange, { debounceTime: 1000 });
|
||||
|
|
@ -114,7 +116,10 @@ watchEffect(async () => {
|
|||
try {
|
||||
newOptions = {
|
||||
...DEFAULT_FILTER_OPTIONS,
|
||||
...(await resolveParameter(typeOptions as unknown as NodeParameterValue)),
|
||||
...(await resolveParameter(
|
||||
typeOptions as unknown as NodeParameterValue,
|
||||
workflowDocumentStore.value.documentId,
|
||||
)),
|
||||
};
|
||||
} catch {
|
||||
// Keep default options
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
|
||||
import type { ConditionResult, FilterOperator } from './types';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { WorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export const getFilterOperator = (key: string) =>
|
||||
OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator;
|
||||
|
|
@ -81,14 +82,17 @@ export const resolveCondition = async ({
|
|||
condition,
|
||||
options,
|
||||
index = 0,
|
||||
workflowDocumentId,
|
||||
}: {
|
||||
condition: FilterConditionValue;
|
||||
options: FilterOptionsValue;
|
||||
index?: number;
|
||||
workflowDocumentId: WorkflowDocumentId;
|
||||
}): Promise<ConditionResult> => {
|
||||
try {
|
||||
const resolved = (await resolveParameter(
|
||||
condition as unknown as NodeParameterValue,
|
||||
workflowDocumentId,
|
||||
)) as FilterConditionValue;
|
||||
|
||||
if (resolved.leftValue === undefined || resolved.rightValue === undefined) {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,10 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
|||
import {
|
||||
createTestExpressionLocalResolveContext,
|
||||
createMockEnterpriseSettings,
|
||||
createTestNode,
|
||||
createTestWorkflowObject,
|
||||
createTestNodeProperties,
|
||||
} from '@/__tests__/mocks';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { NodeConnectionTypes, type INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import { type INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import type { IWorkflowDb, WorkflowListResource } from '@/Interface';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
|
|
@ -723,16 +721,7 @@ describe('ParameterInput.vue', () => {
|
|||
});
|
||||
|
||||
describe('data mapper', () => {
|
||||
const workflow = createTestWorkflowObject({
|
||||
nodes: [createTestNode({ name: 'n0' }), createTestNode({ name: 'n1' })],
|
||||
connections: {
|
||||
n1: {
|
||||
[NodeConnectionTypes.Main]: [[{ node: 'n0', index: 0, type: NodeConnectionTypes.Main }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
const ctx = createTestExpressionLocalResolveContext({
|
||||
workflow,
|
||||
nodeName: 'n0',
|
||||
inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -236,9 +236,9 @@ const isFocused = ref(false);
|
|||
const isSwitchingMode = ref(false);
|
||||
|
||||
const node = computed(() => {
|
||||
const contextNode = expressionLocalResolveCtx?.value?.workflow.getNode(
|
||||
expressionLocalResolveCtx.value.nodeName,
|
||||
);
|
||||
const contextNode =
|
||||
expressionLocalResolveCtx?.value &&
|
||||
workflowDocumentStore.value.getNodeByName(expressionLocalResolveCtx.value.nodeName);
|
||||
return contextNode ?? ndvStore.activeNode ?? undefined;
|
||||
});
|
||||
const nodeType = computed(
|
||||
|
|
@ -492,7 +492,10 @@ const dependentParametersValues = computedAsync(async () => {
|
|||
// Get the resolved parameter values of the current node
|
||||
const currentNodeParameters = node.value?.parameters;
|
||||
try {
|
||||
const resolvedNodeParameters = await workflowHelpers.resolveParameter(currentNodeParameters);
|
||||
const resolvedNodeParameters = await workflowHelpers.resolveParameter(
|
||||
currentNodeParameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
);
|
||||
|
||||
const returnValues: string[] = [];
|
||||
for (let parameterPath of loadOptionsDependsOn) {
|
||||
|
|
@ -813,6 +816,7 @@ async function loadRemoteParameterOptions() {
|
|||
const resolvedNodeParameters = (await workflowHelpers.resolveRequiredParameters(
|
||||
props.parameter,
|
||||
currentNodeParameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
expressionLocalResolveCtx?.value ?? {},
|
||||
)) as INodeParameters;
|
||||
const loadOptionsMethod = getTypeOption('loadOptionsMethod');
|
||||
|
|
@ -1431,7 +1435,6 @@ onUpdated(async () => {
|
|||
|
||||
<ExperimentalEmbeddedNdvMapper
|
||||
v-if="wrapper && isMapperAvailable && node && expressionLocalResolveCtx?.inputNode"
|
||||
:workflow="expressionLocalResolveCtx.workflow"
|
||||
:node="node"
|
||||
:input-node-name="expressionLocalResolveCtx.inputNode.name"
|
||||
:reference="wrapper"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { ChatHubToolContextKey, ExpressionLocalResolveContextSymbol } from '@/ap
|
|||
import { N8nInputLabel } from '@n8n/design-system';
|
||||
import { useCollectionOverhaul } from '@/app/composables/useCollectionOverhaul';
|
||||
import type { ParameterOptionsOverrides } from '@/features/ndv/shared/ndv.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
type Props = {
|
||||
parameter: INodeProperties;
|
||||
|
|
@ -86,6 +87,7 @@ const forceShowExpression = ref(false);
|
|||
const wrapperHovered = ref(false);
|
||||
|
||||
const ndvStore = injectNDVStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const telemetry = useTelemetry();
|
||||
const { isEnabled: isCollectionOverhaulEnabled } = useCollectionOverhaul();
|
||||
|
||||
|
|
@ -95,7 +97,7 @@ const activeNode = computed(() => {
|
|||
const ctx = expressionLocalResolveCtx?.value;
|
||||
|
||||
if (ctx) {
|
||||
return ctx.workflow.getNode(ctx.nodeName);
|
||||
return workflowDocumentStore.value.getNodeByName(ctx.nodeName);
|
||||
}
|
||||
|
||||
return ndvStore.activeNode;
|
||||
|
|
|
|||
|
|
@ -516,7 +516,10 @@ async function getDependentParametersValues(parameter: INodeProperties): Promise
|
|||
// Get the resolved parameter values of the current node
|
||||
const currentNodeParameters = ndvStore.activeNode?.parameters;
|
||||
try {
|
||||
const resolvedNodeParameters = await workflowHelpers.resolveParameter(currentNodeParameters);
|
||||
const resolvedNodeParameters = await workflowHelpers.resolveParameter(
|
||||
currentNodeParameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
);
|
||||
|
||||
const returnValues: string[] = [];
|
||||
for (let parameterPath of loadOptionsDependsOn) {
|
||||
|
|
|
|||
|
|
@ -952,6 +952,7 @@ describe('ResourceLocator', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(mockResolveRequiredParameters).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
testContext,
|
||||
|
|
@ -972,6 +973,7 @@ describe('ResourceLocator', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(mockResolveRequiredParameters).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{},
|
||||
|
|
@ -1009,6 +1011,7 @@ describe('ResourceLocator', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(mockResolveRequiredParameters).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
testContext,
|
||||
|
|
|
|||
|
|
@ -431,6 +431,7 @@ const handleAddResourceClick = async () => {
|
|||
const resolvedNodeParameters = await workflowHelpers.resolveRequiredParameters(
|
||||
props.parameter,
|
||||
currentRequestParams.value.parameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
expressionLocalResolveCtx?.value ?? {},
|
||||
);
|
||||
|
||||
|
|
@ -821,6 +822,7 @@ async function loadResources() {
|
|||
const resolvedNodeParameters = (await workflowHelpers.resolveRequiredParameters(
|
||||
props.parameter,
|
||||
params.parameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
expressionLocalResolveCtx?.value ?? {},
|
||||
)) as INodeParameters;
|
||||
const loadOptionsMethod = getPropertyArgument(currentMode.value, 'searchListMethod') as string;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { useProjectsStore } from '@/features/collaboration/projects/projects.sto
|
|||
import ParameterInputFull from '../ParameterInputFull.vue';
|
||||
|
||||
import { N8nButton, N8nCallout, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
type Props = {
|
||||
parameter: INodeProperties;
|
||||
node: INode | null;
|
||||
|
|
@ -51,6 +52,7 @@ const ndvStore = injectNDVStore();
|
|||
const workflowsStore = useWorkflowsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
teleported: true,
|
||||
|
|
@ -334,6 +336,7 @@ const createRequestParams = async (methodName: string) => {
|
|||
currentNodeParameters: (await resolveRequiredParameters(
|
||||
props.parameter,
|
||||
props.node.parameters,
|
||||
workflowDocumentStore.value.documentId,
|
||||
expressionLocalResolveCtx?.value ?? {},
|
||||
)) as INodeParameters,
|
||||
path: props.path,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { AssignmentValue, IDataObject } from 'n8n-workflow';
|
|||
import { resolveParameter } from '@/app/composables/useWorkflowHelpers';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { isBinaryLike } from '@/app/utils/typeGuards';
|
||||
import type { WorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export function inferAssignmentType(value: unknown): string {
|
||||
if (typeof value === 'boolean') return 'boolean';
|
||||
|
|
@ -14,9 +15,12 @@ export function inferAssignmentType(value: unknown): string {
|
|||
return 'string';
|
||||
}
|
||||
|
||||
export async function typeFromExpression(expression: string): Promise<string> {
|
||||
export async function typeFromExpression(
|
||||
expression: string,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const resolved = await resolveParameter(`=${expression}`);
|
||||
const resolved = await resolveParameter(`=${expression}`, workflowDocumentId);
|
||||
return inferAssignmentType(resolved);
|
||||
} catch {
|
||||
return 'string';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createPinia, setActivePinia } from 'pinia';
|
|||
import { generateCodeForPrompt } from '@/features/ai/assistant/assistant.api';
|
||||
import type { AskAiRequest } from '@/features/ai/assistant/assistant.types';
|
||||
import type { Schema } from '@/Interface';
|
||||
import { createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('./utils', async () => {
|
||||
const actual = await vi.importActual('./utils');
|
||||
|
|
@ -55,7 +56,12 @@ describe('generateCodeForAiTransform - Retry Tests', () => {
|
|||
.mockRejectedValueOnce(new Error('First attempt failed'))
|
||||
.mockResolvedValueOnce({ code: mockGeneratedCode });
|
||||
|
||||
const result = await generateCodeForAiTransform('test prompt', 'test/path', 2);
|
||||
const result = await generateCodeForAiTransform(
|
||||
'test prompt',
|
||||
'test/path',
|
||||
createWorkflowDocumentId(''),
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test/path',
|
||||
|
|
@ -67,9 +73,9 @@ describe('generateCodeForAiTransform - Retry Tests', () => {
|
|||
it('should exhaust retries and throw an error', async () => {
|
||||
vi.mocked(generateCodeForPrompt).mockRejectedValue(new Error('All attempts failed'));
|
||||
|
||||
await expect(generateCodeForAiTransform('test prompt', 'test/path', 3)).rejects.toThrow(
|
||||
'All attempts failed',
|
||||
);
|
||||
await expect(
|
||||
generateCodeForAiTransform('test prompt', 'test/path', createWorkflowDocumentId(''), 3),
|
||||
).rejects.toThrow('All attempts failed');
|
||||
|
||||
expect(generateCodeForPrompt).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
|
@ -78,7 +84,11 @@ describe('generateCodeForAiTransform - Retry Tests', () => {
|
|||
const mockGeneratedCode = 'const example = "no retries needed";';
|
||||
vi.mocked(generateCodeForPrompt).mockResolvedValue({ code: mockGeneratedCode });
|
||||
|
||||
const result = await generateCodeForAiTransform('test prompt', 'test/path');
|
||||
const result = await generateCodeForAiTransform(
|
||||
'test prompt',
|
||||
'test/path',
|
||||
createWorkflowDocumentId(''),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'test/path',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Schema } from '@/Interface';
|
||||
import { ApplicationError, type INodeExecutionData } from 'n8n-workflow';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useDataSchema } from '@/app/composables/useDataSchema';
|
||||
import { executionDataToJson } from '@/app/utils/nodeTypesUtils';
|
||||
|
|
@ -12,8 +11,8 @@ import { format } from 'prettier';
|
|||
import jsParser from 'prettier/plugins/babel';
|
||||
import * as estree from 'prettier/plugins/estree';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export type TextareaRowData = {
|
||||
|
|
@ -21,10 +20,9 @@ export type TextareaRowData = {
|
|||
linesToRowsMap: number[][];
|
||||
};
|
||||
|
||||
export function getParentNodes() {
|
||||
export function getParentNodes(workflowDocumentId: WorkflowDocumentId) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
const activeNode = useNDVStore().activeNode;
|
||||
const { workflowId } = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
|
||||
if (!activeNode) return [];
|
||||
|
||||
|
|
@ -37,8 +35,8 @@ export function getParentNodes() {
|
|||
.filter((n) => n !== null);
|
||||
}
|
||||
|
||||
export function getSchemas() {
|
||||
const parentNodes = getParentNodes();
|
||||
export function getSchemas(workflowDocumentId: WorkflowDocumentId) {
|
||||
const parentNodes = getParentNodes(workflowDocumentId);
|
||||
const parentNodesNames = parentNodes.map((node) => node?.name);
|
||||
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
|
||||
const parentNodesSchemas: Array<{ nodeName: string; schema: Schema }> = parentNodes
|
||||
|
|
@ -185,8 +183,13 @@ export function reducePayloadSizeOrThrow(
|
|||
if (remainingTokensToReduce > 0) throw error;
|
||||
}
|
||||
|
||||
export async function generateCodeForAiTransform(prompt: string, path: string, retries = 1) {
|
||||
const schemas = getSchemas();
|
||||
export async function generateCodeForAiTransform(
|
||||
prompt: string,
|
||||
path: string,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
retries = 1,
|
||||
) {
|
||||
const schemas = getSchemas(workflowDocumentId);
|
||||
|
||||
const payload: AskAiRequest.RequestPayload = {
|
||||
question: prompt,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ const workflowRunData = computed(() => {
|
|||
|
||||
const parentNodes = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return workflowObject.value?.getParentNodesByDepth(activeNode.value.name, 1) ?? [];
|
||||
return workflowDocumentStore.value.getParentNodesByDepth(activeNode.value.name, 1) ?? [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
|
@ -170,7 +170,7 @@ const inputNodeName = computed<string | undefined>(() => {
|
|||
// For sub-nodes, we need to get their connected output node to determine the input
|
||||
// because sub-nodes use specialized outputs (e.g. NodeConnectionTypes.AiTool)
|
||||
// instead of the standard Main output type
|
||||
const connectedOutputNode = workflowObject.value?.getChildNodes(
|
||||
const connectedOutputNode = workflowDocumentStore.value.getChildNodes(
|
||||
activeNode.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
)?.[0];
|
||||
|
|
@ -247,7 +247,7 @@ const maxInputRun = computed(() => {
|
|||
return 0;
|
||||
}
|
||||
|
||||
const workflowNode = workflowObject.value?.getNode(activeNode.value.name);
|
||||
const workflowNode = workflowDocumentStore.value.getNodeByName(activeNode.value.name);
|
||||
|
||||
if (!workflowNode || !activeNodeType.value || !workflowObject.value) {
|
||||
return 0;
|
||||
|
|
@ -746,8 +746,7 @@ onBeforeUnmount(() => {
|
|||
@activate="onWorkflowActivate"
|
||||
/>
|
||||
<InputPanel
|
||||
v-else-if="!isTriggerNode && workflowObject"
|
||||
:workflow-object="workflowObject"
|
||||
v-else-if="!isTriggerNode"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
:linked-runs="linked"
|
||||
|
|
@ -775,9 +774,7 @@ onBeforeUnmount(() => {
|
|||
</template>
|
||||
<template #output>
|
||||
<OutputPanel
|
||||
v-if="workflowObject"
|
||||
data-test-id="output-panel"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="outputRun"
|
||||
:linked-runs="linked"
|
||||
|
|
|
|||
|
|
@ -134,8 +134,8 @@ const workflowRunData = computed(() => {
|
|||
const parentNodes = computed(() => {
|
||||
if (activeNode.value) {
|
||||
return (
|
||||
workflowObject.value
|
||||
?.getParentNodesByDepth(activeNode.value.name, 1)
|
||||
workflowDocumentStore.value
|
||||
.getParentNodesByDepth(activeNode.value.name, 1)
|
||||
.map(({ name }) => name) || []
|
||||
);
|
||||
} else {
|
||||
|
|
@ -174,7 +174,7 @@ const inputNodeName = computed<string | undefined>(() => {
|
|||
// For sub-nodes, we need to get their connected output node to determine the input
|
||||
// because sub-nodes use specialized outputs (e.g. NodeConnectionTypes.AiTool)
|
||||
// instead of the standard Main output type
|
||||
const connectedOutputNode = workflowObject.value?.getChildNodes(
|
||||
const connectedOutputNode = workflowDocumentStore.value.getChildNodes(
|
||||
activeNode.value.name,
|
||||
'ALL_NON_MAIN',
|
||||
)?.[0];
|
||||
|
|
@ -251,7 +251,7 @@ const maxInputRun = computed(() => {
|
|||
return 0;
|
||||
}
|
||||
|
||||
const workflowNode = workflowObject.value?.getNode(activeNode.value.name);
|
||||
const workflowNode = workflowDocumentStore.value.getNodeByName(activeNode.value.name);
|
||||
|
||||
if (!workflowNode || !activeNodeType.value || !workflowObject.value) {
|
||||
return 0;
|
||||
|
|
@ -759,7 +759,6 @@ onBeforeUnmount(() => {
|
|||
/>
|
||||
<InputPanel
|
||||
v-else-if="!isTriggerNode && workflowObject"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
:linked-runs="linked"
|
||||
|
|
@ -829,9 +828,7 @@ onBeforeUnmount(() => {
|
|||
:style="{ width: `${panelWidthPercentage.right}%` }"
|
||||
>
|
||||
<OutputPanel
|
||||
v-if="workflowObject"
|
||||
data-test-id="output-panel"
|
||||
:workflow-object="workflowObject"
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="outputRun"
|
||||
:linked-runs="linked"
|
||||
|
|
|
|||
|
|
@ -48,9 +48,13 @@ import { useI18n } from '@n8n/i18n';
|
|||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useAutocompleteTelemetry } from '@/app/composables/useAutocompleteTelemetry';
|
||||
import { ignoreUpdateAnnotation } from '@/app/utils/forceParse';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from '../plugins/codemirror/completions/constants';
|
||||
import {
|
||||
TARGET_NODE_PARAMETER_FACET,
|
||||
WORKFLOW_DOCUMENT_FACET,
|
||||
} from '../plugins/codemirror/completions/constants';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
import { isEventTargetContainedBy } from '@/app/utils/htmlUtils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export const useExpressionEditor = ({
|
||||
editorRef,
|
||||
|
|
@ -76,6 +80,7 @@ export const useExpressionEditor = ({
|
|||
onChange?: (viewUpdate: ViewUpdate) => void;
|
||||
}) => {
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const { isMacOs } = useDeviceSupport();
|
||||
|
|
@ -241,6 +246,7 @@ export const useExpressionEditor = ({
|
|||
? { nodeName: expressionLocalResolveContext.value.nodeName, parameterPath: '' }
|
||||
: toValue(targetNodeParameterContext),
|
||||
),
|
||||
WORKFLOW_DOCUMENT_FACET.of(workflowDocumentStore.value.documentId),
|
||||
customExtensions.value.of(toValue(extensions)),
|
||||
readOnlyExtensions.value.of([EditorState.readOnly.of(toValue(isReadOnly))]),
|
||||
telemetryExtensions.value.of([]),
|
||||
|
|
@ -388,7 +394,7 @@ export const useExpressionEditor = ({
|
|||
!!workflowsStore.workflowExecutionData?.data?.resultData?.runData[
|
||||
ndvStore.activeNode?.name ?? ''
|
||||
];
|
||||
result.resolved = `[${getExpressionErrorMessage(error, hasRunData)}]`;
|
||||
result.resolved = `[${getExpressionErrorMessage(error, workflowDocumentStore.value.getPinDataSnapshot(), hasRunData)}]`;
|
||||
result.error = true;
|
||||
result.fullError = error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ import type { CompletionSource, CompletionResult } from '@codemirror/autocomplet
|
|||
import { CompletionContext } from '@codemirror/autocomplete';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { n8nLang } from '@/features/shared/editors/plugins/codemirror/n8nLang';
|
||||
import { LUXON_RECOMMENDED_OPTIONS, STRING_RECOMMENDED_OPTIONS } from './constants';
|
||||
import {
|
||||
LUXON_RECOMMENDED_OPTIONS,
|
||||
STRING_RECOMMENDED_OPTIONS,
|
||||
WORKFLOW_DOCUMENT_FACET,
|
||||
} from './constants';
|
||||
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -107,7 +112,7 @@ export async function completions(docWithCursor: string, explicit = false) {
|
|||
const state = EditorState.create({
|
||||
doc,
|
||||
selection: { anchor: cursorPosition },
|
||||
extensions: [n8nLang()],
|
||||
extensions: [n8nLang(), WORKFLOW_DOCUMENT_FACET.of('test@latest')],
|
||||
});
|
||||
|
||||
const context = new CompletionContext(state, cursorPosition, explicit);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { prefixMatch, longestCommonPrefix, resolveAutocompleteExpression } from
|
|||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { Resolved } from './types';
|
||||
import { escapeMappingString } from '@/app/utils/mappingUtils';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from './constants';
|
||||
import { TARGET_NODE_PARAMETER_FACET, WORKFLOW_DOCUMENT_FACET } from './constants';
|
||||
|
||||
/**
|
||||
* Resolution-based completions offered at the start of bracket access notation.
|
||||
|
|
@ -18,6 +18,7 @@ export async function bracketAccessCompletions(
|
|||
context: CompletionContext,
|
||||
): Promise<CompletionResult | null> {
|
||||
const targetNodeParameterContext = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||
const workflowDocumentId = context.state.facet(WORKFLOW_DOCUMENT_FACET);
|
||||
const word = context.matchBefore(/\$[\S\s]*\[.*/);
|
||||
|
||||
if (!word) return null;
|
||||
|
|
@ -36,6 +37,7 @@ export async function bracketAccessCompletions(
|
|||
try {
|
||||
resolved = await resolveAutocompleteExpression(
|
||||
`={{ ${base} }}`,
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext?.nodeName,
|
||||
);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ import {
|
|||
METHODS_SECTION,
|
||||
RECOMMENDED_SECTION,
|
||||
STRING_RECOMMENDED_OPTIONS,
|
||||
WORKFLOW_DOCUMENT_FACET,
|
||||
} from './constants';
|
||||
|
||||
import set from 'lodash/set';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { mockNodes } from '@/__tests__/mocks';
|
||||
|
|
@ -45,7 +47,7 @@ export async function completions(docWithCursor: string, explicit = false) {
|
|||
const state = EditorState.create({
|
||||
doc,
|
||||
selection: { anchor: cursorPosition },
|
||||
extensions: [n8nLang()],
|
||||
extensions: [n8nLang(), WORKFLOW_DOCUMENT_FACET.of('test@latest')],
|
||||
});
|
||||
|
||||
const context = new CompletionContext(state, cursorPosition, explicit);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { withSectionHeader } from './utils';
|
|||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
import { Facet } from '@codemirror/state';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
import type { WorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export const FIELDS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.fields'),
|
||||
|
|
@ -465,3 +466,7 @@ export const TARGET_NODE_PARAMETER_FACET = Facet.define<
|
|||
>({
|
||||
combine: (values) => values[0],
|
||||
});
|
||||
|
||||
export const WORKFLOW_DOCUMENT_FACET = Facet.define<WorkflowDocumentId, WorkflowDocumentId>({
|
||||
combine: (values) => values[0],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
STRING_SECTIONS,
|
||||
TARGET_NODE_PARAMETER_FACET,
|
||||
VARIABLE_SECTIONS,
|
||||
WORKFLOW_DOCUMENT_FACET,
|
||||
} from './constants';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs';
|
||||
|
|
@ -66,6 +67,7 @@ import { javascriptLanguage } from '@codemirror/lang-javascript';
|
|||
import { isPairedItemIntermediateNodesError } from '@/app/utils/expressions';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import type { WorkflowDocumentId } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
/**
|
||||
* Resolution-based completions offered according to datatype.
|
||||
|
|
@ -74,6 +76,7 @@ export async function datatypeCompletions(
|
|||
context: CompletionContext,
|
||||
): Promise<CompletionResult | null> {
|
||||
const targetNodeParameterContext = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||
const workflowDocumentId = context.state.facet(WORKFLOW_DOCUMENT_FACET);
|
||||
const word = context.matchBefore(DATATYPE_REGEX);
|
||||
|
||||
if (!word) return null;
|
||||
|
|
@ -102,6 +105,7 @@ export async function datatypeCompletions(
|
|||
try {
|
||||
resolved = await resolveAutocompleteExpression(
|
||||
`={{ ${base} }}`,
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext?.nodeName,
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -113,6 +117,7 @@ export async function datatypeCompletions(
|
|||
try {
|
||||
resolved = await resolveAutocompleteExpression(
|
||||
`={{ ${expressionWithFirstItem(syntaxTree, base)} }}`,
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext?.nodeName,
|
||||
);
|
||||
} catch {
|
||||
|
|
@ -123,7 +128,9 @@ export async function datatypeCompletions(
|
|||
if (resolved === null) return null;
|
||||
|
||||
try {
|
||||
options = (await datatypeOptions({ resolved, base, tail })).map(stripExcessParens(context));
|
||||
options = (await datatypeOptions({ resolved, base, tail }, workflowDocumentId)).map(
|
||||
stripExcessParens(context),
|
||||
);
|
||||
} catch {
|
||||
options = [];
|
||||
}
|
||||
|
|
@ -138,7 +145,11 @@ export async function datatypeCompletions(
|
|||
// When autocomplete is explicitely opened (by Ctrl+Space or programatically), add completions for the current word with '.' prefix
|
||||
// example: {{ $json.str| }} -> ['length', 'includes()'...] (would usually need a '.' suffix)
|
||||
if (context.explicit && !word.text.endsWith('.') && options.length === 0) {
|
||||
options = await explicitDataTypeOptions(word.text, targetNodeParameterContext);
|
||||
options = await explicitDataTypeOptions(
|
||||
word.text,
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext,
|
||||
);
|
||||
from = word.to;
|
||||
}
|
||||
|
||||
|
|
@ -202,25 +213,33 @@ function filterOptions(options: AliasCompletion[], tail: string): AliasCompletio
|
|||
|
||||
async function explicitDataTypeOptions(
|
||||
expression: string,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
targetNodeParameterContext?: TargetNodeParameterContext,
|
||||
): Promise<AliasCompletion[]> {
|
||||
try {
|
||||
const resolved = await resolveAutocompleteExpression(
|
||||
`={{ ${expression} }}`,
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext?.nodeName,
|
||||
);
|
||||
return await datatypeOptions({
|
||||
resolved,
|
||||
base: expression,
|
||||
tail: '',
|
||||
transformLabel: (label) => '.' + label,
|
||||
});
|
||||
return await datatypeOptions(
|
||||
{
|
||||
resolved,
|
||||
base: expression,
|
||||
tail: '',
|
||||
transformLabel: (label) => '.' + label,
|
||||
},
|
||||
workflowDocumentId,
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function datatypeOptions(input: AutocompleteInput): Promise<AliasCompletion[]> {
|
||||
async function datatypeOptions(
|
||||
input: AutocompleteInput,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
): Promise<AliasCompletion[]> {
|
||||
const { resolved } = input;
|
||||
|
||||
if (resolved === null) return [];
|
||||
|
|
@ -256,7 +275,7 @@ async function datatypeOptions(input: AutocompleteInput): Promise<AliasCompletio
|
|||
}
|
||||
|
||||
if (typeof resolved === 'object') {
|
||||
return await objectOptions(input as AutocompleteInput<IDataObject>);
|
||||
return await objectOptions(input as AutocompleteInput<IDataObject>, workflowDocumentId);
|
||||
}
|
||||
|
||||
return [];
|
||||
|
|
@ -401,6 +420,7 @@ const createCompletionOption = ({
|
|||
|
||||
const customObjectOptions = async (
|
||||
input: AutocompleteInput<IDataObject>,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
): Promise<Completion[]> => {
|
||||
const { base, resolved } = input;
|
||||
|
||||
|
|
@ -413,11 +433,11 @@ const customObjectOptions = async (
|
|||
} else if (base === '$workflow') {
|
||||
return workflowOptions();
|
||||
} else if (base === '$input') {
|
||||
return await inputOptions(base);
|
||||
return await inputOptions(base, workflowDocumentId);
|
||||
} else if (base === '$prevNode') {
|
||||
return prevNodeOptions();
|
||||
} else if (/^\$\(['"][\S\s]+['"]\)$/.test(base)) {
|
||||
return await nodeRefOptions(base);
|
||||
return await nodeRefOptions(base, workflowDocumentId);
|
||||
} else if (base === '$response') {
|
||||
return responseOptions();
|
||||
} else if (isItem(input)) {
|
||||
|
|
@ -429,11 +449,14 @@ const customObjectOptions = async (
|
|||
return [];
|
||||
};
|
||||
|
||||
const objectOptions = async (input: AutocompleteInput<IDataObject>): Promise<Completion[]> => {
|
||||
const objectOptions = async (
|
||||
input: AutocompleteInput<IDataObject>,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
): Promise<Completion[]> => {
|
||||
const { base, resolved, transformLabel = (label) => label } = input;
|
||||
const SKIP = new Set(['__ob__', 'pairedItem']);
|
||||
|
||||
if (isSplitInBatchesAbsent()) SKIP.add('context');
|
||||
if (isSplitInBatchesAbsent(workflowDocumentId)) SKIP.add('context');
|
||||
|
||||
let rawKeys = Object.keys(resolved);
|
||||
|
||||
|
|
@ -442,7 +465,7 @@ const objectOptions = async (input: AutocompleteInput<IDataObject>): Promise<Com
|
|||
rawKeys = Object.keys(descriptors).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
const customOptions = await customObjectOptions(input);
|
||||
const customOptions = await customObjectOptions(input, workflowDocumentId);
|
||||
if (customOptions.length > 0) {
|
||||
// Only return completions that are present in the resolved data
|
||||
return customOptions.filter((option) => option.label in resolved);
|
||||
|
|
@ -958,7 +981,7 @@ export const customDataOptions = () => {
|
|||
].map((doc) => createCompletionOption({ name: doc.name, doc, isFunction: true }));
|
||||
};
|
||||
|
||||
export const nodeRefOptions = async (base: string) => {
|
||||
export const nodeRefOptions = async (base: string, workflowDocumentId: WorkflowDocumentId) => {
|
||||
const itemArgs = [
|
||||
{
|
||||
name: 'branchIndex',
|
||||
|
|
@ -1047,7 +1070,7 @@ export const nodeRefOptions = async (base: string) => {
|
|||
},
|
||||
];
|
||||
|
||||
const noParams = await hasNoParams(base);
|
||||
const noParams = await hasNoParams(base, workflowDocumentId);
|
||||
return applySections({
|
||||
options: options
|
||||
.filter((option) => !(option.doc.name === 'params' && noParams))
|
||||
|
|
@ -1057,7 +1080,7 @@ export const nodeRefOptions = async (base: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const inputOptions = async (base: string) => {
|
||||
export const inputOptions = async (base: string, workflowDocumentId: WorkflowDocumentId) => {
|
||||
const itemArgs = [
|
||||
{
|
||||
name: 'branchIndex',
|
||||
|
|
@ -1120,7 +1143,7 @@ export const inputOptions = async (base: string) => {
|
|||
},
|
||||
];
|
||||
|
||||
const noParams = await hasNoParams(base);
|
||||
const noParams = await hasNoParams(base, workflowDocumentId);
|
||||
return applySections({
|
||||
options: options
|
||||
.filter((option) => !(option.doc.name === 'params' && noParams))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
RECOMMENDED_SECTION,
|
||||
ROOT_DOLLAR_COMPLETIONS,
|
||||
TARGET_NODE_PARAMETER_FACET,
|
||||
WORKFLOW_DOCUMENT_FACET,
|
||||
} from './constants';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
|
||||
|
|
@ -121,27 +122,30 @@ export async function dollarOptions(context: CompletionContext): Promise<Complet
|
|||
}
|
||||
|
||||
const targetNodeParameterContext = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||
const workflowDocumentId = context.state.facet(WORKFLOW_DOCUMENT_FACET);
|
||||
|
||||
if (!hasActiveNode(targetNodeParameterContext)) {
|
||||
if (!hasActiveNode(workflowDocumentId, targetNodeParameterContext)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (await receivesNoBinaryData(targetNodeParameterContext?.nodeName)) SKIP.add('$binary');
|
||||
if (await receivesNoBinaryData(workflowDocumentId, targetNodeParameterContext?.nodeName))
|
||||
SKIP.add('$binary');
|
||||
|
||||
const previousNodesCompletions = autocompletableNodeNames(targetNodeParameterContext).map(
|
||||
(nodeName) => {
|
||||
const label = `$('${escapeMappingString(nodeName)}')`;
|
||||
return {
|
||||
label,
|
||||
info: createInfoBoxRenderer({
|
||||
name: label,
|
||||
returnType: 'Object',
|
||||
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
}),
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
};
|
||||
},
|
||||
);
|
||||
const previousNodesCompletions = autocompletableNodeNames(
|
||||
workflowDocumentId,
|
||||
targetNodeParameterContext,
|
||||
).map((nodeName) => {
|
||||
const label = `$('${escapeMappingString(nodeName)}')`;
|
||||
return {
|
||||
label,
|
||||
info: createInfoBoxRenderer({
|
||||
name: label,
|
||||
returnType: 'Object',
|
||||
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
}),
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
};
|
||||
});
|
||||
|
||||
return recommendedCompletions
|
||||
.concat(ROOT_DOLLAR_COMPLETIONS)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { CompletionSource, CompletionResult } from '@codemirror/autocomplet
|
|||
import { CompletionContext } from '@codemirror/autocomplete';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { n8nLang } from '@/features/shared/editors/plugins/codemirror/n8nLang';
|
||||
import { WORKFLOW_DOCUMENT_FACET } from './constants';
|
||||
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
|
@ -28,7 +29,7 @@ export async function completions(docWithCursor: string, explicit = false) {
|
|||
const state = EditorState.create({
|
||||
doc,
|
||||
selection: { anchor: cursorPosition },
|
||||
extensions: [n8nLang()],
|
||||
extensions: [n8nLang(), WORKFLOW_DOCUMENT_FACET.of('test@latest')],
|
||||
});
|
||||
|
||||
const context = new CompletionContext(state, cursorPosition, explicit);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
|||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import type { WorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowHelpers', () => ({
|
||||
useWorkflowHelpers: vi.fn().mockReturnValue({
|
||||
|
|
@ -32,7 +33,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
|
|||
settings: {},
|
||||
getPinDataSnapshot: () => ({}),
|
||||
workflowTriggerNodes: [],
|
||||
},
|
||||
} as Partial<WorkflowDocumentStore> as WorkflowDocumentStore,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/workflowDocument.store', () => ({
|
||||
|
|
@ -113,16 +114,16 @@ describe('completion utils', () => {
|
|||
];
|
||||
|
||||
mockedStore(useWorkflowsStore);
|
||||
mockWorkflowDocumentStore.getChildNodes.mockReturnValue([]);
|
||||
mockWorkflowDocumentStore.getParentNodesByDepth.mockReturnValue([
|
||||
{ name: 'Node 2', depth: 1 },
|
||||
{ name: 'Node 1', depth: 2 },
|
||||
vi.mocked(mockWorkflowDocumentStore.getChildNodes).mockReturnValue([]);
|
||||
vi.mocked(mockWorkflowDocumentStore.getParentNodesByDepth).mockReturnValue([
|
||||
{ name: 'Node 2', depth: 1, indicies: [] },
|
||||
{ name: 'Node 1', depth: 2, indicies: [] },
|
||||
]);
|
||||
|
||||
const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore');
|
||||
ndvStoreMock.mockReturnValue({ activeNode: nodes[2] });
|
||||
|
||||
expect(autocompletableNodeNames()).toEqual(['Node 2', 'Node 1']);
|
||||
expect(autocompletableNodeNames(mockWorkflowDocumentStore)).toEqual(['Node 2', 'Node 1']);
|
||||
});
|
||||
|
||||
it('should work for AI tool nodes', () => {
|
||||
|
|
@ -133,15 +134,15 @@ describe('completion utils', () => {
|
|||
];
|
||||
|
||||
mockedStore(useWorkflowsStore);
|
||||
mockWorkflowDocumentStore.getChildNodes.mockReturnValue(['Agent']);
|
||||
mockWorkflowDocumentStore.getParentNodesByDepth.mockReturnValue([
|
||||
{ name: 'Normal Node', depth: 1 },
|
||||
vi.mocked(mockWorkflowDocumentStore.getChildNodes).mockReturnValue(['Agent']);
|
||||
vi.mocked(mockWorkflowDocumentStore.getParentNodesByDepth).mockReturnValue([
|
||||
{ name: 'Normal Node', depth: 1, indicies: [] },
|
||||
]);
|
||||
|
||||
const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore');
|
||||
ndvStoreMock.mockReturnValue({ activeNode: nodes[2] });
|
||||
|
||||
expect(autocompletableNodeNames()).toEqual(['Normal Node']);
|
||||
expect(autocompletableNodeNames(mockWorkflowDocumentStore)).toEqual(['Normal Node']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { HTTP_REQUEST_NODE_TYPE, SPLIT_IN_BATCHES_NODE_TYPE } from '@/app/constants';
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY } from '@/features/credentials/credentials.constants';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { resolveParameter } from '@/app/composables/useWorkflowHelpers';
|
||||
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
|
|
@ -18,8 +17,8 @@ import type { DocMetadata } from 'n8n-workflow';
|
|||
import { escapeMappingString } from '@/app/utils/mappingUtils';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
type WorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
/**
|
||||
|
|
@ -167,21 +166,33 @@ export const isValidJavascriptIdentifier = (str: string) => {
|
|||
// resolution-based utils
|
||||
// ----------------------------------
|
||||
|
||||
export async function receivesNoBinaryData(contextNodeName?: string) {
|
||||
export async function receivesNoBinaryData(
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
contextNodeName?: string,
|
||||
) {
|
||||
try {
|
||||
return (
|
||||
(await resolveAutocompleteExpression('={{ $binary }}', contextNodeName))?.data === undefined
|
||||
(await resolveAutocompleteExpression('={{ $binary }}', workflowDocumentId, contextNodeName))
|
||||
?.data === undefined
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasNoParams(toResolve: string, contextNodeName?: string) {
|
||||
export async function hasNoParams(
|
||||
toResolve: string,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
contextNodeName?: string,
|
||||
) {
|
||||
let params;
|
||||
|
||||
try {
|
||||
params = await resolveAutocompleteExpression(`={{ ${toResolve}.params }}`, contextNodeName);
|
||||
params = await resolveAutocompleteExpression(
|
||||
`={{ ${toResolve}.params }}`,
|
||||
workflowDocumentId,
|
||||
contextNodeName,
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -193,7 +204,11 @@ export async function hasNoParams(toResolve: string, contextNodeName?: string) {
|
|||
return paramKeys.length === 1 && isPseudoParam(paramKeys[0]);
|
||||
}
|
||||
|
||||
export async function resolveAutocompleteExpression(expression: string, contextNodeName?: string) {
|
||||
export async function resolveAutocompleteExpression(
|
||||
expression: string,
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
contextNodeName?: string,
|
||||
) {
|
||||
const ndvStore = useNDVStore();
|
||||
const inputData =
|
||||
contextNodeName === undefined && ndvStore.isInputParentOfActiveNode
|
||||
|
|
@ -204,7 +219,7 @@ export async function resolveAutocompleteExpression(expression: string, contextN
|
|||
inputBranchIndex: ndvStore.ndvInputBranchIndex,
|
||||
}
|
||||
: {};
|
||||
return await resolveParameter(expression, {
|
||||
return await resolveParameter(expression, workflowDocumentId, {
|
||||
...inputData,
|
||||
contextNodeName,
|
||||
});
|
||||
|
|
@ -231,7 +246,10 @@ export const isInHttpNodePagination = (targetNodeParameterContext?: TargetNodePa
|
|||
return nodeType === HTTP_REQUEST_NODE_TYPE && path.startsWith('parameters.options.pagination');
|
||||
};
|
||||
|
||||
export const hasActiveNode = (targetNodeParameterContext?: TargetNodeParameterContext) => {
|
||||
export const hasActiveNode = (
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
targetNodeParameterContext?: TargetNodeParameterContext,
|
||||
) => {
|
||||
if (useNDVStore().activeNode?.name !== undefined) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -240,28 +258,20 @@ export const hasActiveNode = (targetNodeParameterContext?: TargetNodeParameterCo
|
|||
return false;
|
||||
}
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
return workflowDocumentStore.getNodeByName(targetNodeParameterContext.nodeName) !== null;
|
||||
};
|
||||
|
||||
export const isSplitInBatchesAbsent = () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
|
||||
export const isSplitInBatchesAbsent = (workflowDocumentId: WorkflowDocumentId) => {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
return !workflowDocumentStore.allNodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
||||
};
|
||||
|
||||
export function autocompletableNodeNames(targetNodeParameterContext?: TargetNodeParameterContext) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
export function autocompletableNodeNames(
|
||||
workflowDocumentId: WorkflowDocumentId,
|
||||
targetNodeParameterContext?: TargetNodeParameterContext,
|
||||
) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
const activeNode =
|
||||
targetNodeParameterContext === undefined
|
||||
? useNDVStore().activeNode
|
||||
|
|
@ -275,17 +285,14 @@ export function autocompletableNodeNames(targetNodeParameterContext?: TargetNode
|
|||
|
||||
// This is a tool node, look for the nearest node with main connections
|
||||
if (nonMainChildren.length > 0) {
|
||||
return nonMainChildren.map(getPreviousNodes).flat();
|
||||
return nonMainChildren.map((child) => getPreviousNodes(workflowDocumentId, child)).flat();
|
||||
}
|
||||
|
||||
return getPreviousNodes(activeNodeName);
|
||||
return getPreviousNodes(workflowDocumentId, activeNodeName);
|
||||
}
|
||||
|
||||
export function getPreviousNodes(nodeName: string) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
export function getPreviousNodes(workflowDocumentId: WorkflowDocumentId, nodeName: string) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
|
||||
return workflowDocumentStore
|
||||
.getParentNodesByDepth(nodeName)
|
||||
.map((node) => node.name)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { hoverTooltipSource, infoBoxTooltips } from './InfoBoxTooltip';
|
|||
import * as utils from '@/features/shared/editors/plugins/codemirror/completions/utils';
|
||||
import * as workflowHelpers from '@/app/composables/useWorkflowHelpers';
|
||||
import { completionStatus } from '@codemirror/autocomplete';
|
||||
import { WORKFLOW_DOCUMENT_FACET } from '@/features/shared/editors/plugins/codemirror/completions/constants';
|
||||
|
||||
vi.mock('@codemirror/autocomplete', async (importOriginal) => {
|
||||
const actual = await importOriginal<{}>();
|
||||
|
|
@ -162,7 +163,7 @@ async function hoverTooltip(docWithCursor: string) {
|
|||
|
||||
const state = EditorState.create({
|
||||
doc,
|
||||
extensions: [n8nLang(), infoBoxTooltips()],
|
||||
extensions: [n8nLang(), infoBoxTooltips(), WORKFLOW_DOCUMENT_FACET.of('test@latest')],
|
||||
});
|
||||
|
||||
const view = new EditorView({ state, parent: document.createElement('div') });
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { autocompletableNodeNames, longestCommonPrefix } from '../../completions/utils';
|
||||
import { typescriptWorkerFacet } from './facet';
|
||||
import { blockCommentSnippet, snippets } from './snippets';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants';
|
||||
import { TARGET_NODE_PARAMETER_FACET, WORKFLOW_DOCUMENT_FACET } from '../../completions/constants';
|
||||
import type { AliasCompletion, Alias } from 'n8n-workflow';
|
||||
import { sortCompletionsByInput } from '../../completions/datatype.completions';
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ export const matchText = (context: CompletionContext) => {
|
|||
export const typescriptCompletionSource: CompletionSource = async (context) => {
|
||||
const { worker } = context.state.facet(typescriptWorkerFacet);
|
||||
const targetNodeParameter = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||
const workflowDocumentId = context.state.facet(WORKFLOW_DOCUMENT_FACET);
|
||||
const word = matchText(context);
|
||||
|
||||
const blockComment = context.matchBefore(/\/\*?\*?/);
|
||||
|
|
@ -96,7 +97,7 @@ export const typescriptCompletionSource: CompletionSource = async (context) => {
|
|||
if (opt.label === '$()') {
|
||||
return [
|
||||
opt,
|
||||
...autocompletableNodeNames(targetNodeParameter).map((name) => ({
|
||||
...autocompletableNodeNames(workflowDocumentId, targetNodeParameter).map((name) => ({
|
||||
...opt,
|
||||
label: `$('${escapeMappingString(name)}')`,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { typescriptHoverTooltips } from './hoverTooltip';
|
|||
import { linter } from '@codemirror/lint';
|
||||
import { typescriptLintSource } from './linter';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants';
|
||||
import { TARGET_NODE_PARAMETER_FACET, WORKFLOW_DOCUMENT_FACET } from '../../completions/constants';
|
||||
|
||||
export function useTypescript(
|
||||
view: MaybeRefOrGetter<EditorView | undefined>,
|
||||
|
|
@ -49,7 +49,10 @@ export function useTypescript(
|
|||
{
|
||||
id: toValue(id),
|
||||
content: Comlink.proxy((toValue(view)?.state.doc ?? Text.empty).toJSON()),
|
||||
allNodeNames: autocompletableNodeNames(toValue(targetNodeParameterContext)),
|
||||
allNodeNames: autocompletableNodeNames(
|
||||
workflowDocumentStore.value.documentId,
|
||||
toValue(targetNodeParameterContext),
|
||||
),
|
||||
variables: useEnvironmentsStore().scopedVariables.map((v) => v.key),
|
||||
inputNodeNames: activeNodeName
|
||||
? (workflowDocumentStore?.value?.getParentNodes(
|
||||
|
|
@ -99,6 +102,7 @@ export function useTypescript(
|
|||
return [
|
||||
typescriptWorkerFacet.of({ worker: worker.value }),
|
||||
TARGET_NODE_PARAMETER_FACET.of(toValue(targetNodeParameterContext)),
|
||||
WORKFLOW_DOCUMENT_FACET.of(workflowDocumentStore.value.documentId),
|
||||
new LanguageSupport(javascriptLanguage, [
|
||||
javascriptLanguage.data.of({ autocomplete: typescriptCompletionSource }),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -24,21 +24,19 @@ import { collectParametersByTab, createCommonNodeSettings } from '@/features/ndv
|
|||
import type { INodeUpdatePropertiesInformation, ITab, IUpdateInformation } from '@/Interface';
|
||||
import { N8nTabs, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import {
|
||||
Workflow,
|
||||
NodeHelpers,
|
||||
deepCopy,
|
||||
type INode,
|
||||
type INodeParameters,
|
||||
type INodeTypes,
|
||||
type INodeType,
|
||||
type IVersionedNodeType,
|
||||
type IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, NodeHelpers, deepCopy, type INode, type INodeParameters } from 'n8n-workflow';
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from 'vue';
|
||||
import { ChatHubToolContextKey, ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
import {
|
||||
ChatHubToolContextKey,
|
||||
ExpressionLocalResolveContextSymbol,
|
||||
WorkflowDocumentStoreKey,
|
||||
} from '@/app/constants';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
const props = defineProps<{
|
||||
initialNode: INode;
|
||||
|
|
@ -139,54 +137,24 @@ const hasCredentialIssues = computed(() => {
|
|||
return Object.keys(credentialIssues?.credentials ?? {}).length > 0;
|
||||
});
|
||||
|
||||
const workflowDocumentStore = computed(() => {
|
||||
const store = useWorkflowDocumentStore(createWorkflowDocumentId('node-tool-workflow'));
|
||||
|
||||
if (node.value) {
|
||||
store.setNodes([node.value]);
|
||||
}
|
||||
|
||||
return store;
|
||||
});
|
||||
|
||||
const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||
if (!node.value) return undefined;
|
||||
|
||||
const nodeTypes: INodeTypes = {
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
const description = nodeTypesStore.getNodeType(nodeType);
|
||||
if (description === null) {
|
||||
throw new Error(`Node type "${nodeType}" not found`);
|
||||
}
|
||||
|
||||
return {
|
||||
description,
|
||||
} as INodeType;
|
||||
},
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
const description = nodeTypesStore.getNodeType(nodeType, version);
|
||||
if (description === null) {
|
||||
throw new Error(`Node type "${nodeType}" (v${version}) not found`);
|
||||
}
|
||||
|
||||
return {
|
||||
description,
|
||||
} as INodeType;
|
||||
},
|
||||
getKnownTypes(): IDataObject {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
// Minimal workflow containing only this node for parameter resolution
|
||||
const workflow = new Workflow({
|
||||
id: 'node-tool-workflow',
|
||||
name: 'Tool Configuration',
|
||||
nodes: [node.value],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
settings: {},
|
||||
});
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: environmentsStore.variablesAsObject,
|
||||
workflow,
|
||||
execution: null,
|
||||
nodeName: node.value.name,
|
||||
additionalKeys: {},
|
||||
connections: {},
|
||||
inputNode: undefined,
|
||||
};
|
||||
});
|
||||
|
|
@ -197,6 +165,7 @@ const isValid = computed(() => {
|
|||
|
||||
// Provide expression resolve context for dynamic parameter loading
|
||||
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
||||
provide(WorkflowDocumentStoreKey, workflowDocumentStore);
|
||||
provide(ChatHubToolContextKey, true);
|
||||
|
||||
function makeUniqueName(baseName: string, existingNames: string[]): string {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, waitFor } from '@testing-library/vue';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import { computed, nextTick } from 'vue';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import ExperimentalEmbeddedNdvMapper from './ExperimentalEmbeddedNdvMapper.vue';
|
||||
|
|
@ -11,14 +10,6 @@ import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
|||
|
||||
describe('ExperimentalEmbeddedNdvMapper', () => {
|
||||
const node = createTestNode({ name: 'n1' });
|
||||
const workflow = createTestWorkflowObject({
|
||||
nodes: [node],
|
||||
connections: {
|
||||
n0: {
|
||||
[NodeConnectionTypes.Main]: [[{ index: 0, node: 'n0', type: NodeConnectionTypes.Main }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('should open the popover on hover if visibleOnHover is true', async () => {
|
||||
const reference = document.createElement('div');
|
||||
|
|
@ -28,7 +19,6 @@ describe('ExperimentalEmbeddedNdvMapper', () => {
|
|||
plugins: [createTestingPinia({ stubActions: false })],
|
||||
},
|
||||
props: {
|
||||
workflow,
|
||||
node,
|
||||
inputNodeName: 'n0',
|
||||
reference,
|
||||
|
|
@ -49,7 +39,6 @@ describe('ExperimentalEmbeddedNdvMapper', () => {
|
|||
plugins: [createTestingPinia({ stubActions: false })],
|
||||
},
|
||||
props: {
|
||||
workflow,
|
||||
node,
|
||||
inputNodeName: 'n0',
|
||||
reference,
|
||||
|
|
@ -72,7 +61,6 @@ describe('ExperimentalEmbeddedNdvMapper', () => {
|
|||
plugins: [pinia],
|
||||
},
|
||||
props: {
|
||||
workflow,
|
||||
node,
|
||||
inputNodeName: 'n0',
|
||||
reference,
|
||||
|
|
@ -95,7 +83,6 @@ describe('ExperimentalEmbeddedNdvMapper', () => {
|
|||
plugins: [createTestingPinia({ stubActions: false })],
|
||||
},
|
||||
props: {
|
||||
workflow,
|
||||
node,
|
||||
inputNodeName: 'n0',
|
||||
reference,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
|||
import { isEventTargetContainedBy } from '@/app/utils/htmlUtils';
|
||||
|
||||
import { N8nPopover } from '@n8n/design-system';
|
||||
import type { WorkflowObjectAccessors } from '@/app/types';
|
||||
type MapperState = { isOpen: true; closeOnMouseLeave: boolean } | { isOpen: false };
|
||||
|
||||
const hoverOptions: UseElementHoverOptions = {
|
||||
|
|
@ -27,7 +26,6 @@ const {
|
|||
reference,
|
||||
visibleOnHover = false,
|
||||
} = defineProps<{
|
||||
workflow: WorkflowObjectAccessors;
|
||||
node: INodeUi;
|
||||
inputNodeName: string;
|
||||
visibleOnHover?: boolean;
|
||||
|
|
@ -115,7 +113,6 @@ onClickOutside(contentElRef, handleReferenceFocusOut);
|
|||
ref="content"
|
||||
:tabindex="-1"
|
||||
:class="$style.inputPanel"
|
||||
:workflow-object="workflow"
|
||||
:run-index="0"
|
||||
compact
|
||||
push-ref=""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import type { INodeUi } from '@/Interface';
|
||||
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
|
||||
export function useExpressionResolveCtx(node: ComputedRef<INodeUi | null | undefined>) {
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
|
@ -47,13 +45,9 @@ export function useExpressionResolveCtx(node: ComputedRef<INodeUi | null | undef
|
|||
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: environmentsStore.variablesAsObject,
|
||||
workflow: workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
|
||||
execution,
|
||||
nodeName,
|
||||
additionalKeys: {},
|
||||
inputNode: findInputNode(),
|
||||
connections: workflowDocumentStore.value.connectionsBySourceNode,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user