Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Execute sub-workflow UX and copy updates (no-changelog) #12834

Merged
merged 16 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
import type {
INodeTypeBaseDescription,
ISupplyDataFunctions,
Expand All @@ -7,6 +6,7 @@ import type {
INodeTypeDescription,
} from 'n8n-workflow';

import { localResourceMapping } from './methods';
import { WorkflowToolService } from './utils/WorkflowToolService';
import { versionDescription } from './versionDescription';

Expand All @@ -21,9 +21,7 @@ export class ToolWorkflowV2 implements INodeType {
}

methods = {
localResourceMapping: {
loadWorkflowInputMappings,
},
localResourceMapping,
};

async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as localResourceMapping from './localResourceMapping';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';

export async function loadSubWorkflowInputs(
this: ILocalLoadOptionsFunctions,
): Promise<ResourceMapperFields> {
const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
let emptyFieldsNotice: string | undefined;
if (fields.length === 0) {
const subworkflowLink = subworkflowInfo?.id
? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflow’s trigger</a>`
: 'sub-workflow’s trigger';

switch (dataMode) {
case 'passthrough':
emptyFieldsNotice = `This sub-workflow will consume all input data passed to it. Define specific expected input in the ${subworkflowLink}.`;
break;
default:
emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`;
break;
}
}
return { fields, emptyFieldsNotice };
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const versionDescription: INodeTypeDescription = {
typeOptions: {
loadOptionsDependsOn: ['workflowId.value'],
resourceMapper: {
localResourceMapperMethod: 'loadWorkflowInputMappings',
localResourceMapperMethod: 'loadSubWorkflowInputs',
valuesLabel: 'Workflow Inputs',
mode: 'map',
fieldWords: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import type {
} from 'n8n-workflow';

import { getWorkflowInfo } from './GenericFunctions';
import { localResourceMapping } from './methods';
import { generatePairedItemData } from '../../../utils/utilities';
import {
getCurrentWorkflowInputData,
loadWorkflowInputMappings,
} from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
import { getCurrentWorkflowInputData } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
export class ExecuteWorkflow implements INodeType {
description: INodeTypeDescription = {
displayName: 'Execute Sub-workflow',
MiloradFilipovic marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -210,7 +208,7 @@ export class ExecuteWorkflow implements INodeType {
typeOptions: {
loadOptionsDependsOn: ['workflowId.value'],
resourceMapper: {
localResourceMapperMethod: 'loadWorkflowInputMappings',
localResourceMapperMethod: 'loadSubWorkflowInputs',
valuesLabel: 'Workflow Inputs',
mode: 'map',
fieldWords: {
Expand Down Expand Up @@ -275,9 +273,7 @@ export class ExecuteWorkflow implements INodeType {
};

methods = {
localResourceMapping: {
loadWorkflowInputMappings,
},
localResourceMapping,
};

async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as localResourceMapping from './localResourceMapping';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this go into the GenericFunctions file we ended up putting all the shared logic? This is straight up duplication otherwise I think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, the second sentence in the emptyFieldNotice is different, I see :o I'll leave this up to you then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it ended up being 50-50 reusable so I decided to leave the two implementations since it's in the spirit of this refactor (leaving the message definition to each node).


import { loadWorkflowInputMappings } from '@utils/workflowInputsResourceMapping/GenericFunctions';

export async function loadSubWorkflowInputs(
this: ILocalLoadOptionsFunctions,
): Promise<ResourceMapperFields> {
const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
let emptyFieldsNotice: string | undefined;
if (fields.length === 0) {
const subworkflowLink = subworkflowInfo?.id
? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflow’s trigger</a>`
: 'sub-workflow’s trigger';

switch (dataMode) {
case 'passthrough':
emptyFieldsNotice = `This sub-workflow will consume all input data passed to it. You can define specific expected input in the ${subworkflowLink}.`;
break;
default:
emptyFieldsNotice = `The sub-workflow isn't set up to accept any inputs. Change this in the ${subworkflowLink}.`;
break;
}
}
return { fields, emptyFieldsNotice };
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type {
IDataObject,
ResourceMapperField,
ILocalLoadOptionsFunctions,
ResourceMapperFields,
ISupplyDataFunctions,
WorkflowInputsData,
} from 'n8n-workflow';
import { jsonParse, NodeOperationError, EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } from 'n8n-workflow';

Expand Down Expand Up @@ -66,8 +66,9 @@ function parseJsonExample(context: IWorkflowNodeContext): JSONSchema7 {
}

export function getFieldEntries(context: IWorkflowNodeContext): {
noFieldsMessage: string | undefined;
dataMode: WorkflowInputsData['dataMode'];
fields: FieldValueOption[];
subworkflowInfo?: WorkflowInputsData['subworkflowInfo'];
} {
const inputSource = context.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH);
let result: FieldValueOption[] | string = 'Internal Error: Invalid input source';
Expand All @@ -92,20 +93,9 @@ export function getFieldEntries(context: IWorkflowNodeContext): {
}

if (Array.isArray(result)) {
let noFieldsMessage: string | undefined;
const dataMode = String(inputSource);
if (result.length === 0) {
const id = context.getWorkflow().id;
switch (dataMode) {
case PASSTHROUGH:
noFieldsMessage = `All input data to this node will be passed to the sub-workflow. You can change this in the <a href="/workflow/${id}" target="_blank">sub-workflow's trigger</a>.`;
break;
default:
noFieldsMessage = `The sub-workflow isn't set up to accept any inputs. Change this in the <a href="/workflow/${id}" target="_blank">sub-workflow's trigger</a>.`;
break;
}
}
return { fields: result, noFieldsMessage };
const workflow = context.getWorkflow();
return { fields: result, dataMode, subworkflowInfo: { id: workflow.id } };
}
throw new NodeOperationError(context.getNode(), result);
}
Expand Down Expand Up @@ -156,14 +146,16 @@ export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) {

export async function loadWorkflowInputMappings(
this: ILocalLoadOptionsFunctions,
): Promise<ResourceMapperFields> {
): Promise<WorkflowInputsData> {
const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE);
let fields: ResourceMapperField[] = [];
let emptyFieldsNotice: string | undefined;
let dataMode: string = PASSTHROUGH;
let subworkflowInfo: { id?: string } | undefined;

if (nodeLoadContext) {
const fieldValues = getFieldEntries(nodeLoadContext);
emptyFieldsNotice = fieldValues.noFieldsMessage;
dataMode = fieldValues.dataMode;
subworkflowInfo = fieldValues.subworkflowInfo;

fields = fieldValues.fields.map((currentWorkflowInput) => {
const field: ResourceMapperField = {
Expand All @@ -182,5 +174,5 @@ export async function loadWorkflowInputMappings(
return field;
});
}
return { fields, emptyFieldsNotice };
return { fields, dataMode, subworkflowInfo };
}
7 changes: 6 additions & 1 deletion packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2660,10 +2660,15 @@ export interface IExecutionSummaryNodeExecutionResult {

export interface ResourceMapperFields {
fields: ResourceMapperField[];
mode?: string;
emptyFieldsNotice?: string;
}

export interface WorkflowInputsData {
fields: ResourceMapperField[];
dataMode: string;
subworkflowInfo?: { id?: string };
}

export interface ResourceMapperField {
id: string;
displayName: string;
Expand Down
Loading