fix(editor): Make expression resolution work in embedded ndv (no-changelog) (#17221)

This commit is contained in:
Suguru Inoue
2025-07-14 12:34:40 +02:00
committed by GitHub
parent d002cc3f7d
commit 075efba2bb
7 changed files with 338 additions and 38 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue'; import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
import { onBeforeUnmount, ref, computed } from 'vue'; import { onBeforeUnmount, ref, computed, provide } from 'vue';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useExperimentalNdvStore } from '../experimentalNdv.store'; import { useExperimentalNdvStore } from '../experimentalNdv.store';
@@ -8,6 +8,9 @@ import NodeTitle from '@/components/NodeTitle.vue';
import { N8nIcon, N8nIconButton } from '@n8n/design-system'; import { N8nIcon, N8nIconButton } from '@n8n/design-system';
import { useVueFlow } from '@vue-flow/core'; import { useVueFlow } from '@vue-flow/core';
import { watchOnce } from '@vueuse/core'; import { watchOnce } from '@vueuse/core';
import { ExpressionLocalResolveContextSymbol } from '@/constants';
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import type { ExpressionLocalResolveContext } from '@/types/expressions';
const { nodeId, isReadOnly, isConfigurable } = defineProps<{ const { nodeId, isReadOnly, isConfigurable } = defineProps<{
nodeId: string; nodeId: string;
@@ -56,6 +59,55 @@ const isVisible = computed(() =>
); );
const isOnceVisible = ref(isVisible.value); const isOnceVisible = ref(isVisible.value);
provide(
ExpressionLocalResolveContextSymbol,
computed<ExpressionLocalResolveContext | undefined>(() => {
if (!node.value) {
return undefined;
}
const workflow = workflowsStore.getCurrentWorkflow();
const runIndex = 0; // not changeable for now
const execution = workflowsStore.workflowExecutionData;
const nodeName = node.value.name;
function findInputNode(): ExpressionLocalResolveContext['inputNode'] {
const taskData = (execution?.data?.resultData.runData[nodeName] ?? [])[runIndex];
const source = taskData?.source[0];
if (source) {
return {
name: source.previousNode,
branchIndex: source.previousNodeOutput ?? 0,
runIndex: source.previousNodeRun ?? 0,
};
}
const inputs = workflow.getParentNodesByDepth(nodeName, 1);
if (inputs.length > 0) {
return {
name: inputs[0].name,
branchIndex: inputs[0].indicies[0] ?? 0,
runIndex: 0,
};
}
return undefined;
}
return {
localResolve: true,
envVars: useEnvironmentsStore().variablesAsObject,
workflow,
execution,
nodeName,
additionalKeys: {},
inputNode: findInputNode(),
};
}),
);
watchOnce(isVisible, (visible) => { watchOnce(isVisible, (visible) => {
isOnceVisible.value = isOnceVisible.value || visible; isOnceVisible.value = isOnceVisible.value || visible;
}); });

View File

@@ -1,5 +1,6 @@
import { import {
computed, computed,
inject,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
ref, ref,
@@ -15,7 +16,7 @@ import { ensureSyntaxTree } from '@codemirror/language';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { Expression, ExpressionExtensions } from 'n8n-workflow'; import { Expression, ExpressionExtensions } from 'n8n-workflow';
import { EXPRESSION_EDITOR_PARSER_TIMEOUT } from '@/constants'; import { EXPRESSION_EDITOR_PARSER_TIMEOUT, ExpressionLocalResolveContextSymbol } from '@/constants';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import type { TargetItem, TargetNodeParameterContext } from '@/Interface'; import type { TargetItem, TargetNodeParameterContext } from '@/Interface';
@@ -75,6 +76,10 @@ export const useExpressionEditor = ({
const autocompleteStatus = ref<'pending' | 'active' | null>(null); const autocompleteStatus = ref<'pending' | 'active' | null>(null);
const dragging = ref(false); const dragging = ref(false);
const hasChanges = ref(false); const hasChanges = ref(false);
const expressionLocalResolveContext = inject(
ExpressionLocalResolveContextSymbol,
computed(() => undefined),
);
const emitChanges = debounce(onChange, 300); const emitChanges = debounce(onChange, 300);
@@ -307,7 +312,12 @@ export const useExpressionEditor = ({
}; };
try { try {
if (!ndvStore.activeNode && toValue(targetNodeParameterContext) === undefined) { if (expressionLocalResolveContext.value) {
result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, {
...expressionLocalResolveContext.value,
additionalKeys: toValue(additionalData),
});
} else if (!ndvStore.activeNode && toValue(targetNodeParameterContext) === undefined) {
// e.g. credential modal // e.g. credential modal
result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData)); result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData));
} else { } else {

View File

@@ -4,8 +4,19 @@ import { isExpression as isExpressionUtil, stringifyExpressionResult } from '@/u
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { createResultError, createResultOk, type IDataObject, type Result } from 'n8n-workflow'; import { createResultError, createResultOk, type IDataObject, type Result } from 'n8n-workflow';
import { computed, onMounted, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue'; import {
computed,
onMounted,
ref,
toRef,
toValue,
inject,
type MaybeRefOrGetter,
watch,
} from 'vue';
import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers'; import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers';
import { ExpressionLocalResolveContextSymbol } from '@/constants';
import type { ExpressionLocalResolveContext } from '@/types/expressions';
export function useResolvedExpression({ export function useResolvedExpression({
expression, expression,
@@ -25,6 +36,11 @@ export function useResolvedExpression({
const { resolveExpression } = useWorkflowHelpers(); const { resolveExpression } = useWorkflowHelpers();
const expressionLocalResolveCtx = inject(
ExpressionLocalResolveContextSymbol,
computed(() => undefined),
);
const resolvedExpression = ref<unknown>(null); const resolvedExpression = ref<unknown>(null);
const resolvedExpressionString = ref(''); const resolvedExpressionString = ref('');
@@ -37,29 +53,27 @@ export function useResolvedExpression({
); );
const isExpression = computed(() => isExpressionUtil(toValue(expression))); const isExpression = computed(() => isExpressionUtil(toValue(expression)));
function resolve(): Result<unknown, Error> { function resolve(ctx?: ExpressionLocalResolveContext): Result<unknown, Error> {
const expressionString = toValue(expression); const expressionString = toValue(expression);
if (!isExpression.value || typeof expressionString !== 'string') { if (!isExpression.value || typeof expressionString !== 'string') {
return { ok: true, result: '' }; return { ok: true, result: '' };
} }
let options: ResolveParameterOptions = { const options: ResolveParameterOptions | ExpressionLocalResolveContext = ctx ?? {
isForCredential: toValue(isForCredential), isForCredential: toValue(isForCredential),
additionalKeys: toValue(additionalData), additionalKeys: toValue(additionalData),
contextNodeName: toValue(contextNodeName), contextNodeName: toValue(contextNodeName),
...(contextNodeName === undefined && ndvStore.isInputParentOfActiveNode
? {
targetItem: targetItem.value ?? undefined,
inputNodeName: ndvStore.ndvInputNodeName,
inputRunIndex: ndvStore.ndvInputRunIndex,
inputBranchIndex: ndvStore.ndvInputBranchIndex,
}
: {}),
}; };
if (contextNodeName === undefined && ndvStore.isInputParentOfActiveNode) {
options = {
...options,
targetItem: targetItem.value ?? undefined,
inputNodeName: ndvStore.ndvInputNodeName,
inputRunIndex: ndvStore.ndvInputRunIndex,
inputBranchIndex: ndvStore.ndvInputBranchIndex,
};
}
try { try {
const resolvedValue = resolveExpression( const resolvedValue = resolveExpression(
expressionString, expressionString,
@@ -78,7 +92,7 @@ export function useResolvedExpression({
function updateExpression() { function updateExpression() {
if (isExpression.value) { if (isExpression.value) {
const resolved = resolve(); const resolved = resolve(expressionLocalResolveCtx.value);
resolvedExpression.value = resolved.ok ? resolved.result : null; resolvedExpression.value = resolved.ok ? resolved.result : null;
resolvedExpressionString.value = stringifyExpressionResult(resolved, hasRunData.value); resolvedExpressionString.value = stringifyExpressionResult(resolved, hasRunData.value);
} else { } else {
@@ -89,6 +103,7 @@ export function useResolvedExpression({
watch( watch(
[ [
expressionLocalResolveCtx,
toRef(expression), toRef(expression),
() => workflowsStore.getWorkflowExecution, () => workflowsStore.getWorkflowExecution,
() => workflowsStore.getWorkflowRunData, () => workflowsStore.getWorkflowRunData,

View File

@@ -1,14 +1,24 @@
import type { IExecutionResponse, IWorkflowDb } from '@/Interface'; import type { IExecutionResponse, IWorkflowDb } from '@/Interface';
import type { WorkflowData } from '@n8n/rest-api-client/api/workflows'; import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { createTestWorkflow } from '@/__tests__/mocks'; import {
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow'; createTestNode,
createTestTaskData,
createTestWorkflow,
createTestWorkflowExecutionResponse,
createTestWorkflowObject,
} from '@/__tests__/mocks';
import {
NodeConnectionTypes,
WEBHOOK_NODE_TYPE,
type AssignmentCollectionValue,
} from 'n8n-workflow';
import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks'; import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks';
import { mockedStore } from '@/__tests__/utils'; import { mockedStore } from '@/__tests__/utils';
@@ -871,3 +881,79 @@ describe('useWorkflowHelpers', () => {
}); });
}); });
}); });
describe(resolveParameter, () => {
describe('with local resolve context', () => {
it('should resolve parameter without execution data', () => {
const result = resolveParameter(
{
f0: '={{ 2 + 2 }}',
f1: '={{ $vars.foo }}',
f2: '={{ String($exotic).toUpperCase() }}',
},
{
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', () => {
const workflowData = createTestWorkflow({
nodes: [createTestNode({ name: 'n0' }), createTestNode({ name: 'n1' })],
connections: {
n0: {
[NodeConnectionTypes.Main]: [
[{ type: NodeConnectionTypes.Main, index: 0, node: 'n1' }],
],
},
},
});
const result = resolveParameter(
{
f0: '={{ $json }}',
f1: '={{ $("n0").item.json }}',
},
{
localResolve: true,
envVars: {},
additionalKeys: {},
workflow: createTestWorkflowObject(workflowData),
execution: createTestWorkflowExecutionResponse({
workflowData,
data: {
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 },
});
});
});
});

View File

@@ -15,6 +15,8 @@ import type {
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
INodeTypes, INodeTypes,
IPinData,
IRunData,
IRunExecutionData, IRunExecutionData,
IWebhookDescription, IWebhookDescription,
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
@@ -30,6 +32,7 @@ import {
import type { import type {
ICredentialsResponse, ICredentialsResponse,
IExecutionResponse,
INodeTypesMaxCount, INodeTypesMaxCount,
INodeUi, INodeUi,
IWorkflowDb, IWorkflowDb,
@@ -58,6 +61,7 @@ import { useProjectsStore } from '@/stores/projects.store';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { findWebhook } from '@n8n/rest-api-client/api/webhooks'; import { findWebhook } from '@n8n/rest-api-client/api/webhooks';
import type { ExpressionLocalResolveContext } from '@/types/expressions';
export type ResolveParameterOptions = { export type ResolveParameterOptions = {
targetItem?: TargetItem; targetItem?: TargetItem;
@@ -71,11 +75,54 @@ export type ResolveParameterOptions = {
export function resolveParameter<T = IDataObject>( export function resolveParameter<T = IDataObject>(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
opts: ResolveParameterOptions | ExpressionLocalResolveContext = {},
): T | null {
if ('localResolve' in opts && opts.localResolve) {
return resolveParameterImpl(
parameter,
() => opts.workflow,
opts.envVars,
opts.workflow.getNode(opts.nodeName),
opts.execution,
true,
opts.workflow.pinData,
{
inputNodeName: opts.inputNode?.name,
inputRunIndex: opts.inputNode?.runIndex,
inputBranchIndex: opts.inputNode?.branchIndex,
additionalKeys: opts.additionalKeys,
},
);
}
const workflowsStore = useWorkflowsStore();
return resolveParameterImpl(
parameter,
workflowsStore.getCurrentWorkflow,
useEnvironmentsStore().variablesAsObject,
useNDVStore().activeNode,
workflowsStore.workflowExecutionData,
workflowsStore.shouldReplaceInputDataWithPinData,
workflowsStore.pinnedWorkflowData,
opts,
);
}
// TODO: move to separate file
function resolveParameterImpl<T = IDataObject>(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
getContextWorkflow: () => Workflow,
envVars: Record<string, string | boolean | number>,
ndvActiveNode: INodeUi | null,
executionData: IExecutionResponse | null,
shouldReplaceInputDataWithPinData: boolean,
pinData: IPinData | undefined,
opts: ResolveParameterOptions = {}, opts: ResolveParameterOptions = {},
): T | null { ): T | null {
let itemIndex = opts?.targetItem?.itemIndex || 0; let itemIndex = opts?.targetItem?.itemIndex || 0;
const workflow = getCurrentWorkflow(); const workflow = getContextWorkflow();
const additionalKeys: IWorkflowDataProxyAdditionalKeys = { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$execution: { $execution: {
@@ -84,7 +131,7 @@ export function resolveParameter<T = IDataObject>(
resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
resumeFormUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, resumeFormUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
}, },
$vars: useEnvironmentsStore().variablesAsObject, $vars: envVars,
// deprecated // deprecated
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
@@ -113,17 +160,15 @@ export function resolveParameter<T = IDataObject>(
const inputName = NodeConnectionTypes.Main; const inputName = NodeConnectionTypes.Main;
const activeNode = const activeNode = ndvActiveNode ?? workflow.getNode(opts.contextNodeName || '');
useNDVStore().activeNode ?? useWorkflowsStore().getNodeByName(opts.contextNodeName || '');
let contextNode = activeNode; let contextNode = activeNode;
if (activeNode) { if (activeNode) {
contextNode = workflow.getParentMainInputNode(activeNode); contextNode = workflow.getParentMainInputNode(activeNode);
} }
const workflowRunData = useWorkflowsStore().getWorkflowRunData; const workflowRunData = executionData?.data?.resultData.runData ?? null;
let parentNode = workflow.getParentNodes(contextNode!.name, inputName, 1); let parentNode = workflow.getParentNodes(contextNode!.name, inputName, 1);
const executionData = useWorkflowsStore().getWorkflowExecution;
let runIndexParent = opts?.inputRunIndex ?? 0; let runIndexParent = opts?.inputRunIndex ?? 0;
const nodeConnection = workflow.getNodeConnectionIndexes(contextNode!.name, parentNode[0]); const nodeConnection = workflow.getNodeConnectionIndexes(contextNode!.name, parentNode[0]);
@@ -159,13 +204,26 @@ export function resolveParameter<T = IDataObject>(
contextNode!.name, contextNode!.name,
inputName, inputName,
runIndexParent, runIndexParent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
nodeConnection, nodeConnection,
); );
if (_connectionInputData === null && contextNode && activeNode?.name !== contextNode.name) { if (_connectionInputData === null && contextNode && activeNode?.name !== contextNode.name) {
// For Sub-Nodes connected to Trigger-Nodes use the data of the root-node // For Sub-Nodes connected to Trigger-Nodes use the data of the root-node
// (Gets for example used by the Memory connected to the Chat-Trigger-Node) // (Gets for example used by the Memory connected to the Chat-Trigger-Node)
const _executeData = executeData([contextNode.name], contextNode.name, inputName, 0); const _executeData = executeDataImpl(
[contextNode.name],
contextNode.name,
inputName,
0,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
);
_connectionInputData = get(_executeData, ['data', inputName, 0], null); _connectionInputData = get(_executeData, ['data', inputName, 0], null);
} }
@@ -206,17 +264,30 @@ export function resolveParameter<T = IDataObject>(
) { ) {
runIndexCurrent = workflowRunData[contextNode!.name].length - 1; runIndexCurrent = workflowRunData[contextNode!.name].length - 1;
} }
let _executeData = executeData( let _executeData = executeDataImpl(
parentNode, parentNode,
contextNode!.name, contextNode!.name,
inputName, inputName,
runIndexCurrent, runIndexCurrent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
runIndexParent, runIndexParent,
); );
if (!_executeData.source) { if (!_executeData.source) {
// fallback to parent's run index for multi-output case // fallback to parent's run index for multi-output case
_executeData = executeData(parentNode, contextNode!.name, inputName, runIndexParent); _executeData = executeDataImpl(
parentNode,
contextNode!.name,
inputName,
runIndexParent,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
executionData?.data?.resultData.runData ?? null,
);
} }
return workflow.expression.getParameterValue( return workflow.expression.getParameterValue(
@@ -308,16 +379,30 @@ function getNodeTypes(): INodeTypes {
return useWorkflowsStore().getNodeTypes(); return useWorkflowsStore().getNodeTypes();
} }
// TODO: move to separate file
// Returns connectionInputData to be able to execute an expression. // Returns connectionInputData to be able to execute an expression.
function connectionInputData( function connectionInputData(
parentNode: string[], parentNode: string[],
currentNode: string, currentNode: string,
inputName: string, inputName: string,
runIndex: number, runIndex: number,
getContextWorkflow: () => Workflow,
shouldReplaceInputDataWithPinData: boolean,
pinData: IPinData | undefined,
workflowRunData: IRunData | null,
nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 }, nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 },
): INodeExecutionData[] | null { ): INodeExecutionData[] | null {
let connectionInputData: INodeExecutionData[] | null = null; let connectionInputData: INodeExecutionData[] | null = null;
const _executeData = executeData(parentNode, currentNode, inputName, runIndex); const _executeData = executeDataImpl(
parentNode,
currentNode,
inputName,
runIndex,
getContextWorkflow,
shouldReplaceInputDataWithPinData,
pinData,
workflowRunData,
);
if (parentNode.length) { if (parentNode.length) {
if ( if (
!Object.keys(_executeData.data).length || !Object.keys(_executeData.data).length ||
@@ -351,6 +436,33 @@ export function executeData(
inputName: string, inputName: string,
runIndex: number, runIndex: number,
parentRunIndex?: number, parentRunIndex?: number,
): IExecuteData {
const workflowsStore = useWorkflowsStore();
return executeDataImpl(
parentNodes,
currentNode,
inputName,
runIndex,
workflowsStore.getCurrentWorkflow,
workflowsStore.shouldReplaceInputDataWithPinData,
workflowsStore.pinnedWorkflowData,
workflowsStore.getWorkflowRunData,
parentRunIndex,
);
}
// TODO: move to separate file
function executeDataImpl(
parentNodes: string[],
currentNode: string,
inputName: string,
runIndex: number,
getContextWorkflow: () => Workflow,
shouldReplaceInputDataWithPinData: boolean,
pinData: IPinData | undefined,
workflowRunData: IRunData | null,
parentRunIndex?: number,
): IExecuteData { ): IExecuteData {
const executeData = { const executeData = {
node: {}, node: {},
@@ -360,12 +472,10 @@ export function executeData(
parentRunIndex = parentRunIndex ?? runIndex; parentRunIndex = parentRunIndex ?? runIndex;
const workflowsStore = useWorkflowsStore();
// Find the parent node which has data // Find the parent node which has data
for (const parentNodeName of parentNodes) { for (const parentNodeName of parentNodes) {
if (workflowsStore.shouldReplaceInputDataWithPinData) { if (shouldReplaceInputDataWithPinData) {
const parentPinData = workflowsStore.pinnedWorkflowData![parentNodeName]; const parentPinData = pinData?.[parentNodeName];
// populate `executeData` from `pinData` // populate `executeData` from `pinData`
@@ -378,7 +488,6 @@ export function executeData(
} }
// populate `executeData` from `runData` // populate `executeData` from `runData`
const workflowRunData = workflowsStore.getWorkflowRunData;
if (workflowRunData === null) { if (workflowRunData === null) {
return executeData; return executeData;
} }
@@ -398,7 +507,7 @@ export function executeData(
[inputName]: workflowRunData[currentNode][runIndex].source, [inputName]: workflowRunData[currentNode][runIndex].source,
}; };
} else { } else {
const workflow = getCurrentWorkflow(); const workflow = getContextWorkflow();
let previousNodeOutput: number | undefined; let previousNodeOutput: number | undefined;
// As the node can be connected through either of the outputs find the correct one // As the node can be connected through either of the outputs find the correct one
@@ -739,7 +848,7 @@ export function useWorkflowHelpers() {
function resolveExpression( function resolveExpression(
expression: string, expression: string,
siblingParameters: INodeParameters = {}, siblingParameters: INodeParameters = {},
opts: ResolveParameterOptions & { c?: number } = {}, opts: ResolveParameterOptions | ExpressionLocalResolveContext = {},
stringifyObject = true, stringifyObject = true,
) { ) {
const parameters = { const parameters = {

View File

@@ -10,7 +10,8 @@ import type {
CanvasNodeHandleInjectionData, CanvasNodeHandleInjectionData,
CanvasNodeInjectionData, CanvasNodeInjectionData,
} from '@/types'; } from '@/types';
import type { InjectionKey, Ref } from 'vue'; import type { ComputedRef, InjectionKey, Ref } from 'vue';
import type { ExpressionLocalResolveContext } from './types/expressions';
export const MAX_WORKFLOW_SIZE = 1024 * 1024 * 16; // Workflow size limit in bytes export const MAX_WORKFLOW_SIZE = 1024 * 1024 * 16; // Workflow size limit in bytes
export const MAX_EXPECTED_REQUEST_SIZE = 2048; // Expected maximum workflow request metadata (i.e. headers) size in bytes export const MAX_EXPECTED_REQUEST_SIZE = 2048; // Expected maximum workflow request metadata (i.e. headers) size in bytes
@@ -928,6 +929,9 @@ export const CanvasNodeKey = 'canvasNode' as unknown as InjectionKey<CanvasNodeI
export const CanvasNodeHandleKey = export const CanvasNodeHandleKey =
'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>; 'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>;
export const PiPWindowSymbol = 'PiPWindow' as unknown as InjectionKey<Ref<Window | undefined>>; export const PiPWindowSymbol = 'PiPWindow' as unknown as InjectionKey<Ref<Window | undefined>>;
export const ExpressionLocalResolveContextSymbol = Symbol(
'ExpressionLocalResolveContext',
) as InjectionKey<ComputedRef<ExpressionLocalResolveContext | undefined>>;
/** Auth */ /** Auth */
export const APP_MODALS_ELEMENT_ID = 'app-modals'; export const APP_MODALS_ELEMENT_ID = 'app-modals';

View File

@@ -1,3 +1,6 @@
import type { Basic, IExecutionResponse } from '@/Interface';
import type { IWorkflowDataProxyAdditionalKeys, Workflow } from 'n8n-workflow';
type Range = { from: number; to: number }; type Range = { from: number; to: number };
export type RawSegment = { text: string; token: string } & Range; export type RawSegment = { text: string; token: string } & Range;
@@ -27,3 +30,24 @@ export namespace ColoringStateEffect {
state?: ResolvableState; state?: ResolvableState;
} & Range; } & Range;
} }
/**
* Collection of data, intended to be sufficient for resolving expressions
* in parameter name/value without referencing global state
*/
export interface ExpressionLocalResolveContext {
localResolve: true;
envVars: Record<string, Basic>;
additionalKeys: IWorkflowDataProxyAdditionalKeys;
workflow: Workflow;
execution: IExecutionResponse | null;
nodeName: string;
/**
* Allowed to be undefined (e.g., trigger node, partial execution)
*/
inputNode?: {
name: string;
runIndex: number;
branchIndex: number;
};
}