mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 14:57:21 +02:00
fix(editor): Fix inputs when extracting sub-workflows with Split Out nodes (#19923)
This commit is contained in:
parent
65b1df9210
commit
fa64bf1ef3
|
|
@ -218,14 +218,14 @@ export function useWorkflowExtraction() {
|
|||
]
|
||||
: [];
|
||||
const triggerParameters =
|
||||
selectionVariables.size > 0
|
||||
selectionVariables.size === 0
|
||||
? {
|
||||
inputSource: 'passthrough',
|
||||
}
|
||||
: {
|
||||
workflowInputs: {
|
||||
values: [...selectionVariables.keys().map((k) => ({ name: k, type: 'any' }))],
|
||||
},
|
||||
}
|
||||
: {
|
||||
inputSource: 'passthrough',
|
||||
};
|
||||
|
||||
const triggerNode: INode = {
|
||||
|
|
@ -505,6 +505,20 @@ export function useWorkflowExtraction() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function trackStartExtractWorkflow(nodeCount: number, success: boolean) {
|
||||
telemetry.track('User started nodes to sub-workflow extraction', {
|
||||
node_count: nodeCount,
|
||||
success,
|
||||
});
|
||||
}
|
||||
|
||||
function trackExtractWorkflow(nodeCount: number, success: boolean) {
|
||||
telemetry.track('User extracted nodes to sub-workflow', {
|
||||
node_count: nodeCount,
|
||||
success,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This mutates the current workflow and creates a new one.
|
||||
* Intended to be called from @WorkflowExtractionNameModal spawned
|
||||
|
|
@ -526,25 +540,11 @@ export function useWorkflowExtraction() {
|
|||
*
|
||||
* @param nodeIds the ids to be extracted from the current workflow into a sub-workflow
|
||||
*/
|
||||
async function extractWorkflow(nodeIds: string[]) {
|
||||
function extractWorkflow(nodeIds: string[]) {
|
||||
const success = tryExtractNodesIntoSubworkflow(nodeIds);
|
||||
trackStartExtractWorkflow(nodeIds.length, success);
|
||||
}
|
||||
|
||||
function trackStartExtractWorkflow(nodeCount: number, success: boolean) {
|
||||
telemetry.track('User started nodes to sub-workflow extraction', {
|
||||
node_count: nodeCount,
|
||||
success,
|
||||
});
|
||||
}
|
||||
|
||||
function trackExtractWorkflow(nodeCount: number, success: boolean) {
|
||||
telemetry.track('User extracted nodes to sub-workflow', {
|
||||
node_count: nodeCount,
|
||||
success,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
adjacencyList,
|
||||
extractWorkflow,
|
||||
|
|
|
|||
|
|
@ -592,7 +592,7 @@ function onTidyUp(event: CanvasLayoutEvent, options?: { trackEvents?: boolean })
|
|||
}
|
||||
|
||||
function onExtractWorkflow(nodeIds: string[]) {
|
||||
void extractWorkflow(nodeIds);
|
||||
extractWorkflow(nodeIds);
|
||||
}
|
||||
|
||||
function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ const ITEM_TO_DATA_ACCESSORS = [
|
|||
/^item/,
|
||||
];
|
||||
|
||||
const SPLIT_OUT_NODE_TYPE = 'n8n-nodes-base.splitOut';
|
||||
|
||||
// These we safely can convert to a normal argument
|
||||
const ITEM_ACCESSORS = ['params', 'isExecuted'];
|
||||
|
||||
|
|
@ -492,6 +494,7 @@ export function extractReferencesInNodeExpressions(
|
|||
insertedStartName: string,
|
||||
graphInputNodeNames?: string[],
|
||||
) {
|
||||
const [start] = graphInputNodeNames ?? [];
|
||||
////
|
||||
// STEP 1 - Validate input invariants
|
||||
////
|
||||
|
|
@ -532,6 +535,8 @@ export function extractReferencesInNodeExpressions(
|
|||
const parameterTreeMappingByNode = new Map<string, ParameterExtractMapping>();
|
||||
// This is used to track all candidates for change, necessary for deduplication
|
||||
const allData = [];
|
||||
// Additional mappings that should contribute to sub-workflow inputs (e.g. Split Out 'fieldToSplitOut')
|
||||
const extraVariableCandidates: ExpressionMapping[] = [];
|
||||
|
||||
for (const node of subGraph) {
|
||||
const [parameterMapping, allMappings] = applyParameterMapping(node.parameters, (s) =>
|
||||
|
|
@ -545,6 +550,40 @@ export function extractReferencesInNodeExpressions(
|
|||
);
|
||||
parameterTreeMappingByNode.set(node.name, parameterMapping);
|
||||
allData.push(...allMappings);
|
||||
|
||||
if (node.name === start && node.type === SPLIT_OUT_NODE_TYPE) {
|
||||
const raw = node.parameters?.fieldToSplitOut;
|
||||
if (typeof raw === 'string' && raw.trim() !== '') {
|
||||
const trimmed = raw.trim();
|
||||
const isExpression = trimmed.startsWith('=');
|
||||
|
||||
// Expressions in Split Out 'fieldToSplitOut' parameters are not supported,
|
||||
// as they define the fields to split out only at execution time.
|
||||
if (isExpression) {
|
||||
throw new OperationalError(
|
||||
`Extracting sub-workflow from Split Out node with 'fieldToSplitOut' parameter having expression "${trimmed}" is not supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Parameter value is a CSV of fields to split out.
|
||||
// Create synthetic $json expressions for each field
|
||||
const fields = isExpression
|
||||
? [trimmed]
|
||||
: trimmed.split(',').map((field) => `={{$json.${field.trim()}}}`);
|
||||
|
||||
for (const expression of fields) {
|
||||
const mappingsFromField = parseReferencingExpressions(
|
||||
expression,
|
||||
nodeRegexps,
|
||||
nodeNames,
|
||||
insertedStartName,
|
||||
graphInputNodeNames?.includes(node.name) ?? false,
|
||||
);
|
||||
|
||||
extraVariableCandidates.push(...mappingsFromField);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////
|
||||
|
|
@ -552,7 +591,7 @@ export function extractReferencesInNodeExpressions(
|
|||
////
|
||||
|
||||
const subGraphNodeNames = new Set(subGraphNames);
|
||||
const dataFromOutsideSubgraph = allData.filter(
|
||||
const dataFromOutsideSubgraph = [...allData, ...extraVariableCandidates].filter(
|
||||
// `nodeNameInExpression` being absent implies direct access via `$json` or `$binary`
|
||||
(x) => !x.nodeNameInExpression || !subGraphNodeNames.has(x.nodeNameInExpression),
|
||||
);
|
||||
|
|
@ -588,6 +627,17 @@ export function extractReferencesInNodeExpressions(
|
|||
output.push(result);
|
||||
}
|
||||
|
||||
for (const candidate of extraVariableCandidates) {
|
||||
const key = originalExpressionMap.get(candidate.originalExpression);
|
||||
if (!key) continue;
|
||||
const canonical = triggerArgumentMap.get(key);
|
||||
if (!canonical) continue;
|
||||
|
||||
if (!allUsedMappings.some((u) => u.replacementName === canonical.replacementName)) {
|
||||
allUsedMappings.push(canonical);
|
||||
}
|
||||
}
|
||||
|
||||
const variables = new Map(allUsedMappings.map((m) => [m.replacementName, m.originalExpression]));
|
||||
return { nodes: output, variables };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -742,5 +742,47 @@ describe('NodeReferenceParserUtils', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract "fieldToSplitOut" constant fields in n8n-nodes-base.splitOut', () => {
|
||||
nodes = [
|
||||
{
|
||||
parameters: {
|
||||
fieldToSplitOut: 'foo,bar',
|
||||
},
|
||||
type: 'n8n-nodes-base.splitOut',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
id: 'splitOutNodeId',
|
||||
name: 'A',
|
||||
},
|
||||
];
|
||||
nodeNames = ['A', 'B'];
|
||||
|
||||
const result = extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['A']);
|
||||
expect([...result.variables.entries()]).toEqual([
|
||||
['foo', '$json.foo'],
|
||||
['bar', '$json.bar'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error at extracting "fieldToSplitOut" expression in n8n-nodes-base.splitOut', () => {
|
||||
nodes = [
|
||||
{
|
||||
parameters: {
|
||||
fieldToSplitOut: '={{ foo,bar }}',
|
||||
},
|
||||
type: 'n8n-nodes-base.splitOut',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
id: 'splitOutNodeId',
|
||||
name: 'A',
|
||||
},
|
||||
];
|
||||
nodeNames = ['A', 'B'];
|
||||
|
||||
expect(() =>
|
||||
extractReferencesInNodeExpressions(nodes, nodeNames, startNodeName, ['A']),
|
||||
).toThrow('not supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user