feat(editor, core, cli): implement new workflow experience (#4358)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node (#4108) * feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node * feat(editor): Do not show duplicate button if canvas contains `maxNodes` amount of nodes * feat(ManualTrigger node): Implement ManualTrigger node (#4110) * feat(ManualTrigger node): Implement ManualTrigger node * 📝 Remove generics doc items from ManualTrigger node * feat(editor-ui): Trigger tab redesign (#4150) * 🚧 Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory * 🚧 Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations * ✨ Implement MainPanel background scrim * ♻️ Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType` * 🐛 Fix SlideTransition for all the NodeCreato panels * 💄 Fix cursos for CategoryItem and NodeItem * 🐛 Make sure ALL_NODE_FILTER is always set when MainPanel is mounted * 🎨 Address PR comments * label: Use Array type for CategorizedItems props * 🏷️ Add proper types for Vue props * 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel * 🎨 Use kebab case for main-panel and icon component * 🏷️ Improve types * feat(editor-ui): Redesign search input inside node creator panel (#4204) * 🚧 Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory * 🚧 Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations * ✨ Implement MainPanel background scrim * ♻️ Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType` * 🐛 Fix SlideTransition for all the NodeCreato panels * 💄 Fix cursos for CategoryItem and NodeItem * 🐛 Make sure ALL_NODE_FILTER is always set when MainPanel is mounted * 🎨 Address PR comments * label: Use Array type for CategorizedItems props * 🏷️ Add proper types for Vue props * 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel * ✨ Redesign search input and unify usage of categorized items * 🏷️ Use lowercase "Boolean" as `isSearchVisible` computed return type * 🔥 Remove useless emit * ✨ Implement no result view based on subcategory, minor fixes * 🎨 Remove unused properties * feat(node-email): Change EmailReadImap display name and name (#4239) * feat(editor-ui): Implement "Choose a Triger" action and related behaviour (#4226) * ✨ Implement "Choose a Triger" action and related behaviour * 🔇 Lint fix * ♻️ Remove PlaceholderTrigger node, add a button instead * 🎨 Merge onMouseEnter and onMouseLeave to a single function * 💡 Add comment * 🔥 Remove PlaceholderNode registration * 🎨 Rename TriggerPlaceholderButton to CanvasAddButton * ✨ Add method to unregister custom action and rework CanvasAddButton centering logic * 🎨 Run `setRecenteredCanvasAddButtonPosition` on `CanvasAddButton` mount * fix(editor): Fix selecting of node from node-creator panel by clicking * 🔀 Merge fixes * fix(editor): Show execute workflow trigger instead of workflow trigger in the trigger helper panel * feat(editor): Fix node creator panel slide transition (#4261) * fix(editor): Fix node creator panel slide-in/slide-out transitions * 🎨 Fix naming * 🎨 Use kebab-case for transition component name * feat(editor): Disable execution and show notice when user tries to run workflow without enabled triggers * fix(editor): Address first batch of new WF experience review (#4279) * fix(editor): Fix first batch of review items * bug(editor): Fix nodeview canvas add button centering * 🔇 Fix linter errors * bug(ManualTrigger Node): Fix manual trigger node execution * fix(editor): Do not show canvas add button in execution or demo mode and prevent clicking if creator is open * fix(editor): do not show pin data tooltip for manual trigger node * fix(editor): do not use nodeViewOffset on zoomToFit * 💄 Add margin for last node creator item and set font-weight to 700 for category title * ✨ Position welcome note next to the added trigger node * 🐛 Remve always true welcome note * feat(editor): Minor UI and UX tweaks (#4328) * 💄 Make top viewport buttons less prominent * ✨ Allow user to switch to all tabs if it contains filter results, move nodecreator state props to its own module * 🔇 Fix linting errors * 🔇 Fix linting errors * 🔇 Fix linting errors * chore(build): Ping Turbo version to 1.5.5 * 💄 Minor traigger panel and node view style changes * 💬 Update display name of execute workflow trigger * feat(core, editor): Update subworkflow execution logic (#4269) * ✨ Implement `findWorkflowStart` * ⚡ Extend `WorkflowOperationError` * ⚡ Add `WorkflowOperationError` to toast * 📘 Extend interface * ✨ Add `subworkflowExecutionError` to store * ✨ Create `SubworkflowOperationError` * ⚡ Render subworkflow error as node error * 🚚 Move subworkflow start validation to `cli` * ⚡ Reset subworkflow execution error state * 🔥 Remove unused import * ⚡ Adjust CLI commands * 🔥 Remove unneeded check * 🔥 Remove stray log * ⚡ Simplify syntax * ⚡ Sort in case both Start and EWT present * ♻️ Address Omar's feedback * 🔥 Remove unneeded lint exception * ✏️ Fix copy * 👕 Fix lint * fix: moved find start node function to catchable place Co-authored-by: Omar Ajoue <krynble@gmail.com> * 💄 Change ExecuteWorkflow node to primary * ✨ Allow user to navigate to all tab if it contains search results * 🐛 Fixed canvas control button while in demo, disable workflow activation for non-activavle nodes and revert zoomToFit bottom offset * :fix: Do not chow request text if there's results * 💬 Update noResults text Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
|
||||
import { INode, LoggerProxy } from 'n8n-workflow';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
ActiveExecutions,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { getLogger } from '../src/Logger';
|
||||
import config from '../config';
|
||||
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
||||
import { findCliWorkflowStart } from '../src/utils';
|
||||
|
||||
export class Execute extends Command {
|
||||
static description = '\nExecutes a given workflow';
|
||||
@@ -116,6 +117,10 @@ export class Execute extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowData) {
|
||||
throw new Error('Failed to retrieve workflow data for requested workflow');
|
||||
}
|
||||
|
||||
// Make sure the settings exist
|
||||
await UserSettings.prepareUserSettings();
|
||||
|
||||
@@ -144,33 +149,14 @@ export class Execute extends Command {
|
||||
workflowId = undefined;
|
||||
}
|
||||
|
||||
// Check if the workflow contains the required "Start" node
|
||||
// "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue
|
||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||
let startNode: INode | undefined;
|
||||
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-non-null-assertion
|
||||
for (const node of workflowData!.nodes) {
|
||||
if (requiredNodeTypes.includes(node.type)) {
|
||||
startNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startNode === undefined) {
|
||||
// If the workflow does not contain a start-node we can not know what
|
||||
// should be executed and with which data to start.
|
||||
console.info(`The workflow does not contain a "Start" node. So it can not be executed.`);
|
||||
// eslint-disable-next-line consistent-return
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
const startingNode = findCliWorkflowStart(workflowData.nodes);
|
||||
|
||||
const user = await getInstanceOwner();
|
||||
const runData: IWorkflowExecutionDataProcess = {
|
||||
executionMode: 'cli',
|
||||
startNodes: [startNode.name],
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
workflowData: workflowData!,
|
||||
startNodes: [startingNode.name],
|
||||
workflowData,
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
@@ -207,6 +193,7 @@ export class Execute extends Command {
|
||||
logger.error('\nExecution error:');
|
||||
logger.info('====================================');
|
||||
logger.error(e.message);
|
||||
if (e.description) logger.error(e.description);
|
||||
logger.error(e.stack);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import config from '../config';
|
||||
import { User } from '../src/databases/entities/User';
|
||||
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
||||
import { findCliWorkflowStart } from '../src/utils';
|
||||
|
||||
export class ExecuteBatch extends Command {
|
||||
static description = '\nExecutes multiple workflows once';
|
||||
@@ -613,16 +614,6 @@ export class ExecuteBatch extends Command {
|
||||
coveredNodes: {},
|
||||
};
|
||||
|
||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||
let startNode: INode | undefined;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const node of workflowData.nodes) {
|
||||
if (requiredNodeTypes.includes(node.type)) {
|
||||
startNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cool feature here.
|
||||
// On each node, on the Settings tab in the node editor you can change
|
||||
// the `Notes` field to add special cases for comparison and snapshots.
|
||||
@@ -659,14 +650,6 @@ export class ExecuteBatch extends Command {
|
||||
});
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
if (startNode === undefined) {
|
||||
// If the workflow does not contain a start-node we can not know what
|
||||
// should be executed and with which data to start.
|
||||
executionResult.error = 'Workflow cannot be started as it does not contain a "Start" node.';
|
||||
executionResult.executionStatus = 'warning';
|
||||
resolve(executionResult);
|
||||
}
|
||||
|
||||
let gotCancel = false;
|
||||
|
||||
// Timeouts execution after 5 minutes.
|
||||
@@ -678,9 +661,11 @@ export class ExecuteBatch extends Command {
|
||||
}, ExecuteBatch.executionTimeout);
|
||||
|
||||
try {
|
||||
const startingNode = findCliWorkflowStart(workflowData.nodes);
|
||||
|
||||
const runData: IWorkflowExecutionDataProcess = {
|
||||
executionMode: 'cli',
|
||||
startNodes: [startNode!.name],
|
||||
startNodes: [startingNode.name],
|
||||
workflowData,
|
||||
userId: ExecuteBatch.instanceOwner.id,
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
IWorkflowHooksOptionalParameters,
|
||||
IWorkflowSettings,
|
||||
LoggerProxy as Logger,
|
||||
SubworkflowOperationError,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
WorkflowHooks,
|
||||
@@ -67,6 +68,7 @@ import {
|
||||
} from './UserManagement/UserManagementHelper';
|
||||
import { whereClause } from './WorkflowHelpers';
|
||||
import { IWorkflowErrorData } from './Interfaces';
|
||||
import { findSubworkflowStart } from './utils';
|
||||
|
||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||
|
||||
@@ -748,21 +750,7 @@ export async function getRunData(
|
||||
): Promise<IWorkflowExecutionDataProcess> {
|
||||
const mode = 'integrated';
|
||||
|
||||
// Find Start-Node
|
||||
const requiredNodeTypes = ['n8n-nodes-base.start'];
|
||||
let startNode: INode | undefined;
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const node of workflowData.nodes) {
|
||||
if (requiredNodeTypes.includes(node.type)) {
|
||||
startNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startNode === undefined) {
|
||||
// If the workflow does not contain a start-node we can not know what
|
||||
// should be executed and with what data to start.
|
||||
throw new Error(`The workflow does not contain a "Start" node and can so not be executed.`);
|
||||
}
|
||||
const startingNode = findSubworkflowStart(workflowData.nodes);
|
||||
|
||||
// Always start with empty data if no inputData got supplied
|
||||
inputData = inputData || [
|
||||
@@ -774,7 +762,7 @@ export async function getRunData(
|
||||
// Initialize the incoming data
|
||||
const nodeExecutionStack: IExecuteData[] = [];
|
||||
nodeExecutionStack.push({
|
||||
node: startNode,
|
||||
node: startingNode,
|
||||
data: {
|
||||
main: [inputData],
|
||||
},
|
||||
|
||||
@@ -361,11 +361,12 @@ export class WorkflowRunnerProcess {
|
||||
) {
|
||||
// Execute all nodes
|
||||
|
||||
const pinDataKeys = this.data?.pinData ? Object.keys(this.data.pinData) : [];
|
||||
const noPinData = pinDataKeys.length === 0;
|
||||
const isPinned = (nodeName: string) => pinDataKeys.includes(nodeName);
|
||||
|
||||
let startNode;
|
||||
if (
|
||||
this.data.startNodes?.length === 1 &&
|
||||
Object.keys(this.data.pinData ?? {}).includes(this.data.startNodes[0])
|
||||
) {
|
||||
if (this.data.startNodes?.length === 1 && (noPinData || isPinned(this.data.startNodes[0]))) {
|
||||
startNode = this.workflow.getNode(this.data.startNodes[0]) ?? undefined;
|
||||
}
|
||||
|
||||
|
||||
30
packages/cli/src/utils.ts
Normal file
30
packages/cli/src/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { CliWorkflowOperationError, SubworkflowOperationError } from 'n8n-workflow';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
|
||||
function findWorkflowStart(executionMode: 'integrated' | 'cli') {
|
||||
return function (nodes: INode[]) {
|
||||
const executeWorkflowTriggerNode = nodes.find(
|
||||
(node) => node.type === 'n8n-nodes-base.executeWorkflowTrigger',
|
||||
);
|
||||
|
||||
if (executeWorkflowTriggerNode) return executeWorkflowTriggerNode;
|
||||
|
||||
const startNode = nodes.find((node) => node.type === 'n8n-nodes-base.start');
|
||||
|
||||
if (startNode) return startNode;
|
||||
|
||||
const title = 'Missing node to start execution';
|
||||
const description =
|
||||
"Please make sure the workflow you're calling contains an Execute Workflow Trigger node";
|
||||
|
||||
if (executionMode === 'integrated') {
|
||||
throw new SubworkflowOperationError(title, description);
|
||||
}
|
||||
|
||||
throw new CliWorkflowOperationError(title, description);
|
||||
};
|
||||
}
|
||||
|
||||
export const findSubworkflowStart = findWorkflowStart('integrated');
|
||||
|
||||
export const findCliWorkflowStart = findWorkflowStart('cli');
|
||||
Reference in New Issue
Block a user