mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
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:
parent
128c3b83df
commit
dae01f3abe
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.198.0",
|
"version": "0.198.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.198.0",
|
"version": "0.198.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
@ -43375,7 +43375,7 @@
|
||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.198.0",
|
"version": "0.198.2",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.18",
|
"@oclif/command": "^1.5.18",
|
||||||
|
@ -43422,7 +43422,7 @@
|
||||||
"lodash.unset": "^4.5.2",
|
"lodash.unset": "^4.5.2",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.138.0",
|
"n8n-core": "~0.138.0",
|
||||||
"n8n-editor-ui": "~0.164.0",
|
"n8n-editor-ui": "~0.164.2",
|
||||||
"n8n-nodes-base": "~0.196.0",
|
"n8n-nodes-base": "~0.196.0",
|
||||||
"n8n-workflow": "~0.120.0",
|
"n8n-workflow": "~0.120.0",
|
||||||
"nodemailer": "^6.7.1",
|
"nodemailer": "^6.7.1",
|
||||||
|
@ -45822,7 +45822,7 @@
|
||||||
},
|
},
|
||||||
"packages/editor-ui": {
|
"packages/editor-ui": {
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.164.0",
|
"version": "0.164.2",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.1.0",
|
"@codemirror/autocomplete": "^6.1.0",
|
||||||
|
@ -72169,7 +72169,7 @@
|
||||||
"lodash.unset": "^4.5.2",
|
"lodash.unset": "^4.5.2",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.138.0",
|
"n8n-core": "~0.138.0",
|
||||||
"n8n-editor-ui": "~0.164.0",
|
"n8n-editor-ui": "~0.164.2",
|
||||||
"n8n-nodes-base": "~0.196.0",
|
"n8n-nodes-base": "~0.196.0",
|
||||||
"n8n-workflow": "~0.120.0",
|
"n8n-workflow": "~0.120.0",
|
||||||
"nodemailer": "^6.7.1",
|
"nodemailer": "^6.7.1",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
|
import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
|
||||||
import { INode, LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveExecutions,
|
ActiveExecutions,
|
||||||
|
@ -25,6 +25,7 @@ import {
|
||||||
import { getLogger } from '../src/Logger';
|
import { getLogger } from '../src/Logger';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
||||||
|
import { findCliWorkflowStart } from '../src/utils';
|
||||||
|
|
||||||
export class Execute extends Command {
|
export class Execute extends Command {
|
||||||
static description = '\nExecutes a given workflow';
|
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
|
// Make sure the settings exist
|
||||||
await UserSettings.prepareUserSettings();
|
await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
|
@ -144,33 +149,14 @@ export class Execute extends Command {
|
||||||
workflowId = undefined;
|
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 {
|
try {
|
||||||
|
const startingNode = findCliWorkflowStart(workflowData.nodes);
|
||||||
|
|
||||||
const user = await getInstanceOwner();
|
const user = await getInstanceOwner();
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
executionMode: 'cli',
|
executionMode: 'cli',
|
||||||
startNodes: [startNode.name],
|
startNodes: [startingNode.name],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
workflowData,
|
||||||
workflowData: workflowData!,
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,6 +193,7 @@ export class Execute extends Command {
|
||||||
logger.error('\nExecution error:');
|
logger.error('\nExecution error:');
|
||||||
logger.info('====================================');
|
logger.info('====================================');
|
||||||
logger.error(e.message);
|
logger.error(e.message);
|
||||||
|
if (e.description) logger.error(e.description);
|
||||||
logger.error(e.stack);
|
logger.error(e.stack);
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { User } from '../src/databases/entities/User';
|
import { User } from '../src/databases/entities/User';
|
||||||
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
import { getInstanceOwner } from '../src/UserManagement/UserManagementHelper';
|
||||||
|
import { findCliWorkflowStart } from '../src/utils';
|
||||||
|
|
||||||
export class ExecuteBatch extends Command {
|
export class ExecuteBatch extends Command {
|
||||||
static description = '\nExecutes multiple workflows once';
|
static description = '\nExecutes multiple workflows once';
|
||||||
|
@ -613,16 +614,6 @@ export class ExecuteBatch extends Command {
|
||||||
coveredNodes: {},
|
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.
|
// We have a cool feature here.
|
||||||
// On each node, on the Settings tab in the node editor you can change
|
// 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.
|
// 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) => {
|
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;
|
let gotCancel = false;
|
||||||
|
|
||||||
// Timeouts execution after 5 minutes.
|
// Timeouts execution after 5 minutes.
|
||||||
|
@ -678,9 +661,11 @@ export class ExecuteBatch extends Command {
|
||||||
}, ExecuteBatch.executionTimeout);
|
}, ExecuteBatch.executionTimeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startingNode = findCliWorkflowStart(workflowData.nodes);
|
||||||
|
|
||||||
const runData: IWorkflowExecutionDataProcess = {
|
const runData: IWorkflowExecutionDataProcess = {
|
||||||
executionMode: 'cli',
|
executionMode: 'cli',
|
||||||
startNodes: [startNode!.name],
|
startNodes: [startingNode.name],
|
||||||
workflowData,
|
workflowData,
|
||||||
userId: ExecuteBatch.instanceOwner.id,
|
userId: ExecuteBatch.instanceOwner.id,
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
IWorkflowHooksOptionalParameters,
|
IWorkflowHooksOptionalParameters,
|
||||||
IWorkflowSettings,
|
IWorkflowSettings,
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
|
SubworkflowOperationError,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
|
@ -67,6 +68,7 @@ import {
|
||||||
} from './UserManagement/UserManagementHelper';
|
} from './UserManagement/UserManagementHelper';
|
||||||
import { whereClause } from './WorkflowHelpers';
|
import { whereClause } from './WorkflowHelpers';
|
||||||
import { IWorkflowErrorData } from './Interfaces';
|
import { IWorkflowErrorData } from './Interfaces';
|
||||||
|
import { findSubworkflowStart } from './utils';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
|
@ -748,21 +750,7 @@ export async function getRunData(
|
||||||
): Promise<IWorkflowExecutionDataProcess> {
|
): Promise<IWorkflowExecutionDataProcess> {
|
||||||
const mode = 'integrated';
|
const mode = 'integrated';
|
||||||
|
|
||||||
// Find Start-Node
|
const startingNode = findSubworkflowStart(workflowData.nodes);
|
||||||
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.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always start with empty data if no inputData got supplied
|
// Always start with empty data if no inputData got supplied
|
||||||
inputData = inputData || [
|
inputData = inputData || [
|
||||||
|
@ -774,7 +762,7 @@ export async function getRunData(
|
||||||
// Initialize the incoming data
|
// Initialize the incoming data
|
||||||
const nodeExecutionStack: IExecuteData[] = [];
|
const nodeExecutionStack: IExecuteData[] = [];
|
||||||
nodeExecutionStack.push({
|
nodeExecutionStack.push({
|
||||||
node: startNode,
|
node: startingNode,
|
||||||
data: {
|
data: {
|
||||||
main: [inputData],
|
main: [inputData],
|
||||||
},
|
},
|
||||||
|
|
|
@ -361,11 +361,12 @@ export class WorkflowRunnerProcess {
|
||||||
) {
|
) {
|
||||||
// Execute all nodes
|
// 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;
|
let startNode;
|
||||||
if (
|
if (this.data.startNodes?.length === 1 && (noPinData || isPinned(this.data.startNodes[0]))) {
|
||||||
this.data.startNodes?.length === 1 &&
|
|
||||||
Object.keys(this.data.pinData ?? {}).includes(this.data.startNodes[0])
|
|
||||||
) {
|
|
||||||
startNode = this.workflow.getNode(this.data.startNodes[0]) ?? undefined;
|
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');
|
9
packages/editor-ui/public/static/webhook-icon.svg
Normal file
9
packages/editor-ui/public/static/webhook-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 356 KiB |
|
@ -269,6 +269,11 @@ export interface IWorkflowTemplate {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INewWorkflowData {
|
||||||
|
name: string;
|
||||||
|
onboardingFlowEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Almost identical to cli.Interfaces.ts
|
// Almost identical to cli.Interfaces.ts
|
||||||
export interface IWorkflowDb {
|
export interface IWorkflowDb {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -756,6 +761,13 @@ export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR';
|
||||||
export interface ISubcategoryItemProps {
|
export interface ISubcategoryItemProps {
|
||||||
subcategory: string;
|
subcategory: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
icon?: string;
|
||||||
|
defaults?: INodeParameters;
|
||||||
|
iconData?: {
|
||||||
|
type: string;
|
||||||
|
icon?: string;
|
||||||
|
fileBuffer?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeItemProps {
|
export interface INodeItemProps {
|
||||||
|
@ -876,6 +888,7 @@ export interface IRootState {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
nodeMetadata: {[nodeName: string]: INodeMetadata};
|
nodeMetadata: {[nodeName: string]: INodeMetadata};
|
||||||
isNpmAvailable: boolean;
|
isNpmAvailable: boolean;
|
||||||
|
subworkflowExecutionError: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICommunityPackageMap {
|
export interface ICommunityPackageMap {
|
||||||
|
@ -981,6 +994,15 @@ export type IFakeDoor = {
|
||||||
|
|
||||||
export type IFakeDoorLocation = 'settings' | 'credentialsModal';
|
export type IFakeDoorLocation = 'settings' | 'credentialsModal';
|
||||||
|
|
||||||
|
export type INodeFilterType = "Regular" | "Trigger" | "All";
|
||||||
|
|
||||||
|
export interface INodeCreatorState {
|
||||||
|
itemsFilter: string;
|
||||||
|
showTabs: boolean;
|
||||||
|
showScrim: boolean;
|
||||||
|
selectedType: INodeFilterType;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISettingsState {
|
export interface ISettingsState {
|
||||||
settings: IN8nUISettings;
|
settings: IN8nUISettings;
|
||||||
promptsData: IN8nPrompts;
|
promptsData: IN8nPrompts;
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default mixins(
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.main-header {
|
.main-header {
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
height: 65px;
|
height: $header-height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId"/>
|
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId"/>
|
||||||
</span>
|
</span>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
|
type="secondary"
|
||||||
:saved="!this.isDirty && !this.isNewWorkflow"
|
:saved="!this.isDirty && !this.isNewWorkflow"
|
||||||
:disabled="isWorkflowSaving"
|
:disabled="isWorkflowSaving"
|
||||||
@click="onSaveButtonClick"
|
@click="onSaveButtonClick"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="node-wrapper" :style="nodePosition" :id="nodeId">
|
<div class="node-wrapper" :style="nodePosition" :id="nodeId">
|
||||||
<div class="select-background" v-show="isSelected"></div>
|
<div class="select-background" v-show="isSelected"></div>
|
||||||
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
|
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
|
||||||
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
|
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
|
||||||
<div v-if="!data.disabled" :class="{'node-info-icon': true, 'shift-icon': shiftOutputCount}">
|
<div v-if="!data.disabled" :class="{'node-info-icon': true, 'shift-icon': shiftOutputCount}">
|
||||||
<div v-if="hasIssues" class="node-issues">
|
<div v-if="hasIssues" class="node-issues">
|
||||||
<n8n-tooltip placement="bottom" >
|
<n8n-tooltip placement="bottom" >
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<div v-touch:tap="disableNode" class="option" :title="$locale.baseText('node.activateDeactivateNode')">
|
<div v-touch:tap="disableNode" class="option" :title="$locale.baseText('node.activateDeactivateNode')">
|
||||||
<font-awesome-icon :icon="nodeDisabledIcon" />
|
<font-awesome-icon :icon="nodeDisabledIcon" />
|
||||||
</div>
|
</div>
|
||||||
<div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')">
|
<div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')" v-if="isDuplicatable">
|
||||||
<font-awesome-icon icon="clone" />
|
<font-awesome-icon icon="clone" />
|
||||||
</div>
|
</div>
|
||||||
<div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')" v-if="!isReadOnly">
|
<div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')" v-if="!isReadOnly">
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {CUSTOM_API_CALL_KEY, LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, WAIT_TIME_UNLIMITED} from '@/constants';
|
import { CUSTOM_API_CALL_KEY, LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, WAIT_TIME_UNLIMITED, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { nodeBase } from '@/components/mixins/nodeBase';
|
import { nodeBase } from '@/components/mixins/nodeBase';
|
||||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
|
@ -126,6 +126,10 @@ export default mixins(
|
||||||
NodeIcon,
|
NodeIcon,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isDuplicatable(): boolean {
|
||||||
|
if(!this.nodeType) return true;
|
||||||
|
return this.nodeType.maxNodes === undefined || this.sameTypeNodes.length < this.nodeType.maxNodes;
|
||||||
|
},
|
||||||
isScheduledGroup (): boolean {
|
isScheduledGroup (): boolean {
|
||||||
return this.nodeType?.group.includes('schedule') === true;
|
return this.nodeType?.group.includes('schedule') === true;
|
||||||
},
|
},
|
||||||
|
@ -183,8 +187,11 @@ export default mixins(
|
||||||
|
|
||||||
return nodes.length === 1;
|
return nodes.length === 1;
|
||||||
},
|
},
|
||||||
|
isManualTypeNode (): boolean {
|
||||||
|
return this.data.type === MANUAL_TRIGGER_NODE_TYPE;
|
||||||
|
},
|
||||||
isTriggerNode (): boolean {
|
isTriggerNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
return this.$store.getters['nodeTypes/isTriggerNode'](this.data.type);
|
||||||
},
|
},
|
||||||
isTriggerNodeTooltipEmpty () : boolean {
|
isTriggerNodeTooltipEmpty () : boolean {
|
||||||
return this.nodeType !== null ? this.nodeType.eventTriggerDescription === '' : false;
|
return this.nodeType !== null ? this.nodeType.eventTriggerDescription === '' : false;
|
||||||
|
@ -198,6 +205,9 @@ export default mixins(
|
||||||
node (): INodeUi | undefined { // same as this.data but reactive..
|
node (): INodeUi | undefined { // same as this.data but reactive..
|
||||||
return this.$store.getters.nodesByName[this.name] as INodeUi | undefined;
|
return this.$store.getters.nodesByName[this.name] as INodeUi | undefined;
|
||||||
},
|
},
|
||||||
|
sameTypeNodes (): INodeUi[] {
|
||||||
|
return this.$store.getters.allNodes.filter((node: INodeUi) => node.type === this.data.type);
|
||||||
|
},
|
||||||
nodeClass (): object {
|
nodeClass (): object {
|
||||||
return {
|
return {
|
||||||
'node-box': true,
|
'node-box': true,
|
||||||
|
@ -378,7 +388,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showPinDataDiscoveryTooltip(dataItemsCount: number): void {
|
showPinDataDiscoveryTooltip(dataItemsCount: number): void {
|
||||||
if (!this.isTriggerNode || this.isScheduledGroup || dataItemsCount === 0) return;
|
if (!this.isTriggerNode || this.isManualTypeNode || this.isScheduledGroup || dataItemsCount === 0) return;
|
||||||
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, 'true');
|
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, 'true');
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!createNodeActive" :class="[$style.nodeButtonsWrapper, showStickyButton ? $style.noEvents : '']" @mouseenter="onCreateMenuHoverIn">
|
<div v-if="!createNodeActive" :class="[$style.nodeButtonsWrapper, showStickyButton ? $style.noEvents : '']" @mouseenter="onCreateMenuHoverIn">
|
||||||
<div :class="$style.nodeCreatorButton">
|
<div :class="$style.nodeCreatorButton">
|
||||||
<n8n-icon-button size="xlarge" icon="plus" @click="openNodeCreator" :title="$locale.baseText('nodeView.addNode')"/>
|
<n8n-icon-button size="xlarge" icon="plus" type="tertiary" :class="$style.nodeCreatorPlus" @click="openNodeCreator" :title="$locale.baseText('nodeView.addNode')"/>
|
||||||
<div :class="[$style.addStickyButton, showStickyButton ? $style.visibleButton : '']" @click="addStickyNote">
|
<div :class="[$style.addStickyButton, showStickyButton ? $style.visibleButton : '']" @click="addStickyNote">
|
||||||
<n8n-icon-button size="medium" type="secondary" :icon="['far', 'note-sticky']" :title="$locale.baseText('nodeView.addSticky')"/>
|
<n8n-icon-button size="medium" type="tertiary" :icon="['far', 'note-sticky']" :title="$locale.baseText('nodeView.addSticky')"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<node-creator :active="createNodeActive" @nodeTypeSelected="nodeTypeSelected" @closeNodeCreator="closeNodeCreator" />
|
<node-creator
|
||||||
|
:active="createNodeActive"
|
||||||
|
@nodeTypeSelected="nodeTypeSelected"
|
||||||
|
@closeNodeCreator="closeNodeCreator"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -120,12 +124,25 @@ export default Vue.extend({
|
||||||
.nodeCreatorButton {
|
.nodeCreatorButton {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: 80px;
|
top: calc(#{$header-height} + var(--spacing-s));
|
||||||
right: 20px;
|
right: var(--spacing-s);
|
||||||
pointer-events: all !important;
|
pointer-events: all !important;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
position: relative;
|
border-color: var(--color-foreground-xdark);
|
||||||
|
color: var(--color-foreground-xdark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: var(--color-background-xlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.nodeCreatorPlus {
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,526 @@
|
||||||
|
<template>
|
||||||
|
<transition :name="activeSubcategoryTitle ? 'panel-slide-in' : 'panel-slide-out'" >
|
||||||
|
<div
|
||||||
|
:class="$style.categorizedItems"
|
||||||
|
ref="mainPanelContainer"
|
||||||
|
@click="onClickInside"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.capture="nodeFilterKeyDown"
|
||||||
|
:key="`${activeSubcategoryTitle}_transition`"
|
||||||
|
>
|
||||||
|
<div class="header" v-if="$slots.header">
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.subcategoryHeader" v-if="activeSubcategory">
|
||||||
|
<button :class="$style.subcategoryBackButton" @click="onSubcategoryClose">
|
||||||
|
<font-awesome-icon :class="$style.subcategoryBackIcon" icon="arrow-left" size="2x" />
|
||||||
|
</button>
|
||||||
|
<span v-text="activeSubcategoryTitle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<search-bar
|
||||||
|
v-if="isSearchVisible"
|
||||||
|
:value="nodeFilter"
|
||||||
|
@input="onNodeFilterChange"
|
||||||
|
:eventBus="searchEventBus"
|
||||||
|
/>
|
||||||
|
<div v-if="searchFilter.length === 0" :class="$style.scrollable">
|
||||||
|
<item-iterator
|
||||||
|
:elements="renderedItems"
|
||||||
|
:activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex"
|
||||||
|
:transitionsEnabled="true"
|
||||||
|
@selected="selected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="$style.scrollable"
|
||||||
|
v-else-if="filteredNodeTypes.length > 0"
|
||||||
|
>
|
||||||
|
<item-iterator
|
||||||
|
:elements="filteredNodeTypes"
|
||||||
|
:activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex"
|
||||||
|
@selected="selected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<no-results v-else :showRequest="filteredAllNodeTypes.length === 0" :show-icon="filteredAllNodeTypes.length === 0">
|
||||||
|
<!-- There are results in other sub-categories/tabs -->
|
||||||
|
<template v-if="filteredAllNodeTypes.length > 0">
|
||||||
|
<p
|
||||||
|
v-html="$locale.baseText('nodeCreator.noResults.clickToSeeResults')"
|
||||||
|
slot="title"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Regular Search -->
|
||||||
|
<template v-else>
|
||||||
|
<p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" slot="title" />
|
||||||
|
<template slot="action">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
||||||
|
<n8n-link @click="selectHttpRequest" v-if="[REGULAR_NODE_FILTER, ALL_NODE_FILTER].includes(selectedType)">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}
|
||||||
|
</n8n-link>
|
||||||
|
<template v-if="selectedType === ALL_NODE_FILTER">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.or') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n8n-link @click="selectWebhook" v-if="[TRIGGER_NODE_FILTER, ALL_NODE_FILTER].includes(selectedType)">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
|
||||||
|
</n8n-link>
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.node') }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</no-results>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue';
|
||||||
|
import camelcase from 'lodash.camelcase';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
import { globalLinkActions } from '@/components/mixins/globalLinkActions';
|
||||||
|
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import ItemIterator from './ItemIterator.vue';
|
||||||
|
import NoResults from './NoResults.vue';
|
||||||
|
import SearchBar from './SearchBar.vue';
|
||||||
|
import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps, ICategoriesWithNodes, ICategoryItemProps, INodeFilterType } from '@/Interface';
|
||||||
|
import { CORE_NODES_CATEGORY, WEBHOOK_NODE_TYPE, HTTP_REQUEST_NODE_TYPE, ALL_NODE_FILTER, TRIGGER_NODE_FILTER, REGULAR_NODE_FILTER, NODE_TYPE_COUNT_MAPPER } from '@/constants';
|
||||||
|
import { matchesNodeType, matchesSelectType } from './helpers';
|
||||||
|
import { BaseTextKey } from '@/plugins/i18n';
|
||||||
|
|
||||||
|
export default mixins(externalHooks, globalLinkActions).extend({
|
||||||
|
name: 'CategorizedItems',
|
||||||
|
components: {
|
||||||
|
ItemIterator,
|
||||||
|
NoResults,
|
||||||
|
SearchBar,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
searchItems: {
|
||||||
|
type: Array as PropType<INodeCreateElement[]>,
|
||||||
|
},
|
||||||
|
excludedCategories: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
excludedSubcategories: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
firstLevelItems: {
|
||||||
|
type: Array as PropType<INodeCreateElement[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
initialActiveCategories: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
initialActiveIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeCategory: this.initialActiveCategories || [] as string[],
|
||||||
|
// Keep track of activated subcategories so we could traverse back more than one level
|
||||||
|
activeSubcategoryHistory: [] as INodeCreateElement[],
|
||||||
|
activeIndex: this.initialActiveIndex,
|
||||||
|
activeSubcategoryIndex: 0,
|
||||||
|
searchEventBus: new Vue(),
|
||||||
|
ALL_NODE_FILTER,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.registerCustomAction('showAllNodeCreatorNodes', this.switchToAllTabAndFilter);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.$store.commit('nodeCreator/setFilter', '');
|
||||||
|
this.unregisterCustomAction('showAllNodeCreatorNodes');
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
activeSubcategory(): INodeCreateElement | null {
|
||||||
|
return this.activeSubcategoryHistory[this.activeSubcategoryHistory.length - 1] || null;
|
||||||
|
},
|
||||||
|
nodeFilter(): string {
|
||||||
|
return this.$store.getters['nodeCreator/itemsFilter'];
|
||||||
|
},
|
||||||
|
selectedType(): INodeFilterType {
|
||||||
|
return this.$store.getters['nodeCreator/selectedType'];
|
||||||
|
},
|
||||||
|
categoriesWithNodes(): ICategoriesWithNodes {
|
||||||
|
return this.$store.getters['nodeTypes/categoriesWithNodes'];
|
||||||
|
},
|
||||||
|
categorizedItems(): INodeCreateElement[] {
|
||||||
|
return this.$store.getters['nodeTypes/categorizedItems'];
|
||||||
|
},
|
||||||
|
activeSubcategoryTitle(): string {
|
||||||
|
if(!this.activeSubcategory || !this.activeSubcategory.properties) return '';
|
||||||
|
const subcategoryName = camelcase((this.activeSubcategory.properties as ISubcategoryItemProps).subcategory);
|
||||||
|
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
|
||||||
|
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
|
||||||
|
|
||||||
|
const titleLocale = this.$locale.baseText(titleLocaleKey);
|
||||||
|
const nameLocale = this.$locale.baseText(nameLocaleKey);
|
||||||
|
|
||||||
|
// If resolved title locale is same as the locale key it means it doesn't exist
|
||||||
|
// so we fallback to the subcategoryName
|
||||||
|
return titleLocale === titleLocaleKey ? nameLocale : titleLocale;
|
||||||
|
},
|
||||||
|
searchFilter(): string {
|
||||||
|
return this.nodeFilter.toLowerCase().trim();
|
||||||
|
},
|
||||||
|
filteredNodeTypes(): INodeCreateElement[] {
|
||||||
|
const searchableNodes = this.subcategorizedNodes.length > 0 ? this.subcategorizedNodes : this.searchItems;
|
||||||
|
const filter = this.searchFilter;
|
||||||
|
const matchedCategorizedNodes = searchableNodes.filter((el: INodeCreateElement) => {
|
||||||
|
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
|
||||||
|
nodeFilter: this.nodeFilter,
|
||||||
|
result: matchedCategorizedNodes,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return matchedCategorizedNodes;
|
||||||
|
},
|
||||||
|
filteredAllNodeTypes(): INodeCreateElement[] {
|
||||||
|
if(this.filteredNodeTypes.length > 0) return [];
|
||||||
|
|
||||||
|
const matchedAllNodex = this.searchItems.filter((el: INodeCreateElement) => {
|
||||||
|
return this.searchFilter && matchesNodeType(el, this.searchFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchedAllNodex;
|
||||||
|
},
|
||||||
|
categorized(): INodeCreateElement[] {
|
||||||
|
return this.categorizedItems && this.categorizedItems
|
||||||
|
.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
|
||||||
|
if((this.excludedCategories || []).includes(el.category)) return accu;
|
||||||
|
|
||||||
|
if(
|
||||||
|
el.type === 'subcategory' &&
|
||||||
|
(this.excludedSubcategories || []).includes((el.properties as ISubcategoryItemProps).subcategory)
|
||||||
|
) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
el.type !== 'category' &&
|
||||||
|
!this.activeCategory.includes(el.category)
|
||||||
|
) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchesSelectType(el, this.selectedType)) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.type === 'category') {
|
||||||
|
accu.push({
|
||||||
|
...el,
|
||||||
|
properties: {
|
||||||
|
expanded: this.activeCategory.includes(el.category),
|
||||||
|
},
|
||||||
|
} as INodeCreateElement);
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
accu.push(el);
|
||||||
|
return accu;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
subcategorizedItems(): INodeCreateElement[] {
|
||||||
|
const activeSubcategory = this.activeSubcategory;
|
||||||
|
if(!activeSubcategory) return [];
|
||||||
|
|
||||||
|
const category = activeSubcategory.category;
|
||||||
|
const subcategory = (activeSubcategory.properties as ISubcategoryItemProps).subcategory;
|
||||||
|
|
||||||
|
// If no category is set, we use all categorized nodes
|
||||||
|
const nodes = category
|
||||||
|
? this.categoriesWithNodes[category][subcategory].nodes
|
||||||
|
: this.categorized;
|
||||||
|
|
||||||
|
return nodes.filter((el: INodeCreateElement) => matchesSelectType(el, this.selectedType));
|
||||||
|
},
|
||||||
|
|
||||||
|
subcategorizedNodes(): INodeCreateElement[] {
|
||||||
|
return this.subcategorizedItems.filter(node => node.type === 'node');
|
||||||
|
},
|
||||||
|
|
||||||
|
renderedItems(): INodeCreateElement[] {
|
||||||
|
if(this.firstLevelItems.length > 0 && this.activeSubcategory === null) return this.firstLevelItems;
|
||||||
|
if(this.subcategorizedItems.length === 0) return this.categorized;
|
||||||
|
|
||||||
|
return this.subcategorizedItems;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSearchVisible(): boolean {
|
||||||
|
if(this.subcategorizedItems.length === 0) return true;
|
||||||
|
|
||||||
|
let totalItems = 0;
|
||||||
|
for (const item of this.subcategorizedItems) {
|
||||||
|
// Category contains many nodes so we need to count all of them
|
||||||
|
// for the current selectedType
|
||||||
|
if(item.type === 'category') {
|
||||||
|
const categoryItems = this.categoriesWithNodes[item.key];
|
||||||
|
const categoryItemsCount = Object.values(categoryItems)?.[0];
|
||||||
|
const countKeys = NODE_TYPE_COUNT_MAPPER[this.selectedType];
|
||||||
|
|
||||||
|
for (const countKey of countKeys) {
|
||||||
|
totalItems += categoryItemsCount[(countKey as "triggerCount" | "regularCount")];
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If it's not category, it must be just a node item so we count it as 1
|
||||||
|
totalItems += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalItems > 9;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isSearchVisible(isVisible) {
|
||||||
|
if(isVisible === false) {
|
||||||
|
// Focus the root container when search is hidden to make sure
|
||||||
|
// keyboard navigation still works
|
||||||
|
this.$nextTick(() => {
|
||||||
|
(this.$refs.mainPanelContainer as HTMLElement).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nodeFilter(newValue, oldValue) {
|
||||||
|
// Reset the index whenver the filter-value changes
|
||||||
|
this.activeIndex = 0;
|
||||||
|
this.activeSubcategoryIndex = 0;
|
||||||
|
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
filteredNodes: this.filteredNodeTypes,
|
||||||
|
});
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
filteredNodes: this.filteredNodeTypes,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
switchToAllTabAndFilter() {
|
||||||
|
const currentFilter = this.nodeFilter;
|
||||||
|
this.$store.commit('nodeCreator/setShowTabs', true);
|
||||||
|
this.$store.commit('nodeCreator/setSelectedType', ALL_NODE_FILTER);
|
||||||
|
this.activeSubcategoryHistory = [];
|
||||||
|
|
||||||
|
this.$nextTick(() => this.$store.commit('nodeCreator/setFilter', currentFilter));
|
||||||
|
},
|
||||||
|
onNodeFilterChange(filter: string) {
|
||||||
|
this.$store.commit('nodeCreator/setFilter', filter);
|
||||||
|
},
|
||||||
|
selectWebhook() {
|
||||||
|
this.$emit('nodeTypeSelected', WEBHOOK_NODE_TYPE);
|
||||||
|
},
|
||||||
|
selectHttpRequest() {
|
||||||
|
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_TYPE);
|
||||||
|
},
|
||||||
|
nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
|
// We only want to propagate 'Escape' as it closes the node-creator and
|
||||||
|
// 'Tab' which toggles it
|
||||||
|
if (!['Escape', 'Tab'].includes(e.key)) e.stopPropagation();
|
||||||
|
|
||||||
|
// Prevent cursors position change
|
||||||
|
if(['ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault();
|
||||||
|
|
||||||
|
if (this.activeSubcategory) {
|
||||||
|
const activeList = this.subcategorizedItems;
|
||||||
|
const activeNodeType = activeList[this.activeSubcategoryIndex];
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' && this.activeSubcategory) {
|
||||||
|
this.activeSubcategoryIndex++;
|
||||||
|
this.activeSubcategoryIndex = Math.min(
|
||||||
|
this.activeSubcategoryIndex,
|
||||||
|
activeList.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (e.key === 'ArrowUp' && this.activeSubcategory) {
|
||||||
|
this.activeSubcategoryIndex--;
|
||||||
|
this.activeSubcategoryIndex = Math.max(this.activeSubcategoryIndex, 0);
|
||||||
|
}
|
||||||
|
else if (e.key === 'Enter') {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowLeft' && activeNodeType?.type === 'category' && (activeNodeType.properties as ICategoryItemProps).expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
this.onSubcategoryClose();
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'category' && !(activeNodeType.properties as ICategoryItemProps).expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeList = this.searchFilter.length > 0 ? this.filteredNodeTypes : this.renderedItems;
|
||||||
|
const activeNodeType = activeList[this.activeIndex];
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
this.activeIndex++;
|
||||||
|
// Make sure that we stop at the last nodeType
|
||||||
|
this.activeIndex = Math.min(
|
||||||
|
this.activeIndex,
|
||||||
|
activeList.length - 1,
|
||||||
|
);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
this.activeIndex--;
|
||||||
|
// Make sure that we do not get before the first nodeType
|
||||||
|
this.activeIndex = Math.max(this.activeIndex, 0);
|
||||||
|
} else if (e.key === 'Enter' && activeNodeType) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'category' && !(activeNodeType.properties as ICategoryItemProps).expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowLeft' && activeNodeType?.type === 'category' && (activeNodeType.properties as ICategoryItemProps).expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected(element: INodeCreateElement) {
|
||||||
|
const typeHandler = {
|
||||||
|
node: () => this.$emit('nodeTypeSelected', (element.properties as INodeItemProps).nodeType.name),
|
||||||
|
category: () => this.onCategorySelected(element.category),
|
||||||
|
subcategory: () => this.onSubcategorySelected(element),
|
||||||
|
};
|
||||||
|
|
||||||
|
typeHandler[element.type]();
|
||||||
|
},
|
||||||
|
onCategorySelected(category: string) {
|
||||||
|
if (this.activeCategory.includes(category)) {
|
||||||
|
this.activeCategory = this.activeCategory.filter(
|
||||||
|
(active: string) => active !== category,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.activeCategory = [...this.activeCategory, category];
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { category_name: category, workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeIndex = this.categorized.findIndex(
|
||||||
|
(el: INodeCreateElement) => el.category === category,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSubcategorySelected(selected: INodeCreateElement) {
|
||||||
|
this.$emit('onSubcategorySelected', selected);
|
||||||
|
this.$store.commit('nodeCreator/setShowTabs', false);
|
||||||
|
this.activeSubcategoryIndex = 0;
|
||||||
|
this.activeSubcategoryHistory.push(selected);
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { selected, workflow_id: this.$store.getters.workflowId });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubcategoryClose() {
|
||||||
|
this.$emit('subcategoryClose', this.activeSubcategory);
|
||||||
|
this.activeSubcategoryHistory.pop();
|
||||||
|
this.activeSubcategoryIndex = 0;
|
||||||
|
this.$store.commit('nodeCreator/setFilter', '');
|
||||||
|
|
||||||
|
if(!this.$store.getters['nodeCreator/showScrim']) {
|
||||||
|
this.$store.commit('nodeCreator/setShowTabs', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickInside() {
|
||||||
|
this.searchEventBus.$emit('focus');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
:global(.panel-slide-in-leave-active),
|
||||||
|
:global(.panel-slide-in-enter-active),
|
||||||
|
:global(.panel-slide-out-leave-active),
|
||||||
|
:global(.panel-slide-out-enter-active) {
|
||||||
|
transition: transform 300ms ease;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:global(.panel-slide-out-enter),
|
||||||
|
:global(.panel-slide-in-leave-to) {
|
||||||
|
transform: translateX(0);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.panel-slide-out-leave-to),
|
||||||
|
:global(.panel-slide-in-enter) {
|
||||||
|
transform: translateX(100%);
|
||||||
|
// Make sure the leaving panel stays on top
|
||||||
|
// for the slide-out panel effect
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categorizedItems {
|
||||||
|
background: white;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: $node-creator-background-color;
|
||||||
|
&:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: '';
|
||||||
|
border-left: 1px solid $node-creator-border-color;
|
||||||
|
width: 1px;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subcategoryHeader {
|
||||||
|
border-bottom: $node-creator-border-color solid 1px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: $node-creator-subcategory-panel-header-bacground-color;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 11px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategoryBackButton {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategoryBackIcon {
|
||||||
|
color: $node-creator-arrow-color;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
height: calc(100% - 120px);
|
||||||
|
padding-top: 1px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.category">
|
<div :class="$style.category">
|
||||||
<span :class="$style.name">
|
<span :class="$style.name">
|
||||||
{{ renderCategoryName(categoryName) }}
|
{{ renderCategoryName(categoryName) }} ({{ nodesCount }})
|
||||||
</span>
|
</span>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:class="$style.arrow"
|
:class="$style.arrow"
|
||||||
|
@ -13,16 +13,49 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue, { PropType } from 'vue';
|
||||||
import camelcase from 'lodash.camelcase';
|
import camelcase from 'lodash.camelcase';
|
||||||
import { CategoryName } from '@/plugins/i18n';
|
import { CategoryName } from '@/plugins/i18n';
|
||||||
|
import { INodeCreateElement, ICategoriesWithNodes } from '@/Interface';
|
||||||
|
import { NODE_TYPE_COUNT_MAPPER } from '@/constants';
|
||||||
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['item'],
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<INodeCreateElement>,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
selectedType(): "Regular" | "Trigger" | "All" {
|
||||||
|
return this.$store.getters['nodeCreator/selectedType'];
|
||||||
|
},
|
||||||
|
categoriesWithNodes(): ICategoriesWithNodes {
|
||||||
|
return this.$store.getters['nodeTypes/categoriesWithNodes'];
|
||||||
|
},
|
||||||
|
categorizedItems(): INodeCreateElement[] {
|
||||||
|
return this.$store.getters['nodeTypes/categorizedItems'];
|
||||||
|
},
|
||||||
categoryName() {
|
categoryName() {
|
||||||
return camelcase(this.item.category);
|
return camelcase(this.item.category);
|
||||||
},
|
},
|
||||||
|
nodesCount(): number {
|
||||||
|
const currentCategory = this.categoriesWithNodes[this.item.category];
|
||||||
|
const subcategories = Object.keys(currentCategory);
|
||||||
|
|
||||||
|
// We need to sum subcategories count for the curent nodeType view
|
||||||
|
// to get the total count of category
|
||||||
|
const count = subcategories.reduce((accu: number, subcategory: string) => {
|
||||||
|
const countKeys = NODE_TYPE_COUNT_MAPPER[this.selectedType];
|
||||||
|
|
||||||
|
for (const countKey of countKeys) {
|
||||||
|
accu += currentCategory[subcategory][(countKey as "triggerCount" | "regularCount")];
|
||||||
|
}
|
||||||
|
|
||||||
|
return accu;
|
||||||
|
}, 0);
|
||||||
|
return count;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
renderCategoryName(categoryName: CategoryName) {
|
renderCategoryName(categoryName: CategoryName) {
|
||||||
|
@ -38,7 +71,7 @@ export default Vue.extend({
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.category {
|
.category {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
line-height: 11px;
|
line-height: 11px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
@ -46,6 +79,7 @@ export default Vue.extend({
|
||||||
border-bottom: 1px solid $node-creator-border-color;
|
border-bottom: 1px solid $node-creator-border-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
|
|
@ -7,20 +7,19 @@
|
||||||
}"
|
}"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
>
|
>
|
||||||
<CategoryItem
|
<category-item
|
||||||
v-if="item.type === 'category'"
|
v-if="item.type === 'category'"
|
||||||
:item="item"
|
:item="item"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubcategoryItem
|
<subcategory-item
|
||||||
v-else-if="item.type === 'subcategory'"
|
v-else-if="item.type === 'subcategory'"
|
||||||
:item="item"
|
:item="item"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NodeItem
|
<node-item
|
||||||
v-else-if="item.type === 'node'"
|
v-else-if="item.type === 'node'"
|
||||||
:nodeType="item.properties.nodeType"
|
:nodeType="item.properties.nodeType"
|
||||||
:bordered="!lastNode"
|
|
||||||
@dragstart="$listeners.dragstart"
|
@dragstart="$listeners.dragstart"
|
||||||
@dragend="$listeners.dragend"
|
@dragend="$listeners.dragend"
|
||||||
/>
|
/>
|
||||||
|
@ -28,10 +27,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue, { PropType } from 'vue';
|
||||||
|
import { INodeCreateElement } from '@/Interface';
|
||||||
import NodeItem from './NodeItem.vue';
|
import NodeItem from './NodeItem.vue';
|
||||||
import CategoryItem from './CategoryItem.vue';
|
|
||||||
import SubcategoryItem from './SubcategoryItem.vue';
|
import SubcategoryItem from './SubcategoryItem.vue';
|
||||||
|
import CategoryItem from './CategoryItem.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'CreatorItem',
|
name: 'CreatorItem',
|
||||||
|
@ -40,7 +40,20 @@ export default Vue.extend({
|
||||||
SubcategoryItem,
|
SubcategoryItem,
|
||||||
NodeItem,
|
NodeItem,
|
||||||
},
|
},
|
||||||
props: ['item', 'active', 'clickable', 'lastNode'],
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<INodeCreateElement>,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
clickable: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
lastNode: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
|
:is="transitionsEnabled ? 'transition-group' : 'div'"
|
||||||
|
class="item-iterator"
|
||||||
|
name="accordion"
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@enter="enter"
|
||||||
|
@before-leave="beforeLeave"
|
||||||
|
@leave="leave"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:is="transitionsEnabled ? 'transition-group' : 'div'"
|
v-for="(item, index) in elements"
|
||||||
name="accordion"
|
:key="item.key"
|
||||||
@before-enter="beforeEnter"
|
:class="item.type"
|
||||||
@enter="enter"
|
:data-key="item.key"
|
||||||
@before-leave="beforeLeave"
|
|
||||||
@leave="leave"
|
|
||||||
>
|
>
|
||||||
<div
|
<creator-item
|
||||||
v-for="(item, index) in elements"
|
:item="item"
|
||||||
:key="item.key"
|
:active="activeIndex === index && !disabled"
|
||||||
:class="item.type"
|
:clickable="!disabled"
|
||||||
:data-key="item.key"
|
:lastNode="
|
||||||
>
|
index === elements.length - 1 || elements[index + 1].type !== 'node'
|
||||||
<CreatorItem
|
"
|
||||||
:item="item"
|
@click="$emit('selected', item)"
|
||||||
:active="activeIndex === index && !disabled"
|
@dragstart="emit('dragstart', item, $event)"
|
||||||
:clickable="!disabled"
|
@dragend="emit('dragend', item, $event)"
|
||||||
:lastNode="
|
/>
|
||||||
index === elements.length - 1 || elements[index + 1].type !== 'node'
|
|
||||||
"
|
|
||||||
@click="$emit('selected', item)"
|
|
||||||
@dragstart="emit('dragstart', item, $event)"
|
|
||||||
@dragend="emit('dragend', item, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { INodeCreateElement } from '@/Interface';
|
import { INodeCreateElement } from '@/Interface';
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue, { PropType } from 'vue';
|
||||||
import CreatorItem from './CreatorItem.vue';
|
import CreatorItem from './CreatorItem.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
@ -41,7 +40,20 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
CreatorItem,
|
CreatorItem,
|
||||||
},
|
},
|
||||||
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
|
props: {
|
||||||
|
elements: {
|
||||||
|
type: Array as PropType<INodeCreateElement[]>,
|
||||||
|
},
|
||||||
|
activeIndex: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
transitionsEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
emit(eventName: string, element: INodeCreateElement, event: Event) {
|
emit(eventName: string, element: INodeCreateElement, event: Event) {
|
||||||
if (this.$props.disabled) {
|
if (this.$props.disabled) {
|
||||||
|
@ -68,6 +80,9 @@ export default Vue.extend({
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.item-iterator > *:last-child {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
.accordion-enter {
|
.accordion-enter {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,176 +2,64 @@
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="container"
|
||||||
ref="mainPanelContainer"
|
ref="mainPanelContainer"
|
||||||
@click="onClickInside"
|
|
||||||
>
|
>
|
||||||
<SlideTransition>
|
|
||||||
<SubcategoryPanel
|
|
||||||
v-if="activeSubcategory"
|
|
||||||
:elements="subcategorizedNodes"
|
|
||||||
:title="activeSubcategory.properties.subcategory"
|
|
||||||
:activeIndex="activeSubcategoryIndex"
|
|
||||||
@close="onSubcategoryClose"
|
|
||||||
@selected="selected"
|
|
||||||
/>
|
|
||||||
</SlideTransition>
|
|
||||||
<div class="main-panel">
|
<div class="main-panel">
|
||||||
<SearchBar
|
<trigger-helper-panel
|
||||||
v-model="nodeFilter"
|
v-if="selectedType === TRIGGER_NODE_FILTER"
|
||||||
:eventBus="searchEventBus"
|
:searchItems="searchItems"
|
||||||
@keydown.native="nodeFilterKeyDown"
|
@nodeTypeSelected="nodeType => $emit('nodeTypeSelected', nodeType)"
|
||||||
/>
|
|
||||||
<div class="type-selector">
|
|
||||||
<el-tabs v-model="selectedType" stretch>
|
|
||||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
|
|
||||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
|
||||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchFilter.length === 0" class="scrollable">
|
|
||||||
<ItemIterator
|
|
||||||
:elements="categorized"
|
|
||||||
:disabled="!!activeSubcategory"
|
|
||||||
:activeIndex="activeIndex"
|
|
||||||
:transitionsEnabled="true"
|
|
||||||
@selected="selected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="scrollable"
|
|
||||||
v-else-if="filteredNodeTypes.length > 0"
|
|
||||||
>
|
>
|
||||||
<ItemIterator
|
<type-selector slot="header" />
|
||||||
:elements="filteredNodeTypes"
|
</trigger-helper-panel>
|
||||||
:activeIndex="activeIndex"
|
<categorized-items
|
||||||
@selected="selected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NoResults
|
|
||||||
v-else
|
v-else
|
||||||
@nodeTypeSelected="$emit('nodeTypeSelected', $event)"
|
:searchItems="searchItems"
|
||||||
/>
|
:excludedSubcategories="[OTHER_TRIGGER_NODES_SUBCATEGORY]"
|
||||||
|
:initialActiveCategories="[CORE_NODES_CATEGORY]"
|
||||||
|
@nodeTypeSelected="nodeType => $emit('nodeTypeSelected', nodeType)"
|
||||||
|
>
|
||||||
|
<type-selector slot="header" />
|
||||||
|
</categorized-items>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import ItemIterator from './ItemIterator.vue';
|
import TriggerHelperPanel from './TriggerHelperPanel.vue';
|
||||||
import NoResults from './NoResults.vue';
|
import { ALL_NODE_FILTER, TRIGGER_NODE_FILTER, OTHER_TRIGGER_NODES_SUBCATEGORY, CORE_NODES_CATEGORY } from '@/constants';
|
||||||
import SearchBar from './SearchBar.vue';
|
import CategorizedItems from './CategorizedItems.vue';
|
||||||
import SubcategoryPanel from './SubcategoryPanel.vue';
|
import TypeSelector from './TypeSelector.vue';
|
||||||
import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps } from '@/Interface';
|
import { INodeCreateElement } from '@/Interface';
|
||||||
import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
|
|
||||||
import SlideTransition from '../../transitions/SlideTransition.vue';
|
|
||||||
import { matchesNodeType, matchesSelectType } from './helpers';
|
|
||||||
|
|
||||||
export default mixins(externalHooks).extend({
|
export default mixins(externalHooks).extend({
|
||||||
name: 'NodeCreateList',
|
name: 'NodeCreateList',
|
||||||
components: {
|
components: {
|
||||||
ItemIterator,
|
TriggerHelperPanel,
|
||||||
NoResults,
|
CategorizedItems,
|
||||||
SubcategoryPanel,
|
TypeSelector,
|
||||||
SlideTransition,
|
},
|
||||||
SearchBar,
|
props: {
|
||||||
|
searchItems: {
|
||||||
|
type: Array as PropType<INodeCreateElement[] | null>,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
props: ['categorizedItems', 'categoriesWithNodes', 'searchItems'],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeCategory: [] as string[],
|
CORE_NODES_CATEGORY,
|
||||||
activeSubcategory: null as INodeCreateElement | null,
|
|
||||||
activeIndex: 1,
|
|
||||||
activeSubcategoryIndex: 0,
|
|
||||||
nodeFilter: '',
|
|
||||||
selectedType: ALL_NODE_FILTER,
|
|
||||||
searchEventBus: new Vue(),
|
|
||||||
REGULAR_NODE_FILTER,
|
|
||||||
TRIGGER_NODE_FILTER,
|
TRIGGER_NODE_FILTER,
|
||||||
ALL_NODE_FILTER,
|
ALL_NODE_FILTER,
|
||||||
|
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
searchFilter(): string {
|
selectedType(): string {
|
||||||
return this.nodeFilter.toLowerCase().trim();
|
return this.$store.getters['nodeCreator/selectedType'];
|
||||||
},
|
|
||||||
filteredNodeTypes(): INodeCreateElement[] {
|
|
||||||
const nodeTypes: INodeCreateElement[] = this.searchItems;
|
|
||||||
const filter = this.searchFilter;
|
|
||||||
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
|
|
||||||
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
|
|
||||||
nodeFilter: this.nodeFilter,
|
|
||||||
result: returnData,
|
|
||||||
selectedType: this.selectedType,
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return returnData;
|
|
||||||
},
|
|
||||||
|
|
||||||
categorized() {
|
|
||||||
return this.categorizedItems && this.categorizedItems
|
|
||||||
.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
|
|
||||||
if (
|
|
||||||
el.type !== 'category' &&
|
|
||||||
!this.activeCategory.includes(el.category)
|
|
||||||
) {
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchesSelectType(el, this.selectedType)) {
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.type === 'category') {
|
|
||||||
accu.push({
|
|
||||||
...el,
|
|
||||||
properties: {
|
|
||||||
expanded: this.activeCategory.includes(el.category),
|
|
||||||
},
|
|
||||||
} as INodeCreateElement);
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
accu.push(el);
|
|
||||||
return accu;
|
|
||||||
}, []);
|
|
||||||
},
|
|
||||||
|
|
||||||
subcategorizedNodes() {
|
|
||||||
const activeSubcategory = this.activeSubcategory as INodeCreateElement;
|
|
||||||
const category = activeSubcategory.category;
|
|
||||||
const subcategory = (activeSubcategory.properties as ISubcategoryItemProps).subcategory;
|
|
||||||
|
|
||||||
return activeSubcategory && this.categoriesWithNodes[category][subcategory]
|
|
||||||
.nodes.filter((el: INodeCreateElement) => matchesSelectType(el, this.selectedType));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
nodeFilter(newValue, oldValue) {
|
|
||||||
// Reset the index whenver the filter-value changes
|
|
||||||
this.activeIndex = 0;
|
|
||||||
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', {
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
selectedType: this.selectedType,
|
|
||||||
filteredNodes: this.filteredNodeTypes,
|
|
||||||
});
|
|
||||||
this.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
selectedType: this.selectedType,
|
|
||||||
filteredNodes: this.filteredNodeTypes,
|
|
||||||
workflow_id: this.$store.getters.workflowId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectedType(newValue, oldValue) {
|
selectedType(newValue, oldValue) {
|
||||||
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
||||||
oldValue,
|
oldValue,
|
||||||
|
@ -184,170 +72,23 @@ export default mixins(externalHooks).extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
mounted() {
|
||||||
nodeFilterKeyDown(e: KeyboardEvent) {
|
|
||||||
if (!['Escape', 'Tab'].includes(e.key)) {
|
|
||||||
// We only want to propagate 'Escape' as it closes the node-creator and
|
|
||||||
// 'Tab' which toggles it
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.activeSubcategory) {
|
|
||||||
const activeList = this.subcategorizedNodes;
|
|
||||||
const activeNodeType = activeList[this.activeSubcategoryIndex];
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown' && this.activeSubcategory) {
|
|
||||||
this.activeSubcategoryIndex++;
|
|
||||||
this.activeSubcategoryIndex = Math.min(
|
|
||||||
this.activeSubcategoryIndex,
|
|
||||||
activeList.length - 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (e.key === 'ArrowUp' && this.activeSubcategory) {
|
|
||||||
this.activeSubcategoryIndex--;
|
|
||||||
this.activeSubcategoryIndex = Math.max(this.activeSubcategoryIndex, 0);
|
|
||||||
}
|
|
||||||
else if (e.key === 'Enter') {
|
|
||||||
this.selected(activeNodeType);
|
|
||||||
}
|
|
||||||
else if (e.key === 'ArrowLeft') {
|
|
||||||
this.onSubcategoryClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let activeList;
|
|
||||||
if (this.searchFilter.length > 0) {
|
|
||||||
activeList = this.filteredNodeTypes;
|
|
||||||
} else {
|
|
||||||
activeList = this.categorized;
|
|
||||||
}
|
|
||||||
const activeNodeType = activeList[this.activeIndex];
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
this.activeIndex++;
|
|
||||||
// Make sure that we stop at the last nodeType
|
|
||||||
this.activeIndex = Math.min(
|
|
||||||
this.activeIndex,
|
|
||||||
activeList.length - 1,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
this.activeIndex--;
|
|
||||||
// Make sure that we do not get before the first nodeType
|
|
||||||
this.activeIndex = Math.max(this.activeIndex, 0);
|
|
||||||
} else if (e.key === 'Enter' && activeNodeType) {
|
|
||||||
this.selected(activeNodeType);
|
|
||||||
} else if (e.key === 'ArrowRight' && activeNodeType && activeNodeType.type === 'subcategory') {
|
|
||||||
this.selected(activeNodeType);
|
|
||||||
} else if (e.key === 'ArrowRight' && activeNodeType && activeNodeType.type === 'category' && !activeNodeType.properties.expanded) {
|
|
||||||
this.selected(activeNodeType);
|
|
||||||
} else if (e.key === 'ArrowLeft' && activeNodeType && activeNodeType.type === 'category' && activeNodeType.properties.expanded) {
|
|
||||||
this.selected(activeNodeType);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected(element: INodeCreateElement) {
|
|
||||||
if (element.type === 'node') {
|
|
||||||
this.$emit('nodeTypeSelected', (element.properties as INodeItemProps).nodeType.name);
|
|
||||||
} else if (element.type === 'category') {
|
|
||||||
this.onCategorySelected(element.category);
|
|
||||||
} else if (element.type === 'subcategory') {
|
|
||||||
this.onSubcategorySelected(element);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCategorySelected(category: string) {
|
|
||||||
if (this.activeCategory.includes(category)) {
|
|
||||||
this.activeCategory = this.activeCategory.filter(
|
|
||||||
(active: string) => active !== category,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.activeCategory = [...this.activeCategory, category];
|
|
||||||
this.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { category_name: category, workflow_id: this.$store.getters.workflowId });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeIndex = this.categorized.findIndex(
|
|
||||||
(el: INodeCreateElement) => el.category === category,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSubcategorySelected(selected: INodeCreateElement) {
|
|
||||||
this.activeSubcategoryIndex = 0;
|
|
||||||
this.activeSubcategory = selected;
|
|
||||||
this.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { selected, workflow_id: this.$store.getters.workflowId });
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubcategoryClose() {
|
|
||||||
this.activeSubcategory = null;
|
|
||||||
this.activeSubcategoryIndex = 0;
|
|
||||||
this.nodeFilter = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
onClickInside() {
|
|
||||||
this.searchEventBus.$emit('focus');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// initial opening effect
|
|
||||||
this.activeCategory = [CORE_NODES_CATEGORY];
|
|
||||||
});
|
|
||||||
this.$externalHooks().run('nodeCreateList.mounted');
|
this.$externalHooks().run('nodeCreateList.mounted');
|
||||||
|
// Make sure tabs are visible on mount
|
||||||
|
this.$store.commit('nodeCreator/setShowTabs', true);
|
||||||
},
|
},
|
||||||
async destroyed() {
|
destroyed() {
|
||||||
|
this.$store.commit('nodeCreator/setSelectedType', ALL_NODE_FILTER);
|
||||||
this.$externalHooks().run('nodeCreateList.destroyed');
|
this.$externalHooks().run('nodeCreateList.destroyed');
|
||||||
this.$telemetry.trackNodesPanel('nodeCreateList.destroyed', { workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.trackNodesPanel('nodeCreateList.destroyed', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
::v-deep .el-tabs__item {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-tabs__active-bar {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-tabs__nav-wrap::after {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
> div {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.main-panel {
|
||||||
.main-panel .scrollable {
|
height: 100%;
|
||||||
height: calc(100% - 160px);
|
|
||||||
padding-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: visible;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
padding-bottom: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-selector {
|
|
||||||
text-align: center;
|
|
||||||
background-color: $node-creator-select-background-color;
|
|
||||||
|
|
||||||
::v-deep .el-tabs > div {
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.el-tabs__nav {
|
|
||||||
height: 43px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,31 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="no-results">
|
<div :class="$style.noResults">
|
||||||
<div class="icon">
|
<div :class="$style.icon" v-if="showIcon">
|
||||||
<NoResultsIcon />
|
<no-results-icon />
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div :class="$style.title">
|
||||||
<div>
|
<slot name="title" />
|
||||||
{{ $locale.baseText('nodeCreator.noResults.weDidntMakeThatYet') }}
|
<div :class="$style.action">
|
||||||
</div>
|
<slot name="action" />
|
||||||
<div class="action">
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
|
||||||
<n8n-link @click="selectHttpRequest">{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}</n8n-link> {{ $locale.baseText('nodeCreator.noResults.or') }}
|
|
||||||
<n8n-link @click="selectWebhook">{{ $locale.baseText('nodeCreator.noResults.webhook') }}</n8n-link> {{ $locale.baseText('nodeCreator.noResults.node') }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="request">
|
<div :class="$style.request" v-if="showRequest">
|
||||||
|
<p v-text="$locale.baseText('nodeCreator.noResults.wantUsToMakeItFaster')" />
|
||||||
<div>
|
<div>
|
||||||
{{ $locale.baseText('nodeCreator.noResults.wantUsToMakeItFaster') }}
|
<n8n-link :to="REQUEST_NODE_FORM_URL">
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<n8n-link
|
|
||||||
:to="REQUEST_NODE_FORM_URL"
|
|
||||||
>
|
|
||||||
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span>
|
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span>
|
||||||
<span>
|
<span>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
class="external"
|
:class="$style.external"
|
||||||
icon="external-link-alt"
|
icon="external-link-alt"
|
||||||
:title="$locale.baseText('nodeCreator.noResults.requestTheNode')"
|
:title="$locale.baseText('nodeCreator.noResults.requestTheNode')"
|
||||||
/>
|
/>
|
||||||
|
@ -38,12 +30,20 @@
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
import { REQUEST_NODE_FORM_URL } from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import NoResultsIcon from './NoResultsIcon.vue';
|
import NoResultsIcon from './NoResultsIcon.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'NoResults',
|
name: 'NoResults',
|
||||||
|
props: {
|
||||||
|
showRequest: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
NoResultsIcon,
|
NoResultsIcon,
|
||||||
},
|
},
|
||||||
|
@ -52,20 +52,11 @@ export default Vue.extend({
|
||||||
REQUEST_NODE_FORM_URL,
|
REQUEST_NODE_FORM_URL,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
selectWebhook() {
|
|
||||||
this.$emit('nodeTypeSelected', WEBHOOK_NODE_TYPE);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectHttpRequest() {
|
|
||||||
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_TYPE);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.no-results {
|
.noResults {
|
||||||
background-color: $node-creator-no-results-background-color;
|
background-color: $node-creator-no-results-background-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -75,27 +66,27 @@ export default Vue.extend({
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
padding: 0 50px;
|
padding: 0 var(--spacing-2xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 22px;
|
font-size: var(--font-size-m);
|
||||||
line-height: 22px;
|
line-height: var(--font-line-height-regular);
|
||||||
margin-top: 50px;
|
margin-top: var(--spacing-xs);
|
||||||
|
|
||||||
div {
|
div {
|
||||||
margin-bottom: 15px;
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action, .request {
|
.action, .request {
|
||||||
font-size: 14px;
|
font-size: var(--font-size-s);
|
||||||
line-height: 19px;
|
line-height: var(--font-line-height-compact);
|
||||||
}
|
}
|
||||||
|
|
||||||
.request {
|
.request {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: var(--spacing-m);
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media (min-height: 550px) {
|
@media (min-height: 550px) {
|
||||||
|
@ -104,13 +95,13 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-top: 100px;
|
margin-top: var(--spacing-2xl);
|
||||||
min-height: 67px;
|
min-height: 67px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.external {
|
.external {
|
||||||
font-size: 12px;
|
font-size: var(--font-size-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<SlideTransition>
|
<div>
|
||||||
<div
|
<aside :class="{'node-creator-scrim': true, expanded: !sidebarMenuCollapsed, active: showScrim}" />
|
||||||
v-if="active"
|
|
||||||
class="node-creator"
|
<slide-transition>
|
||||||
ref="nodeCreator"
|
<div
|
||||||
v-click-outside="onClickOutside"
|
v-if="active"
|
||||||
@dragover="onDragOver"
|
class="node-creator"
|
||||||
@drop="onDrop"
|
ref="nodeCreator"
|
||||||
>
|
v-click-outside="onClickOutside"
|
||||||
<MainPanel
|
@dragover="onDragOver"
|
||||||
@nodeTypeSelected="nodeTypeSelected"
|
@drop="onDrop"
|
||||||
:categorizedItems="categorizedItems"
|
>
|
||||||
:categoriesWithNodes="categoriesWithNodes"
|
<main-panel
|
||||||
:searchItems="searchItems"
|
@nodeTypeSelected="nodeTypeSelected"
|
||||||
/>
|
:searchItems="searchItems"
|
||||||
</div>
|
/>
|
||||||
</SlideTransition>
|
</div>
|
||||||
|
</slide-transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
import { ICategoriesWithNodes, INodeCreateElement } from '@/Interface';
|
import { INodeCreateElement } from '@/Interface';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import SlideTransition from '../../transitions/SlideTransition.vue';
|
import SlideTransition from '../../transitions/SlideTransition.vue';
|
||||||
|
|
||||||
import MainPanel from './MainPanel.vue';
|
import MainPanel from './MainPanel.vue';
|
||||||
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'NodeCreator',
|
name: 'NodeCreator',
|
||||||
|
@ -40,20 +40,16 @@ export default Vue.extend({
|
||||||
active: {
|
active: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('users', ['personalizedNodeTypes']),
|
showScrim(): boolean {
|
||||||
allLatestNodeTypes(): INodeTypeDescription[] {
|
return this.$store.getters['nodeCreator/showScrim'];
|
||||||
return this.$store.getters['nodeTypes/allLatestNodeTypes'];
|
},
|
||||||
|
sidebarMenuCollapsed(): boolean {
|
||||||
|
return this.$store.getters['ui/sidebarMenuCollapsed'];
|
||||||
},
|
},
|
||||||
visibleNodeTypes(): INodeTypeDescription[] {
|
visibleNodeTypes(): INodeTypeDescription[] {
|
||||||
return this.allLatestNodeTypes.filter((nodeType) => !nodeType.hidden);
|
return this.$store.getters['nodeTypes/visibleNodeTypes'];
|
||||||
},
|
|
||||||
categoriesWithNodes(): ICategoriesWithNodes {
|
|
||||||
return getCategoriesWithNodes(this.visibleNodeTypes, this.personalizedNodeTypes as string[]);
|
|
||||||
},
|
|
||||||
categorizedItems(): INodeCreateElement[] {
|
|
||||||
return getCategorizedList(this.categoriesWithNodes);
|
|
||||||
},
|
},
|
||||||
searchItems(): INodeCreateElement[] {
|
searchItems(): INodeCreateElement[] {
|
||||||
const sorted = [...this.visibleNodeTypes];
|
const sorted = [...this.visibleNodeTypes];
|
||||||
|
@ -102,6 +98,11 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
active(isActive) {
|
||||||
|
if(isActive === false) this.$store.commit('nodeCreator/setShowScrim', false);
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -113,20 +114,31 @@ export default Vue.extend({
|
||||||
.node-creator {
|
.node-creator {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: $header-height;
|
top: $header-height;
|
||||||
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: $node-creator-width;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $node-creator-background-color;
|
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
|
width: $node-creator-width;
|
||||||
color: $node-creator-text-color;
|
color: $node-creator-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
.node-creator-scrim {
|
||||||
box-sizing: border-box;
|
position: fixed;
|
||||||
content: ' ';
|
top: $header-height;
|
||||||
border-left: 1px solid $node-creator-border-color;
|
right: 0;
|
||||||
width: 1px;
|
bottom: 0;
|
||||||
position: absolute;
|
left: $sidebar-width;
|
||||||
height: 100%;
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--color-background-dark);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
left: $sidebar-expanded-width
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
draggable
|
draggable
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
:class="{[$style['node-item']]: true, [$style.bordered]: bordered}"
|
:class="{[$style['node-item']]: true}"
|
||||||
>
|
>
|
||||||
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
|
<node-icon :class="$style['node-icon']" :nodeType="nodeType" />
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.details">
|
<div :class="$style.details">
|
||||||
<span :class="$style.name">
|
<span :class="$style.name">
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="isTrigger" :class="$style['trigger-icon']">
|
<span v-if="isTrigger" :class="$style['trigger-icon']">
|
||||||
<TriggerIcon />
|
<trigger-icon />
|
||||||
</span>
|
</span>
|
||||||
<n8n-tooltip v-if="isCommunityNode" placement="top">
|
<n8n-tooltip v-if="isCommunityNode" placement="top">
|
||||||
<div
|
<div
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
ref="draggable"
|
ref="draggable"
|
||||||
v-show="dragging"
|
v-show="dragging"
|
||||||
>
|
>
|
||||||
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" />
|
<node-icon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,26 +54,29 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import {getNewNodePosition, NODE_SIZE} from '@/views/canvasHelpers';
|
import Vue, { PropType } from 'vue';
|
||||||
import Vue from 'vue';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
import NodeIcon from '../../NodeIcon.vue';
|
|
||||||
import TriggerIcon from '../../TriggerIcon.vue';
|
|
||||||
|
|
||||||
|
import { getNewNodePosition, NODE_SIZE } from '@/views/canvasHelpers';
|
||||||
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
|
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
|
||||||
import { isCommunityPackageName } from '../../helpers';
|
|
||||||
|
|
||||||
Vue.component('NodeIcon', NodeIcon);
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
Vue.component('TriggerIcon', TriggerIcon);
|
import TriggerIcon from '@/components/TriggerIcon.vue';
|
||||||
|
import { isCommunityPackageName } from '@/components/helpers';
|
||||||
|
|
||||||
|
Vue.component('node-icon', NodeIcon);
|
||||||
|
Vue.component('trigger-icon', TriggerIcon);
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'NodeItem',
|
name: 'NodeItem',
|
||||||
props: [
|
props: {
|
||||||
'active',
|
nodeType: {
|
||||||
'filter',
|
type: Object as PropType<INodeTypeDescription>,
|
||||||
'nodeType',
|
},
|
||||||
'bordered',
|
active: {
|
||||||
],
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dragging: false,
|
dragging: false,
|
||||||
|
@ -160,10 +163,7 @@ export default Vue.extend({
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
cursor: grab;
|
||||||
&.bordered {
|
|
||||||
border-bottom: 1px solid $node-creator-border-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
@ -177,7 +177,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: bold;
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
@ -189,7 +189,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 11px;
|
font-size: var(--font-size-2xs);
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $node-creator-description-color;
|
color: $node-creator-description-color;
|
||||||
|
|
|
@ -1,40 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="search-container">
|
<div :class="$style.searchContainer">
|
||||||
<div :class="{ prefix: true, active: value.length > 0 }">
|
<div :class="{ [$style.prefix]: true, [$style.active]: value.length > 0 }">
|
||||||
<font-awesome-icon icon="search" />
|
<font-awesome-icon icon="search" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div :class="$style.text">
|
||||||
<input
|
<input
|
||||||
:placeholder="$locale.baseText('nodeCreator.searchBar.searchNodes')"
|
:placeholder="$locale.baseText('nodeCreator.searchBar.searchNodes')"
|
||||||
ref="input"
|
ref="input"
|
||||||
:value="value"
|
:value="value"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
:class="$style.input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="suffix" v-if="value.length > 0" @click="clear">
|
<div :class="$style.suffix" v-if="value.length > 0" @click="clear">
|
||||||
<span class="clear el-icon-close clickable"></span>
|
<button :class="[$style.clear, $style.clickable]">
|
||||||
|
<font-awesome-icon icon="times-circle" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
|
||||||
export default mixins(externalHooks).extend({
|
export default mixins(externalHooks).extend({
|
||||||
name: "SearchBar",
|
name: "SearchBar",
|
||||||
props: ["value", "eventBus"],
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
eventBus: {
|
||||||
|
type: Object as PropType<Vue>,
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.$props.eventBus) {
|
if (this.eventBus) {
|
||||||
this.$props.eventBus.$on("focus", () => {
|
this.eventBus.$on("focus", this.focus);
|
||||||
this.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(this.focus, 0);
|
||||||
this.focus();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
this.$externalHooks().run('nodeCreator_searchBar.mount', { inputRef: this.$refs['input'] });
|
this.$externalHooks().run('nodeCreator_searchBar.mount', { inputRef: this.$refs['input'] });
|
||||||
},
|
},
|
||||||
|
@ -53,25 +59,37 @@ export default mixins(externalHooks).extend({
|
||||||
this.$emit("input", "");
|
this.$emit("input", "");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.eventBus) {
|
||||||
|
this.eventBus.$off("focus", this.focus);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.search-container {
|
.searchContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 60px;
|
height: 40px;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 14px;
|
margin: var(--spacing-s);
|
||||||
padding-right: 20px;
|
filter: drop-shadow(0px 2px 5px rgba(46, 46, 50, 0.04));
|
||||||
border-bottom: 1px solid $node-creator-border-color;
|
|
||||||
|
border: 1px solid $node-creator-border-color;
|
||||||
background-color: $node-creator-search-background-color;
|
background-color: $node-creator-search-background-color;
|
||||||
color: $node-creator-search-placeholder-color;
|
color: $node-creator-search-placeholder-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--color-secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefix {
|
.prefix {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 16px;
|
font-size: var(--font-size-m);
|
||||||
margin-right: 14px;
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $color-primary !important;
|
color: $color-primary !important;
|
||||||
|
@ -83,10 +101,10 @@ export default mixins(externalHooks).extend({
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none !important;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 18px;
|
font-size: var(--font-size-s);
|
||||||
-webkit-appearance: none;
|
appearance: none;
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
|
|
||||||
|
@ -99,32 +117,22 @@ export default mixins(externalHooks).extend({
|
||||||
|
|
||||||
.suffix {
|
.suffix {
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
text-align: center;
|
text-align: right;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
background-color: $node-creator-search-clear-background-color;
|
background-color: $node-creator-search-clear-color;
|
||||||
border-radius: 50%;
|
padding: 0;
|
||||||
height: 16px;
|
border: none;
|
||||||
width: 16px;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
|
||||||
color: $node-creator-search-background-color;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
svg path {
|
||||||
background-color: $node-creator-search-clear-background-color-hover;
|
fill: $node-creator-search-clear-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:hover svg path {
|
||||||
line-height: 16px;
|
fill: $node-creator-search-clear-background-color-hover;
|
||||||
display: flex;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
font-size: 15px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.subcategory">
|
<div :class="{[$style.subcategory]: true, [$style.subcategoryWithIcon]: hasIcon}">
|
||||||
|
<node-icon v-if="hasIcon" :class="$style.subcategoryIcon" :nodeType="itemProperties" />
|
||||||
<div :class="$style.details">
|
<div :class="$style.details">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||||
|
@ -15,14 +16,30 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue, { PropType } from 'vue';
|
||||||
import camelcase from 'lodash.camelcase';
|
import camelcase from 'lodash.camelcase';
|
||||||
|
|
||||||
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import { INodeCreateElement, ISubcategoryItemProps } from '@/Interface';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['item'],
|
components: {
|
||||||
|
NodeIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<INodeCreateElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
subcategoryName() {
|
itemProperties() : ISubcategoryItemProps {
|
||||||
return camelcase(this.item.properties.subcategory);
|
return this.item.properties as ISubcategoryItemProps;
|
||||||
|
},
|
||||||
|
subcategoryName(): string {
|
||||||
|
return camelcase(this.itemProperties.subcategory);
|
||||||
|
},
|
||||||
|
hasIcon(): boolean {
|
||||||
|
return this.itemProperties.icon !== undefined || this.itemProperties.iconData !== undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -30,11 +47,23 @@ export default Vue.extend({
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.subcategoryIcon {
|
||||||
|
min-width: 26px;
|
||||||
|
max-width: 26px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.subcategory {
|
.subcategory {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 11px 16px 11px 30px;
|
padding: 11px 16px 11px 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subcategoryWithIcon {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 12px;
|
||||||
|
padding: 11px 8px 11px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
@ -42,13 +71,13 @@ export default Vue.extend({
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: var(--font-weight-bold);
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 11px;
|
font-size: var(--font-size-2xs);
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $node-creator-description-color;
|
color: $node-creator-description-color;
|
||||||
|
@ -57,6 +86,7 @@ export default Vue.extend({
|
||||||
.action {
|
.action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="subcategory-panel">
|
|
||||||
<div class="subcategory-header">
|
|
||||||
<div class="clickable" @click="onBackArrowClick">
|
|
||||||
<font-awesome-icon class="back-arrow" icon="arrow-left" />
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scrollable">
|
|
||||||
<ItemIterator
|
|
||||||
:elements="elements"
|
|
||||||
:activeIndex="activeIndex"
|
|
||||||
@selected="$emit('selected', $event)"
|
|
||||||
@dragstart="$emit('dragstart', $event)"
|
|
||||||
@dragend="$emit('dragend', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import camelcase from 'lodash.camelcase';
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
import ItemIterator from './ItemIterator.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'SubcategoryPanel',
|
|
||||||
components: {
|
|
||||||
ItemIterator,
|
|
||||||
},
|
|
||||||
props: ['title', 'elements', 'activeIndex'],
|
|
||||||
computed: {
|
|
||||||
subcategoryName() {
|
|
||||||
return camelcase(this.title);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onBackArrowClick() {
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.subcategory-panel {
|
|
||||||
position: absolute;
|
|
||||||
background: $node-creator-search-background-color;
|
|
||||||
z-index: 100;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
box-sizing: border-box;
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid $node-creator-border-color;
|
|
||||||
width: 1px;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategory-header {
|
|
||||||
border: $node-creator-border-color solid 1px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: $node-creator-subcategory-panel-header-bacground-color;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 11px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-arrow {
|
|
||||||
color: $node-creator-arrow-color;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: visible;
|
|
||||||
height: calc(100% - 100px);
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
padding-bottom: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
<template>
|
||||||
|
<div :class="{ [$style.triggerHelperContainer]: true, [$style.isRoot]: isRoot }">
|
||||||
|
<categorized-items
|
||||||
|
ref="categorizedItems"
|
||||||
|
@subcategoryClose="onSubcategoryClose"
|
||||||
|
@onSubcategorySelected="onSubcategorySelected"
|
||||||
|
@nodeTypeSelected="nodeType => $emit('nodeTypeSelected', nodeType)"
|
||||||
|
:initialActiveIndex="0"
|
||||||
|
:searchItems="searchItems"
|
||||||
|
:firstLevelItems="isRoot ? items : []"
|
||||||
|
:excludedCategories="[CORE_NODES_CATEGORY]"
|
||||||
|
:initialActiveCategories="[COMMUNICATION_CATEGORY]"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<slot name="header" />
|
||||||
|
<p v-if="isRoot" v-text="$locale.baseText('nodeCreator.triggerHelperPanel.title')" :class="$style.title" />
|
||||||
|
</template>
|
||||||
|
</categorized-items>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
import { INodeCreateElement } from '@/Interface';
|
||||||
|
import { CORE_NODES_CATEGORY, CRON_NODE_TYPE, WEBHOOK_NODE_TYPE, OTHER_TRIGGER_NODES_SUBCATEGORY, EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, COMMUNICATION_CATEGORY } from '@/constants';
|
||||||
|
|
||||||
|
import ItemIterator from './ItemIterator.vue';
|
||||||
|
import CategorizedItems from './CategorizedItems.vue';
|
||||||
|
import SearchBar from './SearchBar.vue';
|
||||||
|
|
||||||
|
export default mixins(externalHooks).extend({
|
||||||
|
name: 'TriggerHelperPanel',
|
||||||
|
components: {
|
||||||
|
ItemIterator,
|
||||||
|
CategorizedItems,
|
||||||
|
SearchBar,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
searchItems: {
|
||||||
|
type: Array as PropType<INodeCreateElement[] | null>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
CORE_NODES_CATEGORY,
|
||||||
|
COMMUNICATION_CATEGORY,
|
||||||
|
isRoot: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
items() {
|
||||||
|
return [{
|
||||||
|
key: "core_nodes",
|
||||||
|
type: "subcategory",
|
||||||
|
title: this.$locale.baseText('nodeCreator.subcategoryNames.appTriggerNodes'),
|
||||||
|
properties: {
|
||||||
|
subcategory: "App Trigger Nodes",
|
||||||
|
description: this.$locale.baseText('nodeCreator.subcategoryDescriptions.appTriggerNodes'),
|
||||||
|
icon: "fa:satellite-dish",
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CRON_NODE_TYPE,
|
||||||
|
type: "node",
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
|
||||||
|
group: [],
|
||||||
|
name: CRON_NODE_TYPE,
|
||||||
|
displayName: this.$locale.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName'),
|
||||||
|
description: this.$locale.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDescription'),
|
||||||
|
icon: "fa:clock",
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: WEBHOOK_NODE_TYPE,
|
||||||
|
type: "node",
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: WEBHOOK_NODE_TYPE,
|
||||||
|
displayName: this.$locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
|
||||||
|
description: this.$locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
|
||||||
|
iconData: {
|
||||||
|
type: "file",
|
||||||
|
icon: "webhook",
|
||||||
|
fileBuffer: "/static/webhook-icon.svg",
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
type: "node",
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
displayName: this.$locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
|
||||||
|
description: this.$locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
|
||||||
|
icon: "fa:mouse-pointer",
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
type: "node",
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
displayName: this.$locale.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDisplayName'),
|
||||||
|
description: this.$locale.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDescription'),
|
||||||
|
icon: "fa:sign-out-alt",
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "subcategory",
|
||||||
|
key: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
|
description: this.$locale.baseText('nodeCreator.subcategoryDescriptions.otherTriggerNodes'),
|
||||||
|
icon: "fa:folder-open",
|
||||||
|
defaults: {
|
||||||
|
color: "#7D838F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isRootSubcategory(subcategory: INodeCreateElement) {
|
||||||
|
return this.items.find(item => item.key === subcategory.key) !== undefined;
|
||||||
|
},
|
||||||
|
onSubcategorySelected() {
|
||||||
|
this.isRoot = false;
|
||||||
|
},
|
||||||
|
onSubcategoryClose(subcategory: INodeCreateElement) {
|
||||||
|
this.isRoot = this.isRootSubcategory(subcategory);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.triggerHelperContainer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Remove node item border on the root level
|
||||||
|
&.isRoot {
|
||||||
|
--node-item-border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.itemCreator {
|
||||||
|
height: calc(100% - 120px);
|
||||||
|
padding-top: 1px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
padding: var(--spacing-s) var(--spacing-s) var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="type-selector" v-if="showTabs">
|
||||||
|
<el-tabs stretch :value="selectedType" @input="setType">
|
||||||
|
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||||
|
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||||
|
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ALL_NODE_FILTER, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'NodeCreateTypeSelector',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
|
ALL_NODE_FILTER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setType(type: string) {
|
||||||
|
this.$store.commit('nodeCreator/setSelectedType', type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showTabs(): boolean {
|
||||||
|
return this.$store.getters['nodeCreator/showTabs'];
|
||||||
|
},
|
||||||
|
selectedType(): string {
|
||||||
|
return this.$store.getters['nodeCreator/selectedType'];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .el-tabs__item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-tabs__active-bar {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-tabs__nav-wrap::after {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-selector {
|
||||||
|
text-align: center;
|
||||||
|
background-color: $node-creator-select-background-color;
|
||||||
|
|
||||||
|
::v-deep .el-tabs > div {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.el-tabs__nav {
|
||||||
|
height: 43px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,145 +1,7 @@
|
||||||
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER, PERSONALIZED_CATEGORY } from '@/constants';
|
import { REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER } from '@/constants';
|
||||||
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
|
import { INodeCreateElement, INodeItemProps } from '@/Interface';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
|
|
||||||
if (!accu[category]) {
|
|
||||||
accu[category] = {};
|
|
||||||
}
|
|
||||||
if (!accu[category][subcategory]) {
|
|
||||||
accu[category][subcategory] = {
|
|
||||||
triggerCount: 0,
|
|
||||||
regularCount: 0,
|
|
||||||
nodes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const isTrigger = nodeType.group.includes('trigger');
|
|
||||||
if (isTrigger) {
|
|
||||||
accu[category][subcategory].triggerCount++;
|
|
||||||
}
|
|
||||||
if (!isTrigger) {
|
|
||||||
accu[category][subcategory].regularCount++;
|
|
||||||
}
|
|
||||||
accu[category][subcategory].nodes.push({
|
|
||||||
type: 'node',
|
|
||||||
key: `${category}_${nodeType.name}`,
|
|
||||||
category,
|
|
||||||
properties: {
|
|
||||||
nodeType,
|
|
||||||
subcategory,
|
|
||||||
},
|
|
||||||
includedByTrigger: isTrigger,
|
|
||||||
includedByRegular: !isTrigger,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
|
|
||||||
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
|
|
||||||
return sorted.reduce(
|
|
||||||
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
|
||||||
if (personalizedNodeTypes.includes(nodeType.name)) {
|
|
||||||
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nodeType.codex || !nodeType.codex.categories) {
|
|
||||||
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeType.codex.categories.forEach((_category: string) => {
|
|
||||||
const category = _category.trim();
|
|
||||||
const subcategory =
|
|
||||||
nodeType.codex &&
|
|
||||||
nodeType.codex.subcategories &&
|
|
||||||
nodeType.codex.subcategories[category]
|
|
||||||
? nodeType.codex.subcategories[category][0]
|
|
||||||
: UNCATEGORIZED_SUBCATEGORY;
|
|
||||||
|
|
||||||
addNodeToCategory(accu, nodeType, category, subcategory);
|
|
||||||
});
|
|
||||||
return accu;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
|
||||||
const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
|
|
||||||
const categories = Object.keys(categoriesWithNodes);
|
|
||||||
const sorted = categories.filter(
|
|
||||||
(category: string) =>
|
|
||||||
!excludeFromSort.includes(category),
|
|
||||||
);
|
|
||||||
sorted.sort();
|
|
||||||
|
|
||||||
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
|
||||||
const categories = getCategories(categoriesWithNodes);
|
|
||||||
|
|
||||||
return categories.reduce(
|
|
||||||
(accu: INodeCreateElement[], category: string) => {
|
|
||||||
if (!categoriesWithNodes[category]) {
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryEl: INodeCreateElement = {
|
|
||||||
type: 'category',
|
|
||||||
key: category,
|
|
||||||
category,
|
|
||||||
properties: {
|
|
||||||
expanded: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const subcategories = Object.keys(categoriesWithNodes[category]);
|
|
||||||
if (subcategories.length === 1) {
|
|
||||||
const subcategory = categoriesWithNodes[category][
|
|
||||||
subcategories[0]
|
|
||||||
];
|
|
||||||
if (subcategory.triggerCount > 0) {
|
|
||||||
categoryEl.includedByTrigger = subcategory.triggerCount > 0;
|
|
||||||
}
|
|
||||||
if (subcategory.regularCount > 0) {
|
|
||||||
categoryEl.includedByRegular = subcategory.regularCount > 0;
|
|
||||||
}
|
|
||||||
return [...accu, categoryEl, ...subcategory.nodes];
|
|
||||||
}
|
|
||||||
|
|
||||||
subcategories.sort();
|
|
||||||
const subcategorized = subcategories.reduce(
|
|
||||||
(accu: INodeCreateElement[], subcategory: string) => {
|
|
||||||
const subcategoryEl: INodeCreateElement = {
|
|
||||||
type: 'subcategory',
|
|
||||||
key: `${category}_${subcategory}`,
|
|
||||||
category,
|
|
||||||
properties: {
|
|
||||||
subcategory,
|
|
||||||
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
|
|
||||||
},
|
|
||||||
includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0,
|
|
||||||
includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (subcategoryEl.includedByTrigger) {
|
|
||||||
categoryEl.includedByTrigger = true;
|
|
||||||
}
|
|
||||||
if (subcategoryEl.includedByRegular) {
|
|
||||||
categoryEl.includedByRegular = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
accu.push(subcategoryEl);
|
|
||||||
return accu;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...accu, categoryEl, ...subcategorized];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => {
|
export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => {
|
||||||
if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) {
|
if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WEBHOOK_NODE_TYPE } from '@/constants';
|
import { WEBHOOK_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||||
import { INodeUi } from '@/Interface';
|
import { INodeUi } from '@/Interface';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
@ -72,7 +72,10 @@ export default mixins(
|
||||||
return this.$store.getters.isActionActive('workflowRunning');
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
},
|
},
|
||||||
isTriggerNode (): boolean {
|
isTriggerNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
return this.$store.getters['nodeTypes/isTriggerNode'](this.node.type);
|
||||||
|
},
|
||||||
|
isManualTriggerNode (): boolean {
|
||||||
|
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
|
||||||
},
|
},
|
||||||
isPollingTypeNode (): boolean {
|
isPollingTypeNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.polling);
|
return !!(this.nodeType && this.nodeType.polling);
|
||||||
|
@ -138,7 +141,7 @@ export default mixins(
|
||||||
return this.$locale.baseText('ndv.execute.fetchEvent');
|
return this.$locale.baseText('ndv.execute.fetchEvent');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isTriggerNode && !this.isScheduleTrigger) {
|
if (this.isTriggerNode && !this.isScheduleTrigger && !this.isManualTriggerNode) {
|
||||||
return this.$locale.baseText('ndv.execute.listenForEvent');
|
return this.$locale.baseText('ndv.execute.listenForEvent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ export default mixins(
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
isTriggerNode (): boolean {
|
isTriggerNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
return this.$store.getters['nodeTypes/isTriggerNode'](this.node.type);
|
||||||
},
|
},
|
||||||
isPollingTypeNode (): boolean {
|
isPollingTypeNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.polling);
|
return !!(this.nodeType && this.nodeType.polling);
|
||||||
|
@ -150,6 +150,8 @@ export default mixins(
|
||||||
return executionData.resultData.runData;
|
return executionData.resultData.runData;
|
||||||
},
|
},
|
||||||
hasNodeRun(): boolean {
|
hasNodeRun(): boolean {
|
||||||
|
if (this.$store.getters.subworkflowExecutionError) return true;
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name),
|
this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name),
|
||||||
);
|
);
|
||||||
|
|
|
@ -158,6 +158,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="paneType === 'output' && hasSubworkflowExecutionError" :class="$style.stretchVertically">
|
||||||
|
<NodeErrorView :error="subworkflowExecutionError" :class="$style.errorDisplay" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!hasNodeRun" :class="$style.center">
|
<div v-else-if="!hasNodeRun" :class="$style.center">
|
||||||
<slot name="node-not-run"></slot>
|
<slot name="node-not-run"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -499,7 +503,7 @@ export default mixins(
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
isTriggerNode (): boolean {
|
isTriggerNode (): boolean {
|
||||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
return this.$store.getters['nodeTypes/isTriggerNode'](this.node.type);
|
||||||
},
|
},
|
||||||
canPinData (): boolean {
|
canPinData (): boolean {
|
||||||
return !this.isPaneTypeInput &&
|
return !this.isPaneTypeInput &&
|
||||||
|
@ -522,6 +526,12 @@ export default mixins(
|
||||||
hasNodeRun(): boolean {
|
hasNodeRun(): boolean {
|
||||||
return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData));
|
return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData));
|
||||||
},
|
},
|
||||||
|
subworkflowExecutionError(): Error | null {
|
||||||
|
return this.$store.getters.subworkflowExecutionError;
|
||||||
|
},
|
||||||
|
hasSubworkflowExecutionError(): boolean {
|
||||||
|
return Boolean(this.subworkflowExecutionError);
|
||||||
|
},
|
||||||
hasRunError(): boolean {
|
hasRunError(): boolean {
|
||||||
return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error);
|
return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error);
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:loading="isSaving"
|
:loading="isSaving"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="$style.button"
|
:class="$style.button"
|
||||||
|
:type="type"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -36,6 +37,10 @@ export default Vue.extend({
|
||||||
savedLabel: {
|
savedLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
saveButtonLabel() {
|
saveButtonLabel() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER } from '@/constants';
|
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER, NON_ACTIVATABLE_TRIGGER_NODE_TYPES } from '@/constants';
|
||||||
import { INodeUi, ITemplatesNode } from '@/Interface';
|
import { INodeUi, ITemplatesNode } from '@/Interface';
|
||||||
import { isResourceLocatorValue } from '@/typeGuards';
|
import { isResourceLocatorValue } from '@/typeGuards';
|
||||||
import dateformat from 'dateformat';
|
import dateformat from 'dateformat';
|
||||||
|
@ -49,10 +49,7 @@ export function getTriggerNodeServiceName(nodeType: INodeTypeDescription): strin
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActivatableTriggerNodes(nodes: INodeUi[]) {
|
export function getActivatableTriggerNodes(nodes: INodeUi[]) {
|
||||||
return nodes.filter((node: INodeUi) => {
|
return nodes.filter((node: INodeUi) => !node.disabled && !NON_ACTIVATABLE_TRIGGER_NODE_TYPES.includes(node.type));
|
||||||
// Error Trigger does not behave like other triggers and workflows using it can not be activated
|
|
||||||
return !node.disabled && node.type !== ERROR_TRIGGER_NODE_TYPE;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterTemplateNodes(nodes: ITemplatesNode[]) {
|
export function filterTemplateNodes(nodes: ITemplatesNode[]) {
|
||||||
|
|
|
@ -30,6 +30,9 @@
|
||||||
registerCustomAction(key: string, action: Function) {
|
registerCustomAction(key: string, action: Function) {
|
||||||
this.customActions[key] = action;
|
this.customActions[key] = action;
|
||||||
},
|
},
|
||||||
|
unregisterCustomAction(key: string) {
|
||||||
|
Vue.delete(this.customActions, key);
|
||||||
|
},
|
||||||
delegateClick(e: MouseEvent) {
|
delegateClick(e: MouseEvent) {
|
||||||
const clickedElement = e.target;
|
const clickedElement = e.target;
|
||||||
if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
|
if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
|
||||||
|
|
|
@ -29,13 +29,31 @@ export const nodeBase = mixins(
|
||||||
return this.data.id;
|
return this.data.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: [
|
props: {
|
||||||
'name',
|
name: {
|
||||||
'instance',
|
type: String,
|
||||||
'isReadOnly',
|
},
|
||||||
'isActive',
|
instance: {
|
||||||
'hideActions',
|
// We can't use PropType<jsPlumbInstance> here because the version of jsplumb doesn't
|
||||||
],
|
// include correct typing for draggable instance(`clearDragSelection`, `destroyDraggable`, etc.)
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
isReadOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
hideActions: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
disableSelecting: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
showCustomTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||||
// Add Inputs
|
// Add Inputs
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
|
SubworkflowOperationError,
|
||||||
TelemetryHelpers,
|
TelemetryHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -291,19 +292,34 @@ export const pushConnection = mixins(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let title: string;
|
if (runDataExecuted.data.resultData.error?.name === 'SubworkflowOperationError') {
|
||||||
if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
const error = runDataExecuted.data.resultData.error as SubworkflowOperationError;
|
||||||
title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
|
|
||||||
|
this.$store.commit('setSubworkflowExecutionError', error);
|
||||||
|
|
||||||
|
this.$showMessage({
|
||||||
|
title: error.message,
|
||||||
|
message: error.description,
|
||||||
|
type: 'error',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
title = 'Problem executing workflow';
|
let title: string;
|
||||||
|
if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
||||||
|
title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
|
||||||
|
} else {
|
||||||
|
title = 'Problem executing workflow';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$showMessage({
|
||||||
|
title,
|
||||||
|
message: runDataExecutedErrorMessage,
|
||||||
|
type: 'error',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$showMessage({
|
|
||||||
title,
|
|
||||||
message: runDataExecutedErrorMessage,
|
|
||||||
type: 'error',
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Workflow did execute without a problem
|
// Workflow did execute without a problem
|
||||||
this.$titleSet(workflow.name as string, 'IDLE');
|
this.$titleSet(workflow.name as string, 'IDLE');
|
||||||
|
|
|
@ -38,6 +38,8 @@ export const workflowRun = mixins(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$store.commit('setSubworkflowExecutionError', null);
|
||||||
|
|
||||||
this.$store.commit('addActiveAction', 'workflowRunning');
|
this.$store.commit('addActiveAction', 'workflowRunning');
|
||||||
|
|
||||||
let response: IExecutionPushResponse;
|
let response: IExecutionPushResponse;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<slot></slot>
|
<slot />
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -21,4 +21,5 @@ export default Vue.extend({
|
||||||
.slide-enter {
|
.slide-enter {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
</style>
|
||||||
|
|
|
@ -92,6 +92,7 @@ export const ITEM_LISTS_NODE_TYPE = 'n8n-nodes-base.itemLists';
|
||||||
export const JIRA_NODE_TYPE = 'n8n-nodes-base.jira';
|
export const JIRA_NODE_TYPE = 'n8n-nodes-base.jira';
|
||||||
export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
|
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||||
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
||||||
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||||
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||||
|
@ -110,11 +111,19 @@ export const THE_HIVE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.theHiveTrigger';
|
||||||
export const QUICKBOOKS_NODE_TYPE = 'n8n-nodes-base.quickbooks';
|
export const QUICKBOOKS_NODE_TYPE = 'n8n-nodes-base.quickbooks';
|
||||||
export const WEBHOOK_NODE_TYPE = 'n8n-nodes-base.webhook';
|
export const WEBHOOK_NODE_TYPE = 'n8n-nodes-base.webhook';
|
||||||
export const WORKABLE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.workableTrigger';
|
export const WORKABLE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.workableTrigger';
|
||||||
|
export const WORKFLOW_TRIGGER_NODE_TYPE = 'n8n-nodes-base.workflowTrigger';
|
||||||
|
export const EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE = 'n8n-nodes-base.executeWorkflowTrigger';
|
||||||
export const WOOCOMMERCE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.wooCommerceTrigger';
|
export const WOOCOMMERCE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.wooCommerceTrigger';
|
||||||
export const XERO_NODE_TYPE = 'n8n-nodes-base.xero';
|
export const XERO_NODE_TYPE = 'n8n-nodes-base.xero';
|
||||||
export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
|
export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
|
||||||
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
|
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
|
||||||
|
|
||||||
|
export const NON_ACTIVATABLE_TRIGGER_NODE_TYPES = [
|
||||||
|
ERROR_TRIGGER_NODE_TYPE,
|
||||||
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
];
|
||||||
|
|
||||||
export const MULTIPLE_OUTPUT_NODE_TYPES = [
|
export const MULTIPLE_OUTPUT_NODE_TYPES = [
|
||||||
IF_NODE_TYPE,
|
IF_NODE_TYPE,
|
||||||
SWITCH_NODE_TYPE,
|
SWITCH_NODE_TYPE,
|
||||||
|
@ -127,6 +136,7 @@ export const PIN_DATA_NODE_TYPES_DENYLIST = [
|
||||||
|
|
||||||
// Node creator
|
// Node creator
|
||||||
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||||
|
export const COMMUNICATION_CATEGORY = 'Communication';
|
||||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
export const SUBCATEGORY_DESCRIPTIONS: {
|
export const SUBCATEGORY_DESCRIPTIONS: {
|
||||||
[category: string]: { [subcategory: string]: string };
|
[category: string]: { [subcategory: string]: string };
|
||||||
|
@ -144,6 +154,7 @@ export const ALL_NODE_FILTER = 'All';
|
||||||
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
||||||
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
||||||
export const PERSONALIZED_CATEGORY = 'Suggested Nodes';
|
export const PERSONALIZED_CATEGORY = 'Suggested Nodes';
|
||||||
|
export const OTHER_TRIGGER_NODES_SUBCATEGORY = 'Other Trigger Nodes';
|
||||||
|
|
||||||
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||||
|
|
||||||
|
@ -262,6 +273,11 @@ export const HIRING_BANNER = `
|
||||||
Love n8n? Help us build the future of automation! https://n8n.io/careers
|
Love n8n? Help us build the future of automation! https://n8n.io/careers
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const NODE_TYPE_COUNT_MAPPER = {
|
||||||
|
[REGULAR_NODE_FILTER]: ['regularCount'],
|
||||||
|
[TRIGGER_NODE_FILTER]: ['triggerCount'],
|
||||||
|
[ALL_NODE_FILTER]: ['triggerCount', 'regularCount'],
|
||||||
|
};
|
||||||
export const TEMPLATES_NODES_FILTER = [
|
export const TEMPLATES_NODES_FILTER = [
|
||||||
'n8n-nodes-base.start',
|
'n8n-nodes-base.start',
|
||||||
'n8n-nodes-base.respondToWebhook',
|
'n8n-nodes-base.respondToWebhook',
|
||||||
|
|
39
packages/editor-ui/src/modules/nodeCreator.ts
Normal file
39
packages/editor-ui/src/modules/nodeCreator.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { ALL_NODE_FILTER } from '@/constants';
|
||||||
|
import { Module } from 'vuex';
|
||||||
|
import {
|
||||||
|
IRootState,
|
||||||
|
INodeCreatorState,
|
||||||
|
INodeFilterType,
|
||||||
|
} from '@/Interface';
|
||||||
|
|
||||||
|
const module: Module<INodeCreatorState, IRootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
itemsFilter: '',
|
||||||
|
showTabs: true,
|
||||||
|
showScrim: false,
|
||||||
|
selectedType: ALL_NODE_FILTER,
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
showTabs: (state: INodeCreatorState) => state.showTabs,
|
||||||
|
showScrim: (state: INodeCreatorState) => state.showScrim,
|
||||||
|
selectedType: (state: INodeCreatorState) => state.selectedType,
|
||||||
|
itemsFilter: (state: INodeCreatorState) => state.itemsFilter,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setShowTabs(state: INodeCreatorState, isVisible: boolean) {
|
||||||
|
state.showTabs = isVisible;
|
||||||
|
},
|
||||||
|
setShowScrim(state: INodeCreatorState, isVisible: boolean) {
|
||||||
|
state.showScrim = isVisible;
|
||||||
|
},
|
||||||
|
setSelectedType(state: INodeCreatorState, selectedNodeType: INodeFilterType) {
|
||||||
|
state.selectedType = selectedNodeType;
|
||||||
|
},
|
||||||
|
setFilter(state: INodeCreatorState, search: INodeFilterType) {
|
||||||
|
state.itemsFilter = search;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default module;
|
|
@ -19,7 +19,8 @@ import {
|
||||||
getResourceLocatorResults,
|
getResourceLocatorResults,
|
||||||
} from '@/api/nodeTypes';
|
} from '@/api/nodeTypes';
|
||||||
import { omit } from '@/utils';
|
import { omit } from '@/utils';
|
||||||
import type { IRootState, INodeTypesState, IResourceLocatorReqParams } from '../Interface';
|
import type { IRootState, INodeTypesState, ICategoriesWithNodes, INodeCreateElement, IResourceLocatorReqParams } from '../Interface';
|
||||||
|
import { getCategoriesWithNodes, getCategorizedList } from './nodeTypesHelpers';
|
||||||
|
|
||||||
const module: Module<INodeTypesState, IRootState> = {
|
const module: Module<INodeTypesState, IRootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
@ -55,6 +56,19 @@ const module: Module<INodeTypesState, IRootState> = {
|
||||||
|
|
||||||
return nodeType || null;
|
return nodeType || null;
|
||||||
},
|
},
|
||||||
|
isTriggerNode: (state, getters) => (nodeTypeName: string) => {
|
||||||
|
const nodeType = getters.getNodeType(nodeTypeName);
|
||||||
|
return !!(nodeType && nodeType.group.includes('trigger'));
|
||||||
|
},
|
||||||
|
visibleNodeTypes: (state, getters): INodeTypeDescription[] => {
|
||||||
|
return getters.allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => !nodeType.hidden);
|
||||||
|
},
|
||||||
|
categoriesWithNodes: (state, getters, rootState, rootGetters): ICategoriesWithNodes => {
|
||||||
|
return getCategoriesWithNodes(getters.visibleNodeTypes, rootGetters['users/personalizedNodeTypes']);
|
||||||
|
},
|
||||||
|
categorizedItems: (state, getters): INodeCreateElement[] => {
|
||||||
|
return getCategorizedList(getters.categoriesWithNodes);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setNodeTypes(state, newNodeTypes: INodeTypeDescription[] = []) {
|
setNodeTypes(state, newNodeTypes: INodeTypeDescription[] = []) {
|
||||||
|
|
150
packages/editor-ui/src/modules/nodeTypesHelpers.ts
Normal file
150
packages/editor-ui/src/modules/nodeTypesHelpers.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, PERSONALIZED_CATEGORY } from '@/constants';
|
||||||
|
import { INodeCreateElement, ICategoriesWithNodes } from '@/Interface';
|
||||||
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
|
||||||
|
if (!accu[category]) {
|
||||||
|
accu[category] = {};
|
||||||
|
}
|
||||||
|
if (!accu[category][subcategory]) {
|
||||||
|
accu[category][subcategory] = {
|
||||||
|
triggerCount: 0,
|
||||||
|
regularCount: 0,
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const isTrigger = nodeType.group.includes('trigger');
|
||||||
|
if (isTrigger) {
|
||||||
|
accu[category][subcategory].triggerCount++;
|
||||||
|
}
|
||||||
|
if (!isTrigger) {
|
||||||
|
accu[category][subcategory].regularCount++;
|
||||||
|
}
|
||||||
|
accu[category][subcategory].nodes.push({
|
||||||
|
type: 'node',
|
||||||
|
key: `${category}_${nodeType.name}`,
|
||||||
|
category,
|
||||||
|
properties: {
|
||||||
|
nodeType,
|
||||||
|
subcategory,
|
||||||
|
},
|
||||||
|
includedByTrigger: isTrigger,
|
||||||
|
includedByRegular: !isTrigger,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
|
||||||
|
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
|
||||||
|
return sorted.reduce(
|
||||||
|
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
||||||
|
if (personalizedNodeTypes.includes(nodeType.name)) {
|
||||||
|
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeType.codex || !nodeType.codex.categories) {
|
||||||
|
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeType.codex.categories.forEach((_category: string) => {
|
||||||
|
const category = _category.trim();
|
||||||
|
const subcategories =
|
||||||
|
nodeType.codex &&
|
||||||
|
nodeType.codex.subcategories &&
|
||||||
|
nodeType.codex.subcategories[category]
|
||||||
|
? nodeType.codex.subcategories[category]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if(subcategories === null || subcategories.length === 0) {
|
||||||
|
addNodeToCategory(accu, nodeType, category, UNCATEGORIZED_SUBCATEGORY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subcategories.forEach(subcategory => {
|
||||||
|
addNodeToCategory(accu, nodeType, category, subcategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
return accu;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
||||||
|
const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
|
||||||
|
const categories = Object.keys(categoriesWithNodes);
|
||||||
|
const sorted = categories.filter(
|
||||||
|
(category: string) =>
|
||||||
|
!excludeFromSort.includes(category),
|
||||||
|
);
|
||||||
|
sorted.sort();
|
||||||
|
|
||||||
|
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
||||||
|
const categories = getCategories(categoriesWithNodes);
|
||||||
|
|
||||||
|
return categories.reduce(
|
||||||
|
(accu: INodeCreateElement[], category: string) => {
|
||||||
|
if (!categoriesWithNodes[category]) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryEl: INodeCreateElement = {
|
||||||
|
type: 'category',
|
||||||
|
key: category,
|
||||||
|
category,
|
||||||
|
properties: {
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const subcategories = Object.keys(categoriesWithNodes[category]);
|
||||||
|
if (subcategories.length === 1) {
|
||||||
|
const subcategory = categoriesWithNodes[category][
|
||||||
|
subcategories[0]
|
||||||
|
];
|
||||||
|
if (subcategory.triggerCount > 0) {
|
||||||
|
categoryEl.includedByTrigger = subcategory.triggerCount > 0;
|
||||||
|
}
|
||||||
|
if (subcategory.regularCount > 0) {
|
||||||
|
categoryEl.includedByRegular = subcategory.regularCount > 0;
|
||||||
|
}
|
||||||
|
return [...accu, categoryEl, ...subcategory.nodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
subcategories.sort();
|
||||||
|
const subcategorized = subcategories.reduce(
|
||||||
|
(accu: INodeCreateElement[], subcategory: string) => {
|
||||||
|
const subcategoryEl: INodeCreateElement = {
|
||||||
|
type: 'subcategory',
|
||||||
|
key: `${category}_${subcategory}`,
|
||||||
|
category,
|
||||||
|
properties: {
|
||||||
|
subcategory,
|
||||||
|
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
|
||||||
|
},
|
||||||
|
includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0,
|
||||||
|
includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subcategoryEl.includedByTrigger) {
|
||||||
|
categoryEl.includedByTrigger = true;
|
||||||
|
}
|
||||||
|
if (subcategoryEl.includedByRegular) {
|
||||||
|
categoryEl.includedByRegular = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
accu.push(subcategoryEl);
|
||||||
|
return accu;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...accu, categoryEl, ...subcategorized];
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
|
@ -23,17 +23,17 @@ import {
|
||||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||||
FAKE_DOOR_FEATURES,
|
FAKE_DOOR_FEATURES,
|
||||||
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
|
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
|
||||||
|
ALL_NODE_FILTER,
|
||||||
IMPORT_CURL_MODAL_KEY,
|
IMPORT_CURL_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
IExecutionResponse,
|
|
||||||
IFakeDoor,
|
|
||||||
IFakeDoorLocation,
|
IFakeDoorLocation,
|
||||||
IRootState,
|
IRootState,
|
||||||
IRunDataDisplayMode,
|
IRunDataDisplayMode,
|
||||||
IUiState,
|
IUiState,
|
||||||
|
INodeFilterType,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
|
@ -329,6 +329,9 @@ const module: Module<IUiState, IRootState> = {
|
||||||
setOutputPanelEditModeValue: (state: IUiState, payload: string) => {
|
setOutputPanelEditModeValue: (state: IUiState, payload: string) => {
|
||||||
Vue.set(state.ndv.output.editMode, 'value', payload);
|
Vue.set(state.ndv.output.editMode, 'value', payload);
|
||||||
},
|
},
|
||||||
|
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
|
||||||
|
state.mainPanelPosition = relativePosition;
|
||||||
|
},
|
||||||
setMappableNDVInputFocus(state: IUiState, paramName: string) {
|
setMappableNDVInputFocus(state: IUiState, paramName: string) {
|
||||||
Vue.set(state.ndv, 'focusedMappableInput', paramName);
|
Vue.set(state.ndv, 'focusedMappableInput', paramName);
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,7 @@ $node-creator-item-hover-border-color: var(--color-text-light);
|
||||||
$node-creator-arrow-color: var(--color-text-light);
|
$node-creator-arrow-color: var(--color-text-light);
|
||||||
$node-creator-no-results-background-color: var(--color-background-xlight);
|
$node-creator-no-results-background-color: var(--color-background-xlight);
|
||||||
$node-creator-close-button-color: var(--color-text-xlight);
|
$node-creator-close-button-color: var(--color-text-xlight);
|
||||||
|
$node-creator-search-clear-color: var(--color-text-xlight);
|
||||||
$node-creator-search-clear-background-color: var(--color-text-light);
|
$node-creator-search-clear-background-color: var(--color-text-light);
|
||||||
$node-creator-search-clear-background-color-hover: var(--color-text-base);
|
$node-creator-search-clear-background-color-hover: var(--color-text-base);
|
||||||
$node-creator-search-placeholder-color: var(--color-text-light);
|
$node-creator-search-placeholder-color: var(--color-text-light);
|
||||||
|
|
|
@ -636,16 +636,31 @@
|
||||||
"nodeCreator.noResults.requestTheNode": "Request the node",
|
"nodeCreator.noResults.requestTheNode": "Request the node",
|
||||||
"nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?",
|
"nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?",
|
||||||
"nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet",
|
"nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet",
|
||||||
|
"nodeCreator.noResults.clickToSeeResults": "To see results, <a data-action='showAllNodeCreatorNodes'>click here</a>",
|
||||||
"nodeCreator.noResults.webhook": "Webhook",
|
"nodeCreator.noResults.webhook": "Webhook",
|
||||||
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
||||||
|
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
|
||||||
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data fields, run code",
|
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data fields, run code",
|
||||||
"nodeCreator.subcategoryDescriptions.files": "Work with CSV, XML, text, images etc.",
|
"nodeCreator.subcategoryDescriptions.files": "Work with CSV, XML, text, images etc.",
|
||||||
"nodeCreator.subcategoryDescriptions.flow": "Branches, core triggers, merge data",
|
"nodeCreator.subcategoryDescriptions.flow": "Branches, core triggers, merge data",
|
||||||
"nodeCreator.subcategoryDescriptions.helpers": "HTTP Requests (API calls), date and time, scrape HTML",
|
"nodeCreator.subcategoryDescriptions.helpers": "HTTP Requests (API calls), date and time, scrape HTML",
|
||||||
|
"nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, and more",
|
||||||
|
"nodeCreator.subcategoryNames.appTriggerNodes": "On App Event",
|
||||||
"nodeCreator.subcategoryNames.dataTransformation": "Data Transformation",
|
"nodeCreator.subcategoryNames.dataTransformation": "Data Transformation",
|
||||||
"nodeCreator.subcategoryNames.files": "Files",
|
"nodeCreator.subcategoryNames.files": "Files",
|
||||||
"nodeCreator.subcategoryNames.flow": "Flow",
|
"nodeCreator.subcategoryNames.flow": "Flow",
|
||||||
"nodeCreator.subcategoryNames.helpers": "Helpers",
|
"nodeCreator.subcategoryNames.helpers": "Helpers",
|
||||||
|
"nodeCreator.subcategoryNames.otherTriggerNodes": "Other ways...",
|
||||||
|
"nodeCreator.subcategoryTitles.otherTriggerNodes": "Other Trigger Nodes",
|
||||||
|
"nodeCreator.triggerHelperPanel.title": "When should this workflow run?",
|
||||||
|
"nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName": "On a Schedule",
|
||||||
|
"nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
|
||||||
|
"nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On Webhook Call",
|
||||||
|
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
|
||||||
|
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually",
|
||||||
|
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
|
||||||
|
"nodeCreator.triggerHelperPanel.workflowTriggerDisplayName": "When Called By Another Workflow",
|
||||||
|
"nodeCreator.triggerHelperPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
|
||||||
"nodeCredentials.createNew": "Create New",
|
"nodeCredentials.createNew": "Create New",
|
||||||
"nodeCredentials.credentialFor": "Credential for {credentialType}",
|
"nodeCredentials.credentialFor": "Credential for {credentialType}",
|
||||||
"nodeCredentials.issues": "Issues",
|
"nodeCredentials.issues": "Issues",
|
||||||
|
@ -702,14 +717,19 @@
|
||||||
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
|
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
|
||||||
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
|
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
|
||||||
"nodeView.addNode": "Add node",
|
"nodeView.addNode": "Add node",
|
||||||
|
"nodeView.addATriggerNodeFirst": "Add a <a data-action='showNodeCreator'>Trigger Node</a> first",
|
||||||
|
"nodeView.addOrEnableTriggerNode": "<a data-action='showNodeCreator'>Add</a> or enable a Trigger node to execute the workflow",
|
||||||
"nodeView.addSticky": "Click to add sticky note",
|
"nodeView.addSticky": "Click to add sticky note",
|
||||||
|
"nodeView.cantExecuteNoTrigger": "Cannot execute workflow",
|
||||||
|
"nodeView.canvasAddButton.addATriggerNodeBeforeExecuting": "Add a Trigger Node before executing the workflow",
|
||||||
|
"nodeView.canvasAddButton.addFirstStep": "Add first step…",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText": "",
|
"nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText": "",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
"nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
||||||
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
||||||
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
||||||
"nodeView.executesTheWorkflowFromTheStartOrWebhookNode": "Executes the workflow from the 'start' or 'webhook' node",
|
"nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node",
|
||||||
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
||||||
"nodeView.loadingTemplate": "Loading template",
|
"nodeView.loadingTemplate": "Loading template",
|
||||||
"nodeView.moreInfo": "More info",
|
"nodeView.moreInfo": "More info",
|
||||||
|
@ -736,10 +756,10 @@
|
||||||
"nodeView.showError.stopExecution.title": "Problem stopping execution",
|
"nodeView.showError.stopExecution.title": "Problem stopping execution",
|
||||||
"nodeView.showError.stopWaitingForWebhook.title": "Problem deleting test webhook",
|
"nodeView.showError.stopWaitingForWebhook.title": "Problem deleting test webhook",
|
||||||
"nodeView.showMessage.addNodeButton.message": "'{nodeTypeName}' is an unknown node type",
|
"nodeView.showMessage.addNodeButton.message": "'{nodeTypeName}' is an unknown node type",
|
||||||
"nodeView.showMessage.addNodeButton.title": "Could not create node",
|
"nodeView.showMessage.addNodeButton.title": "Could not insert node",
|
||||||
"nodeView.showMessage.keyDown.title": "Workflow created",
|
"nodeView.showMessage.keyDown.title": "Workflow created",
|
||||||
"nodeView.showMessage.showMaxNodeTypeError.message": "Only {count} '{nodeTypeDataDisplayName}' node is allowed in a workflow | Only {count} '{nodeTypeDataDisplayName}' nodes are allowed in a workflow",
|
"nodeView.showMessage.showMaxNodeTypeError.message": "Only one '{nodeTypeDataDisplayName}' node is allowed in a workflow | Only {count} '{nodeTypeDataDisplayName}' nodes are allowed in a workflow",
|
||||||
"nodeView.showMessage.showMaxNodeTypeError.title": "Could not create node",
|
"nodeView.showMessage.showMaxNodeTypeError.title": "Could not insert node",
|
||||||
"nodeView.showMessage.stopExecutionCatch.message": "It completed before it could be stopped",
|
"nodeView.showMessage.stopExecutionCatch.message": "It completed before it could be stopped",
|
||||||
"nodeView.showMessage.stopExecutionCatch.title": "Workflow finished executing",
|
"nodeView.showMessage.stopExecutionCatch.title": "Workflow finished executing",
|
||||||
"nodeView.showMessage.stopExecutionTry.title": "Execution stopped",
|
"nodeView.showMessage.stopExecutionTry.title": "Execution stopped",
|
||||||
|
|
|
@ -67,6 +67,7 @@ import {
|
||||||
faLink,
|
faLink,
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
faMapSigns,
|
faMapSigns,
|
||||||
|
faMousePointer,
|
||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
faPause,
|
faPause,
|
||||||
faPauseCircle,
|
faPauseCircle,
|
||||||
|
@ -83,11 +84,13 @@ import {
|
||||||
faRedo,
|
faRedo,
|
||||||
faRss,
|
faRss,
|
||||||
faSave,
|
faSave,
|
||||||
|
faSatelliteDish,
|
||||||
faSearch,
|
faSearch,
|
||||||
faSearchMinus,
|
faSearchMinus,
|
||||||
faSearchPlus,
|
faSearchPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
|
faSignOutAlt,
|
||||||
faSlidersH,
|
faSlidersH,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
faStop,
|
faStop,
|
||||||
|
@ -185,6 +188,7 @@ addIcon(faKey);
|
||||||
addIcon(faLink);
|
addIcon(faLink);
|
||||||
addIcon(faLightbulb);
|
addIcon(faLightbulb);
|
||||||
addIcon(faMapSigns);
|
addIcon(faMapSigns);
|
||||||
|
addIcon(faMousePointer);
|
||||||
addIcon(faNetworkWired);
|
addIcon(faNetworkWired);
|
||||||
addIcon(faPause);
|
addIcon(faPause);
|
||||||
addIcon(faPauseCircle);
|
addIcon(faPauseCircle);
|
||||||
|
@ -201,11 +205,13 @@ addIcon(faQuestionCircle);
|
||||||
addIcon(faRedo);
|
addIcon(faRedo);
|
||||||
addIcon(faRss);
|
addIcon(faRss);
|
||||||
addIcon(faSave);
|
addIcon(faSave);
|
||||||
|
addIcon(faSatelliteDish);
|
||||||
addIcon(faSearch);
|
addIcon(faSearch);
|
||||||
addIcon(faSearchMinus);
|
addIcon(faSearchMinus);
|
||||||
addIcon(faSearchPlus);
|
addIcon(faSearchPlus);
|
||||||
addIcon(faServer);
|
addIcon(faServer);
|
||||||
addIcon(faSignInAlt);
|
addIcon(faSignInAlt);
|
||||||
|
addIcon(faSignOutAlt);
|
||||||
addIcon(faSlidersH);
|
addIcon(faSlidersH);
|
||||||
addIcon(faSpinner);
|
addIcon(faSpinner);
|
||||||
addIcon(faSolidStickyNote);
|
addIcon(faSolidStickyNote);
|
||||||
|
|
|
@ -46,6 +46,7 @@ import templates from './modules/templates';
|
||||||
import {stringSizeInBytes} from "@/components/helpers";
|
import {stringSizeInBytes} from "@/components/helpers";
|
||||||
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
|
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
|
||||||
import communityNodes from './modules/communityNodes';
|
import communityNodes from './modules/communityNodes';
|
||||||
|
import nodeCreator from './modules/nodeCreator';
|
||||||
import { isJsonKeyObject } from './utils';
|
import { isJsonKeyObject } from './utils';
|
||||||
import { getPairedItemsMapping } from './pairedItemUtils';
|
import { getPairedItemsMapping } from './pairedItemUtils';
|
||||||
|
|
||||||
|
@ -102,6 +103,7 @@ const state: IRootState = {
|
||||||
sidebarMenuItems: [],
|
sidebarMenuItems: [],
|
||||||
instanceId: '',
|
instanceId: '',
|
||||||
nodeMetadata: {},
|
nodeMetadata: {},
|
||||||
|
subworkflowExecutionError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = {
|
const modules = {
|
||||||
|
@ -115,6 +117,7 @@ const modules = {
|
||||||
users,
|
users,
|
||||||
ui,
|
ui,
|
||||||
communityNodes,
|
communityNodes,
|
||||||
|
nodeCreator,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const store = new Vuex.Store({
|
export const store = new Vuex.Store({
|
||||||
|
@ -171,6 +174,9 @@ export const store = new Vuex.Store({
|
||||||
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
||||||
Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt);
|
Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt);
|
||||||
},
|
},
|
||||||
|
setSubworkflowExecutionError(state, subworkflowExecutionError: Error | null) {
|
||||||
|
state.subworkflowExecutionError = subworkflowExecutionError;
|
||||||
|
},
|
||||||
setActiveExecutions(state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) {
|
setActiveExecutions(state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) {
|
||||||
Vue.set(state, 'activeExecutions', newActiveExecutions);
|
Vue.set(state, 'activeExecutions', newActiveExecutions);
|
||||||
},
|
},
|
||||||
|
@ -724,6 +730,10 @@ export const store = new Vuex.Store({
|
||||||
return state.activeCredentialType;
|
return state.activeCredentialType;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
subworkflowExecutionError: (state): Error | null => {
|
||||||
|
return state.subworkflowExecutionError;
|
||||||
|
},
|
||||||
|
|
||||||
isActionActive: (state) => (action: string): boolean => {
|
isActionActive: (state) => (action: string): boolean => {
|
||||||
return state.activeActions.includes(action);
|
return state.activeActions.includes(action);
|
||||||
},
|
},
|
||||||
|
|
95
packages/editor-ui/src/views/CanvasAddButton.vue
Normal file
95
packages/editor-ui/src/views/CanvasAddButton.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.container" :style="containerCssVars" ref="container">
|
||||||
|
<n8n-tooltip placement="top" :value="showTooltip" manual :disabled="isScrimActive" :popper-class="$style.tooltip" :open-delay="700">
|
||||||
|
<button :class="$style.button" @click="$emit('click')">
|
||||||
|
<font-awesome-icon icon="plus" size="lg" />
|
||||||
|
</button>
|
||||||
|
<template #content>
|
||||||
|
<p v-text="$locale.baseText('nodeView.canvasAddButton.addATriggerNodeBeforeExecuting')" />
|
||||||
|
</template>
|
||||||
|
</n8n-tooltip>
|
||||||
|
<p :class="$style.label" v-text="$locale.baseText('nodeView.canvasAddButton.addFirstStep')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { XYPosition } from '@/Interface';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'CanvasAddButton',
|
||||||
|
props: {
|
||||||
|
showTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
containerCssVars(): Record<string, string> {
|
||||||
|
const position = this.position as XYPosition;
|
||||||
|
return {
|
||||||
|
'--trigger-placeholder-left-position': `${position[0]}px`,
|
||||||
|
'--trigger-placeholder-top-position': `${position[1]}px`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isScrimActive(): boolean {
|
||||||
|
return this.$store.getters['nodeCreator/showScrim'];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
top: var(--trigger-placeholder-top-position);
|
||||||
|
left: var(--trigger-placeholder-left-position);
|
||||||
|
// We have to increase z-index to make sure it's higher than selecting box in NodeView
|
||||||
|
// otherwise the clics wouldn't register
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
&:hover .button svg path {
|
||||||
|
fill: var(--color-primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: var(--color-foreground-xlight);
|
||||||
|
border: 2px dashed var(--color-foreground-xdark);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 26px !important;
|
||||||
|
height: 40px;
|
||||||
|
path {
|
||||||
|
fill: var(--color-foreground-xdark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: max-content;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
line-height: var(--font-line-height-xloose );
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,22 +4,73 @@
|
||||||
@touchmove="mouseMoveNodeWorkflow" @mousedown="mouseDown" v-touch:tap="touchTap" @mouseup="mouseUp"
|
@touchmove="mouseMoveNodeWorkflow" @mousedown="mouseDown" v-touch:tap="touchTap" @mouseup="mouseUp"
|
||||||
@wheel="wheelScroll">
|
@wheel="wheelScroll">
|
||||||
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
|
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
|
||||||
<div id="node-view" class="node-view" :style="workflowStyle">
|
<div
|
||||||
|
id="node-view"
|
||||||
|
class="node-view"
|
||||||
|
:style="workflowStyle"
|
||||||
|
ref="nodeView"
|
||||||
|
>
|
||||||
|
<canvas-add-button
|
||||||
|
:style="canvasAddButtonStyle"
|
||||||
|
@click="showTriggerCreator('tirger_placeholder_button')"
|
||||||
|
v-show="showCanvasAddButton"
|
||||||
|
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
|
||||||
|
:position="canvasAddButtonPosition"
|
||||||
|
@hook:mounted="setRecenteredCanvasAddButtonPosition"
|
||||||
|
/>
|
||||||
<div v-for="nodeData in nodes" :key="nodeData.id">
|
<div v-for="nodeData in nodes" :key="nodeData.id">
|
||||||
<node v-if="nodeData.type !== STICKY_NODE_TYPE" @duplicateNode="duplicateNode"
|
<node
|
||||||
@deselectAllNodes="deselectAllNodes" @deselectNode="nodeDeselectedByName" @nodeSelected="nodeSelectedByName"
|
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
||||||
@removeNode="removeNode" @runWorkflow="onRunNode" @moved="onNodeMoved" @run="onNodeRun" :key="nodeData.id"
|
@duplicateNode="duplicateNode"
|
||||||
:name="nodeData.name" :isReadOnly="isReadOnly" :instance="instance"
|
@deselectAllNodes="deselectAllNodes"
|
||||||
:isActive="!!activeNode && activeNode.name === nodeData.name" :hideActions="pullConnActive" />
|
@deselectNode="nodeDeselectedByName"
|
||||||
<Sticky v-else @deselectAllNodes="deselectAllNodes" @deselectNode="nodeDeselectedByName"
|
@nodeSelected="nodeSelectedByName"
|
||||||
@nodeSelected="nodeSelectedByName" @removeNode="removeNode" :key="nodeData.id" :name="nodeData.name"
|
@removeNode="removeNode"
|
||||||
:isReadOnly="isReadOnly" :instance="instance" :isActive="!!activeNode && activeNode.name === nodeData.name"
|
@runWorkflow="onRunNode"
|
||||||
:nodeViewScale="nodeViewScale" :gridSize="GRID_SIZE" :hideActions="pullConnActive" />
|
@moved="onNodeMoved"
|
||||||
|
@run="onNodeRun"
|
||||||
|
:key="`${nodeData.id}_node`"
|
||||||
|
:name="nodeData.name"
|
||||||
|
:isReadOnly="isReadOnly"
|
||||||
|
:instance="instance"
|
||||||
|
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
||||||
|
:hideActions="pullConnActive"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="custom-tooltip"
|
||||||
|
v-text="$locale.baseText('nodeView.placeholderNode.addTriggerNodeBeforeExecuting')"
|
||||||
|
/>
|
||||||
|
</node>
|
||||||
|
<sticky
|
||||||
|
v-else
|
||||||
|
@deselectAllNodes="deselectAllNodes"
|
||||||
|
@deselectNode="nodeDeselectedByName"
|
||||||
|
@nodeSelected="nodeSelectedByName"
|
||||||
|
@removeNode="removeNode"
|
||||||
|
:key="`${nodeData.id}_sticky`"
|
||||||
|
:name="nodeData.name"
|
||||||
|
:isReadOnly="isReadOnly"
|
||||||
|
:instance="instance"
|
||||||
|
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
||||||
|
:nodeViewScale="nodeViewScale"
|
||||||
|
:gridSize="GRID_SIZE"
|
||||||
|
:hideActions="pullConnActive"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NodeDetailsView :readOnly="isReadOnly" :renaming="renamingActive" @valueChanged="valueChanged" />
|
<node-details-view
|
||||||
<node-creation v-if="!isReadOnly" :create-node-active="createNodeActive" :node-view-scale="nodeViewScale" @toggleNodeCreator="onToggleNodeCreator" @addNode="onAddNode"/>
|
:readOnly="isReadOnly"
|
||||||
|
:renaming="renamingActive"
|
||||||
|
@valueChanged="valueChanged"
|
||||||
|
/>
|
||||||
|
<node-creation
|
||||||
|
v-if="!isReadOnly"
|
||||||
|
:create-node-active="createNodeActive"
|
||||||
|
:node-view-scale="nodeViewScale"
|
||||||
|
@toggleNodeCreator="onToggleNodeCreator"
|
||||||
|
@addNode="onAddNode"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
:class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
:class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
||||||
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')"
|
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')"
|
||||||
|
@ -31,10 +82,25 @@
|
||||||
<n8n-icon-button v-if="nodeViewScale !== 1 && !isDemo" @click="resetZoom" type="tertiary" size="large"
|
<n8n-icon-button v-if="nodeViewScale !== 1 && !isDemo" @click="resetZoom" type="tertiary" size="large"
|
||||||
:title="$locale.baseText('nodeView.resetZoom')" icon="undo" />
|
:title="$locale.baseText('nodeView.resetZoom')" icon="undo" />
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-execute-wrapper" v-if="!isReadOnly">
|
<div
|
||||||
<n8n-button @click.stop="onRunWorkflow" :loading="workflowRunning" :label="runButtonText"
|
class="workflow-execute-wrapper" v-if="!isReadOnly"
|
||||||
:title="$locale.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')" size="large"
|
>
|
||||||
icon="play-circle" type="primary" />
|
<span
|
||||||
|
@mouseenter="showTriggerMissingToltip(true)"
|
||||||
|
@mouseleave="showTriggerMissingToltip(false)"
|
||||||
|
@click="onRunContainerClick"
|
||||||
|
>
|
||||||
|
<n8n-button
|
||||||
|
@click.stop="onRunWorkflow"
|
||||||
|
:loading="workflowRunning"
|
||||||
|
:label="runButtonText"
|
||||||
|
:title="$locale.baseText('nodeView.executesTheWorkflowFromATriggerNode')"
|
||||||
|
size="large"
|
||||||
|
icon="play-circle"
|
||||||
|
type="primary"
|
||||||
|
:disabled="isExecutionDisabled"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
<n8n-icon-button v-if="workflowRunning === true && !executionWaitingForWebhook" icon="stop" size="large"
|
<n8n-icon-button v-if="workflowRunning === true && !executionWaitingForWebhook" icon="stop" size="large"
|
||||||
class="stop-execution" type="secondary" :title="stopExecutionInProgress
|
class="stop-execution" type="secondary" :title="stopExecutionInProgress
|
||||||
|
@ -46,7 +112,7 @@
|
||||||
icon="stop" size="large" :title="$locale.baseText('nodeView.stopWaitingForWebhookCall')" type="secondary"
|
icon="stop" size="large" :title="$locale.baseText('nodeView.stopWaitingForWebhookCall')" type="secondary"
|
||||||
@click.stop="stopWaitingForWebhook()" />
|
@click.stop="stopWaitingForWebhook()" />
|
||||||
|
|
||||||
<n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning"
|
<n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning && !allTriggersDisabled"
|
||||||
:title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')" icon="trash" size="large"
|
:title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')" icon="trash" size="large"
|
||||||
@click.stop="clearExecutionData()" />
|
@click.stop="clearExecutionData()" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +126,8 @@ import {
|
||||||
} from 'jsplumb';
|
} from 'jsplumb';
|
||||||
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
||||||
|
import once from 'lodash/once';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FIRST_ONBOARDING_PROMPT_TIMEOUT,
|
FIRST_ONBOARDING_PROMPT_TIMEOUT,
|
||||||
MODAL_CANCEL,
|
MODAL_CANCEL,
|
||||||
|
@ -75,6 +143,7 @@ import {
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
WORKFLOW_OPEN_MODAL_KEY,
|
WORKFLOW_OPEN_MODAL_KEY,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
@ -82,6 +151,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
import { mouseSelect } from '@/components/mixins/mouseSelect';
|
import { mouseSelect } from '@/components/mixins/mouseSelect';
|
||||||
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
|
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
|
import { globalLinkActions } from '@/components/mixins/globalLinkActions';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
import { titleChange } from '@/components/mixins/titleChange';
|
import { titleChange } from '@/components/mixins/titleChange';
|
||||||
import { newVersions } from '@/components/mixins/newVersions';
|
import { newVersions } from '@/components/mixins/newVersions';
|
||||||
|
@ -93,6 +163,7 @@ import NodeDetailsView from '@/components/NodeDetailsView.vue';
|
||||||
import Node from '@/components/Node.vue';
|
import Node from '@/components/Node.vue';
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import Sticky from '@/components/Sticky.vue';
|
import Sticky from '@/components/Sticky.vue';
|
||||||
|
import CanvasAddButton from './CanvasAddButton.vue';
|
||||||
|
|
||||||
import * as CanvasHelpers from './canvasHelpers';
|
import * as CanvasHelpers from './canvasHelpers';
|
||||||
|
|
||||||
|
@ -129,6 +200,7 @@ import {
|
||||||
XYPosition,
|
XYPosition,
|
||||||
IPushDataExecutionFinished,
|
IPushDataExecutionFinished,
|
||||||
ITag,
|
ITag,
|
||||||
|
INewWorkflowData,
|
||||||
IWorkflowTemplate,
|
IWorkflowTemplate,
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
IWorkflowToShare,
|
IWorkflowToShare,
|
||||||
|
@ -158,6 +230,7 @@ export default mixins(
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
workflowRun,
|
workflowRun,
|
||||||
newVersions,
|
newVersions,
|
||||||
|
globalLinkActions,
|
||||||
debounceHelper,
|
debounceHelper,
|
||||||
)
|
)
|
||||||
.extend({
|
.extend({
|
||||||
|
@ -168,6 +241,7 @@ export default mixins(
|
||||||
NodeCreator: () => import('@/components/Node/NodeCreator/NodeCreator.vue'),
|
NodeCreator: () => import('@/components/Node/NodeCreator/NodeCreator.vue'),
|
||||||
NodeSettings,
|
NodeSettings,
|
||||||
Sticky,
|
Sticky,
|
||||||
|
CanvasAddButton,
|
||||||
NodeCreation: () => import('@/components/Node/NodeCreation.vue'),
|
NodeCreation: () => import('@/components/Node/NodeCreation.vue'),
|
||||||
},
|
},
|
||||||
errorCaptured: (err, vm, info) => {
|
errorCaptured: (err, vm, info) => {
|
||||||
|
@ -182,7 +256,7 @@ export default mixins(
|
||||||
this.createNodeActive = false;
|
this.createNodeActive = false;
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
async handler(value, oldValue) {
|
async handler () {
|
||||||
// Load a workflow
|
// Load a workflow
|
||||||
let workflowId = null as string | null;
|
let workflowId = null as string | null;
|
||||||
if (this.$route && this.$route.params.name) {
|
if (this.$route && this.$route.params.name) {
|
||||||
|
@ -201,9 +275,14 @@ export default mixins(
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
containsTrigger(containsTrigger) {
|
||||||
|
// Re-center CanvasAddButton if there's no triggers
|
||||||
|
if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
|
||||||
|
else this.tryToAddWelcomeSticky();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async beforeRouteLeave(to, from, next) {
|
async beforeRouteLeave(to, from, next) {
|
||||||
|
this.$store.commit('setSubworkflowExecutionError', null);
|
||||||
const result = this.$store.getters.getStateIsDirty;
|
const result = this.$store.getters.getStateIsDirty;
|
||||||
if (result) {
|
if (result) {
|
||||||
const confirmModal = await this.confirmModal(
|
const confirmModal = await this.confirmModal(
|
||||||
|
@ -257,6 +336,12 @@ export default mixins(
|
||||||
isDemo(): boolean {
|
isDemo(): boolean {
|
||||||
return this.$route.name === VIEWS.DEMO;
|
return this.$route.name === VIEWS.DEMO;
|
||||||
},
|
},
|
||||||
|
isExecutionView(): boolean {
|
||||||
|
return this.$route.name === VIEWS.EXECUTION;
|
||||||
|
},
|
||||||
|
showCanvasAddButton(): boolean {
|
||||||
|
return this.loadingService === null && !this.containsTrigger && !this.isDemo && !this.isExecutionView;
|
||||||
|
},
|
||||||
lastSelectedNode(): INodeUi | null {
|
lastSelectedNode(): INodeUi | null {
|
||||||
return this.$store.getters.lastSelectedNode;
|
return this.$store.getters.lastSelectedNode;
|
||||||
},
|
},
|
||||||
|
@ -275,14 +360,19 @@ export default mixins(
|
||||||
return this.$locale.baseText('nodeView.runButtonText.executingWorkflow');
|
return this.$locale.baseText('nodeView.runButtonText.executingWorkflow');
|
||||||
},
|
},
|
||||||
workflowStyle(): object {
|
workflowStyle(): object {
|
||||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
const offsetPosition = this.getNodeViewOffsetPosition;
|
||||||
return {
|
return {
|
||||||
left: offsetPosition[0] + 'px',
|
left: offsetPosition[0] + 'px',
|
||||||
top: offsetPosition[1] + 'px',
|
top: offsetPosition[1] + 'px',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
canvasAddButtonStyle(): object {
|
||||||
|
return {
|
||||||
|
'pointer-events': this.createNodeActive ? 'none' : 'all',
|
||||||
|
};
|
||||||
|
},
|
||||||
backgroundStyle(): object {
|
backgroundStyle(): object {
|
||||||
return CanvasHelpers.getBackgroundStyles(this.nodeViewScale, this.$store.getters.getNodeViewOffsetPosition);
|
return CanvasHelpers.getBackgroundStyles(this.nodeViewScale, this.getNodeViewOffsetPosition);
|
||||||
},
|
},
|
||||||
workflowClasses() {
|
workflowClasses() {
|
||||||
const returnClasses = [];
|
const returnClasses = [];
|
||||||
|
@ -305,6 +395,26 @@ export default mixins(
|
||||||
workflowRunning(): boolean {
|
workflowRunning(): boolean {
|
||||||
return this.$store.getters.isActionActive('workflowRunning');
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
},
|
},
|
||||||
|
allTriggersDisabled(): boolean {
|
||||||
|
const disabledTriggerNodes = this.triggerNodes.filter(node => node.disabled);
|
||||||
|
|
||||||
|
return disabledTriggerNodes.length === this.triggerNodes.length;
|
||||||
|
},
|
||||||
|
triggerNodes(): INodeUi[] {
|
||||||
|
return this.nodes.filter(node =>
|
||||||
|
node.type === START_NODE_TYPE ||
|
||||||
|
this.$store.getters['nodeTypes/isTriggerNode'](node.type),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
containsTrigger(): boolean {
|
||||||
|
return this.triggerNodes.length > 0;
|
||||||
|
},
|
||||||
|
isExecutionDisabled(): boolean {
|
||||||
|
return !this.containsTrigger || this.allTriggersDisabled;
|
||||||
|
},
|
||||||
|
getNodeViewOffsetPosition(): XYPosition {
|
||||||
|
return this.$store.getters.getNodeViewOffsetPosition;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -325,6 +435,9 @@ export default mixins(
|
||||||
dropPrevented: false,
|
dropPrevented: false,
|
||||||
renamingActive: false,
|
renamingActive: false,
|
||||||
showStickyButton: false,
|
showStickyButton: false,
|
||||||
|
showTriggerMissingTooltip: false,
|
||||||
|
canvasAddButtonPosition: [1, 1] as XYPosition,
|
||||||
|
workflowData: null as INewWorkflowData | null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -333,8 +446,12 @@ export default mixins(
|
||||||
// could add up with them registred multiple times
|
// could add up with them registred multiple times
|
||||||
document.removeEventListener('keydown', this.keyDown);
|
document.removeEventListener('keydown', this.keyDown);
|
||||||
document.removeEventListener('keyup', this.keyUp);
|
document.removeEventListener('keyup', this.keyUp);
|
||||||
|
this.unregisterCustomAction('showNodeCreator');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showTriggerMissingToltip(isVisible: boolean) {
|
||||||
|
this.showTriggerMissingTooltip = isVisible;
|
||||||
|
},
|
||||||
onRunNode(nodeName: string, source: string) {
|
onRunNode(nodeName: string, source: string) {
|
||||||
const node = this.$store.getters.getNodeByName(nodeName);
|
const node = this.$store.getters.getNodeByName(nodeName);
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
|
@ -358,6 +475,25 @@ export default mixins(
|
||||||
|
|
||||||
this.runWorkflow();
|
this.runWorkflow();
|
||||||
},
|
},
|
||||||
|
onRunContainerClick() {
|
||||||
|
if (this.containsTrigger && !this.allTriggersDisabled) return;
|
||||||
|
|
||||||
|
const message = this.containsTrigger && this.allTriggersDisabled
|
||||||
|
? this.$locale.baseText('nodeView.addOrEnableTriggerNode')
|
||||||
|
: this.$locale.baseText('nodeView.addATriggerNodeFirst');
|
||||||
|
|
||||||
|
this.registerCustomAction('showNodeCreator', () => this.showTriggerCreator('no_trigger_execution_tooltip'));
|
||||||
|
const notice = this.$showMessage({
|
||||||
|
type: 'info',
|
||||||
|
title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'),
|
||||||
|
message,
|
||||||
|
duration: 3000,
|
||||||
|
onClick: () => setTimeout(() => {
|
||||||
|
// Close the creator panel if user clicked on the link
|
||||||
|
if(this.createNodeActive) notice.close();
|
||||||
|
}, 0),
|
||||||
|
});
|
||||||
|
},
|
||||||
clearExecutionData() {
|
clearExecutionData() {
|
||||||
this.$store.commit('setWorkflowExecutionData', null);
|
this.$store.commit('setWorkflowExecutionData', null);
|
||||||
this.updateNodesExecutionIssues();
|
this.updateNodesExecutionIssues();
|
||||||
|
@ -436,6 +572,13 @@ export default mixins(
|
||||||
const saved = await this.saveCurrentWorkflow();
|
const saved = await this.saveCurrentWorkflow();
|
||||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||||
},
|
},
|
||||||
|
showTriggerCreator(source: string) {
|
||||||
|
if(this.createNodeActive) return;
|
||||||
|
this.$store.commit('nodeCreator/setSelectedType', TRIGGER_NODE_FILTER);
|
||||||
|
this.$store.commit('nodeCreator/setShowScrim', true);
|
||||||
|
this.onToggleNodeCreator({ source, createNodeActive: true });
|
||||||
|
this.$nextTick(() => this.$store.commit('nodeCreator/setShowTabs', false));
|
||||||
|
},
|
||||||
async openExecution(executionId: string) {
|
async openExecution(executionId: string) {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
|
||||||
|
@ -560,7 +703,7 @@ export default mixins(
|
||||||
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
||||||
|
|
||||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||||
await this.$store.dispatch('workflows/getNewWorkflowData', data.name);
|
this.workflowData = await this.$store.dispatch('workflows/getNewWorkflowData', data.name);
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
@ -999,21 +1142,21 @@ export default mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
resetZoom() {
|
resetZoom() {
|
||||||
const { scale, offset } = CanvasHelpers.scaleReset({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
|
const { scale, offset } = CanvasHelpers.scaleReset({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
|
||||||
|
|
||||||
this.setZoomLevel(scale);
|
this.setZoomLevel(scale);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
|
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
|
||||||
},
|
},
|
||||||
|
|
||||||
zoomIn() {
|
zoomIn() {
|
||||||
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleBigger({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
|
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleBigger({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
|
||||||
|
|
||||||
this.setZoomLevel(scale);
|
this.setZoomLevel(scale);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
|
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
|
||||||
},
|
},
|
||||||
|
|
||||||
zoomOut() {
|
zoomOut() {
|
||||||
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleSmaller({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
|
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleSmaller({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
|
||||||
|
|
||||||
this.setZoomLevel(scale);
|
this.setZoomLevel(scale);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
|
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
|
||||||
|
@ -1021,7 +1164,7 @@ export default mixins(
|
||||||
|
|
||||||
setZoomLevel(zoomLevel: number) {
|
setZoomLevel(zoomLevel: number) {
|
||||||
this.nodeViewScale = zoomLevel; // important for background
|
this.nodeViewScale = zoomLevel; // important for background
|
||||||
const element = this.instance.getContainer() as HTMLElement;
|
const element = this.$refs.nodeView as HTMLElement;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1039,15 +1182,44 @@ export default mixins(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.instance.setZoom(zoomLevel);
|
this.instance.setZoom(zoomLevel);
|
||||||
},
|
},
|
||||||
|
setRecenteredCanvasAddButtonPosition (offset?: XYPosition) {
|
||||||
|
|
||||||
zoomToFit() {
|
const position = CanvasHelpers.getMidCanvasPosition(this.nodeViewScale, offset || [0, 0]);
|
||||||
|
|
||||||
|
position[0] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
||||||
|
position[1] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
||||||
|
|
||||||
|
this.canvasAddButtonPosition = CanvasHelpers.getNewNodePosition(this.nodes, position);
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaceholderTriggerNodeUI (): INodeUi {
|
||||||
|
this.setRecenteredCanvasAddButtonPosition();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
...CanvasHelpers.DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
|
||||||
|
position: this.canvasAddButtonPosition,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// Extend nodes with placeholder trigger button as NodeUI object
|
||||||
|
// with the centered position if canvas doesn't contains trigger node
|
||||||
|
getNodesWithPlaceholderNode(): INodeUi[] {
|
||||||
const nodes = this.$store.getters.allNodes as INodeUi[];
|
const nodes = this.$store.getters.allNodes as INodeUi[];
|
||||||
|
|
||||||
|
const extendedNodes = this.containsTrigger
|
||||||
|
? nodes
|
||||||
|
: [this.getPlaceholderTriggerNodeUI(), ...nodes];
|
||||||
|
|
||||||
|
return extendedNodes;
|
||||||
|
},
|
||||||
|
zoomToFit() {
|
||||||
|
const nodes = this.getNodesWithPlaceholderNode() as INodeUi[];
|
||||||
|
|
||||||
if (nodes.length === 0) { // some unknown workflow executions
|
if (nodes.length === 0) { // some unknown workflow executions
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { zoomLevel, offset } = CanvasHelpers.getZoomToFit(nodes, !this.isDemo);
|
const { zoomLevel, offset } = CanvasHelpers.getZoomToFit(nodes);
|
||||||
|
|
||||||
this.setZoomLevel(zoomLevel);
|
this.setZoomLevel(zoomLevel);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
|
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
|
||||||
|
@ -1313,7 +1485,6 @@ export default mixins(
|
||||||
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
|
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
|
||||||
if (nodeTypeName) {
|
if (nodeTypeName) {
|
||||||
const mousePosition = this.getMousePositionWithinNodeView(event);
|
const mousePosition = this.getMousePositionWithinNodeView(event);
|
||||||
const sidebarOffset = this.sidebarMenuCollapsed ? CanvasHelpers.SIDEBAR_WIDTH : CanvasHelpers.SIDEBAR_WIDTH_EXPANDED;
|
|
||||||
|
|
||||||
this.addNode(nodeTypeName, {
|
this.addNode(nodeTypeName, {
|
||||||
position: [
|
position: [
|
||||||
|
@ -1460,7 +1631,7 @@ export default mixins(
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
|
|
||||||
if (options.position) {
|
if (options.position) {
|
||||||
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, options.position);
|
newNodeData.position = CanvasHelpers.getNewNodePosition(this.getNodesWithPlaceholderNode(), options.position);
|
||||||
} else if (lastSelectedNode) {
|
} else if (lastSelectedNode) {
|
||||||
const lastSelectedConnection = this.lastSelectedConnection;
|
const lastSelectedConnection = this.lastSelectedConnection;
|
||||||
if (lastSelectedConnection) { // set when injecting into a connection
|
if (lastSelectedConnection) { // set when injecting into a connection
|
||||||
|
@ -1499,8 +1670,14 @@ export default mixins(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no node is active find a free spot
|
// If added node is a trigger and it's the first one added to the canvas
|
||||||
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, this.lastClickPosition);
|
// we place it at canvasAddButtonPosition to replace the canvas add button
|
||||||
|
const position = this.$store.getters['nodeTypes/isTriggerNode'](nodeTypeName) && !this.containsTrigger
|
||||||
|
? this.canvasAddButtonPosition
|
||||||
|
// If no node is active find a free spot
|
||||||
|
: this.lastClickPosition as XYPosition;
|
||||||
|
|
||||||
|
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1944,48 +2121,41 @@ export default mixins(
|
||||||
},
|
},
|
||||||
async newWorkflow(): Promise<void> {
|
async newWorkflow(): Promise<void> {
|
||||||
await this.resetWorkspace();
|
await this.resetWorkspace();
|
||||||
const newWorkflow = await this.$store.dispatch('workflows/getNewWorkflowData');
|
this.workflowData = await this.$store.dispatch('workflows/getNewWorkflowData');
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', false);
|
this.$store.commit('setStateDirty', false);
|
||||||
|
|
||||||
await this.addNodes([{
|
|
||||||
id: uuid(),
|
|
||||||
...CanvasHelpers.DEFAULT_START_NODE,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
this.nodeSelectedByName(CanvasHelpers.DEFAULT_START_NODE.name, false);
|
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', false);
|
|
||||||
|
|
||||||
this.setZoomLevel(1);
|
this.setZoomLevel(1);
|
||||||
|
this.zoomToFit();
|
||||||
const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined;
|
|
||||||
|
|
||||||
if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [0, 0] });
|
|
||||||
// For novice users (onboardingFlowEnabled == true)
|
|
||||||
// Inject welcome sticky note and zoom to fit
|
|
||||||
if (newWorkflow.onboardingFlowEnabled && !this.isReadOnly) {
|
|
||||||
this.$nextTick(async () => {
|
|
||||||
await this.addNodes([
|
|
||||||
{
|
|
||||||
id: uuid(),
|
|
||||||
...CanvasHelpers.WELCOME_STICKY_NODE,
|
|
||||||
parameters: {
|
|
||||||
// Use parameters from the template but add translated content
|
|
||||||
...CanvasHelpers.WELCOME_STICKY_NODE.parameters,
|
|
||||||
content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
this.zoomToFit();
|
|
||||||
this.$telemetry.track('welcome note inserted');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
tryToAddWelcomeSticky: once(async function(this: any) {
|
||||||
|
const newWorkflow = this.workflowData;
|
||||||
|
const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined;
|
||||||
|
if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') {
|
||||||
|
// For novice users (onboardingFlowEnabled == true)
|
||||||
|
// Inject welcome sticky note and zoom to fit
|
||||||
|
|
||||||
|
if (newWorkflow?.onboardingFlowEnabled && !this.isReadOnly) {
|
||||||
|
const collisionPadding = CanvasHelpers.GRID_SIZE + CanvasHelpers.NODE_SIZE;
|
||||||
|
// Position the welcome sticky left to the added trigger node
|
||||||
|
let position: XYPosition = [...(this.triggerNodes[0].position as XYPosition)];
|
||||||
|
|
||||||
|
position[0] -= CanvasHelpers.WELCOME_STICKY_NODE.parameters.width + (CanvasHelpers.GRID_SIZE * 4);
|
||||||
|
position = CanvasHelpers.getNewNodePosition(this.nodes, position, [collisionPadding, collisionPadding]);
|
||||||
|
|
||||||
|
await this.addNodes([{
|
||||||
|
id: uuid(),
|
||||||
|
...CanvasHelpers.WELCOME_STICKY_NODE,
|
||||||
|
parameters: {
|
||||||
|
// Use parameters from the template but add translated content
|
||||||
|
...CanvasHelpers.WELCOME_STICKY_NODE.parameters,
|
||||||
|
content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
|
||||||
|
},
|
||||||
|
position,
|
||||||
|
}]);
|
||||||
|
this.$telemetry.track('welcome note inserted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
async initView(): Promise<void> {
|
async initView(): Promise<void> {
|
||||||
if (this.$route.params.action === 'workflowSave') {
|
if (this.$route.params.action === 'workflowSave') {
|
||||||
// In case the workflow got saved we do not have to run init
|
// In case the workflow got saved we do not have to run init
|
||||||
|
@ -2001,7 +2171,7 @@ export default mixins(
|
||||||
const templateId = this.$route.params.id;
|
const templateId = this.$route.params.id;
|
||||||
await this.openWorkflowTemplate(templateId);
|
await this.openWorkflowTemplate(templateId);
|
||||||
}
|
}
|
||||||
else if (this.$route.name === VIEWS.EXECUTION) {
|
else if (this.isExecutionView) {
|
||||||
// Load an execution
|
// Load an execution
|
||||||
const executionId = this.$route.params.id;
|
const executionId = this.$route.params.id;
|
||||||
await this.openExecution(executionId);
|
await this.openExecution(executionId);
|
||||||
|
@ -2057,7 +2227,7 @@ export default mixins(
|
||||||
document.addEventListener('keyup', this.keyUp);
|
document.addEventListener('keyup', this.keyUp);
|
||||||
|
|
||||||
window.addEventListener("beforeunload", (e) => {
|
window.addEventListener("beforeunload", (e) => {
|
||||||
if (this.isDemo) {
|
if (this.isDemo){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (this.$store.getters.getStateIsDirty === true) {
|
else if (this.$store.getters.getStateIsDirty === true) {
|
||||||
|
@ -2368,7 +2538,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
// "requiredNodeTypes" are also defined in cli/commands/run.ts
|
// "requiredNodeTypes" are also defined in cli/commands/run.ts
|
||||||
const requiredNodeTypes = [START_NODE_TYPE];
|
const requiredNodeTypes: string[] = [];
|
||||||
|
|
||||||
if (requiredNodeTypes.includes(node.type)) {
|
if (requiredNodeTypes.includes(node.type)) {
|
||||||
// The node is of the required type so check first
|
// The node is of the required type so check first
|
||||||
|
@ -2921,7 +3091,7 @@ export default mixins(
|
||||||
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
||||||
},
|
},
|
||||||
async loadNodeTypes(): Promise<void> {
|
async loadNodeTypes(): Promise<void> {
|
||||||
this.$store.dispatch('nodeTypes/getNodeTypes');
|
await this.$store.dispatch('nodeTypes/getNodeTypes');
|
||||||
},
|
},
|
||||||
async loadCredentialTypes(): Promise<void> {
|
async loadCredentialTypes(): Promise<void> {
|
||||||
await this.$store.dispatch('credentials/fetchCredentialTypes', true);
|
await this.$store.dispatch('credentials/fetchCredentialTypes', true);
|
||||||
|
@ -3017,6 +3187,11 @@ export default mixins(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToggleNodeCreator({ source, createNodeActive }: { source?: string; createNodeActive: boolean }) {
|
onToggleNodeCreator({ source, createNodeActive }: { source?: string; createNodeActive: boolean }) {
|
||||||
|
if (createNodeActive === this.createNodeActive) return;
|
||||||
|
|
||||||
|
// Default to the trigger tab in node creator if there's no trigger node yet
|
||||||
|
if (!this.containsTrigger) this.$store.commit('nodeCreator/setSelectedType', TRIGGER_NODE_FILTER);
|
||||||
|
|
||||||
this.createNodeActive = createNodeActive;
|
this.createNodeActive = createNodeActive;
|
||||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, createNodeActive });
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, createNodeActive });
|
||||||
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source, createNodeActive, workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source, createNodeActive, workflow_id: this.$store.getters.workflowId });
|
||||||
|
@ -3140,7 +3315,7 @@ export default mixins(
|
||||||
color: #444;
|
color: #444;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
|
|
||||||
&.expanded {
|
&:not(.demo-zoom-menu).expanded {
|
||||||
left: $sidebar-expanded-width + $--zoom-menu-margin;
|
left: $sidebar-expanded-width + $--zoom-menu-margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3175,6 +3350,7 @@ export default mixins(
|
||||||
background-color: var(--color-canvas-background);
|
background-color: var(--color-canvas-background);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-view-wrapper {
|
.node-view-wrapper {
|
||||||
|
@ -3212,14 +3388,15 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-execute-wrapper {
|
.workflow-execute-wrapper {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
line-height: 65px;
|
display: flex;
|
||||||
left: calc(50% - 150px);
|
justify-content: center;
|
||||||
bottom: 30px;
|
left: 50%;
|
||||||
width: 300px;
|
transform: translateX(-50%);
|
||||||
text-align: center;
|
bottom: 110px;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
>* {
|
> * {
|
||||||
margin-inline-end: 0.625rem;
|
margin-inline-end: 0.625rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const MIN_X_TO_SHOW_OUTPUT_LABEL = 90;
|
||||||
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
||||||
|
|
||||||
export const NODE_SIZE = 100;
|
export const NODE_SIZE = 100;
|
||||||
|
export const PLACEHOLDER_TRIGGER_NODE_SIZE = 100;
|
||||||
export const DEFAULT_START_POSITION_X = 180;
|
export const DEFAULT_START_POSITION_X = 180;
|
||||||
export const DEFAULT_START_POSITION_Y = 240;
|
export const DEFAULT_START_POSITION_Y = 240;
|
||||||
export const HEADER_HEIGHT = 65;
|
export const HEADER_HEIGHT = 65;
|
||||||
|
@ -38,6 +39,7 @@ export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
|
||||||
const LOOPBACK_MINIMUM = 140;
|
const LOOPBACK_MINIMUM = 140;
|
||||||
export const INPUT_UUID_KEY = '-input';
|
export const INPUT_UUID_KEY = '-input';
|
||||||
export const OUTPUT_UUID_KEY = '-output';
|
export const OUTPUT_UUID_KEY = '-output';
|
||||||
|
export const PLACEHOLDER_BUTTON = 'PlaceholderTriggerButton';
|
||||||
|
|
||||||
export const DEFAULT_START_NODE = {
|
export const DEFAULT_START_NODE = {
|
||||||
name: 'Start',
|
name: 'Start',
|
||||||
|
@ -50,13 +52,24 @@ export const DEFAULT_START_NODE = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_PLACEHOLDER_TRIGGER_BUTTON = {
|
||||||
|
name: 'Choose a Trigger...',
|
||||||
|
type: PLACEHOLDER_BUTTON,
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [],
|
||||||
|
parameters: {
|
||||||
|
height: PLACEHOLDER_TRIGGER_NODE_SIZE,
|
||||||
|
width: PLACEHOLDER_TRIGGER_NODE_SIZE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const WELCOME_STICKY_NODE = {
|
export const WELCOME_STICKY_NODE = {
|
||||||
name: QUICKSTART_NOTE_NAME,
|
name: QUICKSTART_NOTE_NAME,
|
||||||
type: STICKY_NODE_TYPE,
|
type: STICKY_NODE_TYPE,
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [
|
position: [
|
||||||
-260,
|
0,
|
||||||
200,
|
0,
|
||||||
] as XYPosition,
|
] as XYPosition,
|
||||||
parameters: {
|
parameters: {
|
||||||
height: 300,
|
height: 300,
|
||||||
|
@ -233,8 +246,10 @@ export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||||
|
|
||||||
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
||||||
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
||||||
const xOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
|
const hasCustomDimensions = [STICKY_NODE_TYPE, PLACEHOLDER_BUTTON].includes(node.type);
|
||||||
const yOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
|
const xOffset = hasCustomDimensions && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
|
||||||
|
const yOffset = hasCustomDimensions && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
|
||||||
|
|
||||||
const x = node.position[0];
|
const x = node.position[0];
|
||||||
const y = node.position[1];
|
const y = node.position[1];
|
||||||
|
|
||||||
|
@ -429,11 +444,29 @@ const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function closestNumberDivisibleBy(inputNumber: number, divisibleBy: number) {
|
||||||
|
const quotient = Math.ceil(inputNumber / divisibleBy);
|
||||||
|
|
||||||
|
// 1st possible closest number
|
||||||
|
const inputNumber1 = divisibleBy * quotient;
|
||||||
|
|
||||||
|
// 2nd possible closest number
|
||||||
|
const inputNumber2 = (inputNumber * divisibleBy) > 0
|
||||||
|
? (divisibleBy * (quotient + 1))
|
||||||
|
: (divisibleBy * (quotient - 1));
|
||||||
|
|
||||||
|
// if true, then inputNumber1 is the required closest number
|
||||||
|
if (Math.abs(inputNumber - inputNumber1) < Math.abs(inputNumber - inputNumber2)) return inputNumber1;
|
||||||
|
|
||||||
|
// else inputNumber2 is the required closest number
|
||||||
|
return inputNumber2;
|
||||||
|
}
|
||||||
|
|
||||||
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
|
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
|
||||||
const targetPosition: XYPosition = [...newPosition];
|
const targetPosition: XYPosition = [...newPosition];
|
||||||
|
|
||||||
targetPosition[0] = targetPosition[0] - (targetPosition[0] % GRID_SIZE);
|
targetPosition[0] = closestNumberDivisibleBy(targetPosition[0], GRID_SIZE);
|
||||||
targetPosition[1] = targetPosition[1] - (targetPosition[1] % GRID_SIZE);
|
targetPosition[1] = closestNumberDivisibleBy(targetPosition[1], GRID_SIZE);
|
||||||
|
|
||||||
if (!movePosition) {
|
if (!movePosition) {
|
||||||
movePosition = [40, 40];
|
movePosition = [40, 40];
|
||||||
|
@ -478,7 +511,8 @@ export const getRelativePosition = (x: number, y: number, scale: number, offset:
|
||||||
|
|
||||||
export const getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => {
|
export const getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => {
|
||||||
const { editorWidth, editorHeight } = getContentDimensions();
|
const { editorWidth, editorHeight } = getContentDimensions();
|
||||||
return getRelativePosition((editorWidth - SIDEBAR_WIDTH) / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
|
|
||||||
|
return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition) => {
|
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition) => {
|
||||||
|
@ -630,16 +664,14 @@ const getContentDimensions = (): { editorWidth: number, editorHeight: number } =
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {offset: XYPosition, zoomLevel: number} => {
|
export const getZoomToFit = (nodes: INodeUi[], addFooterPadding = true): {offset: XYPosition, zoomLevel: number} => {
|
||||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
const { minX, minY, maxX, maxY } = getWorkflowCorners(nodes);
|
||||||
const { editorWidth, editorHeight } = getContentDimensions();
|
const { editorWidth, editorHeight } = getContentDimensions();
|
||||||
const sidebarWidth = addComponentPadding ? SIDEBAR_WIDTH : 0;
|
const footerHeight = addFooterPadding ? 200 : 100;
|
||||||
const headerHeight = addComponentPadding ? HEADER_HEIGHT: 0;
|
|
||||||
const footerHeight = addComponentPadding ? 200 : 100;
|
|
||||||
|
|
||||||
const PADDING = NODE_SIZE * 4;
|
const PADDING = NODE_SIZE * 4;
|
||||||
|
|
||||||
const diffX = maxX - minX + sidebarWidth + PADDING;
|
const diffX = maxX - minX + PADDING;
|
||||||
const scaleX = editorWidth / diffX;
|
const scaleX = editorWidth / diffX;
|
||||||
|
|
||||||
const diffY = maxY - minY + PADDING;
|
const diffY = maxY - minY + PADDING;
|
||||||
|
@ -648,14 +680,14 @@ export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {off
|
||||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||||
|
|
||||||
let xOffset = (minX * -1) * zoomLevel; // find top right corner
|
let xOffset = (minX * -1) * zoomLevel; // find top right corner
|
||||||
xOffset += (editorWidth - sidebarWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
|
xOffset += (editorWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
let yOffset = (minY * -1) * zoomLevel + headerHeight; // find top right corner
|
let yOffset = (minY * -1) * zoomLevel; // find top right corner
|
||||||
yOffset += (editorHeight - headerHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
|
yOffset += (editorHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
return {
|
return {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
offset: [xOffset, yOffset - headerHeight],
|
offset: [closestNumberDivisibleBy(xOffset, GRID_SIZE), closestNumberDivisibleBy(yOffset, GRID_SIZE)],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export class Cron implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Cron',
|
displayName: 'Cron',
|
||||||
name: 'cron',
|
name: 'cron',
|
||||||
icon: 'fa:calendar',
|
icon: 'fa:clock',
|
||||||
group: ['trigger', 'schedule'],
|
group: ['trigger', 'schedule'],
|
||||||
version: 1,
|
version: 1,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
|
@ -24,7 +24,7 @@ export class Cron implements INodeType {
|
||||||
'Your cron trigger will now trigger executions on the schedule you have defined.',
|
'Your cron trigger will now trigger executions on the schedule you have defined.',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Cron',
|
name: 'Cron',
|
||||||
color: '#00FF00',
|
color: '#29a568',
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||||
inputs: [],
|
inputs: [],
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Helpers"]
|
"Core Nodes": [
|
||||||
|
"Helpers",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import _ from 'lodash';
|
||||||
|
|
||||||
export class EmailReadImap implements INodeType {
|
export class EmailReadImap implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'EmailReadImap',
|
displayName: 'Email Trigger (IMAP)',
|
||||||
name: 'emailReadImap',
|
name: 'emailReadImap',
|
||||||
icon: 'fa:inbox',
|
icon: 'fa:inbox',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
|
@ -38,7 +38,7 @@ export class EmailReadImap implements INodeType {
|
||||||
description: 'Triggers the workflow when a new email is received',
|
description: 'Triggers the workflow when a new email is received',
|
||||||
eventTriggerDescription: 'Waiting for you to receive an email',
|
eventTriggerDescription: 'Waiting for you to receive an email',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'IMAP Email',
|
name: 'Email Trigger',
|
||||||
color: '#44AA22',
|
color: '#44AA22',
|
||||||
},
|
},
|
||||||
inputs: [],
|
inputs: [],
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Helpers"]
|
"Core Nodes": [
|
||||||
|
"Helpers",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Execute Workflow',
|
displayName: 'Execute Workflow',
|
||||||
name: 'executeWorkflow',
|
name: 'executeWorkflow',
|
||||||
icon: 'fa:network-wired',
|
icon: 'fa:sign-in-alt',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.executeWorkflowTrigger",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Core Nodes"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.n8n-nodes-base.executeWorkflowTrigger/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generic": []
|
||||||
|
},
|
||||||
|
"subcategories": {
|
||||||
|
"Core Nodes": [
|
||||||
|
"Helpers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import { INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ExecuteWorkflowTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Execute Workflow Trigger',
|
||||||
|
name: 'executeWorkflowTrigger',
|
||||||
|
icon: 'fa:sign-out-alt',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Runs the flow when called by the Execute Workflow node from a different workflow',
|
||||||
|
maxNodes: 1,
|
||||||
|
defaults: {
|
||||||
|
name: 'When Called By Another Workflow',
|
||||||
|
color: '#ff6d5a',
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the \'execute workflow\' node will be output by this node.',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
return this.prepareOutputData(items);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,9 @@
|
||||||
},
|
},
|
||||||
"alias": ["Time", "Scheduler", "Polling"],
|
"alias": ["Time", "Scheduler", "Polling"],
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Flow"]
|
"Core Nodes": [
|
||||||
|
"Flow",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
},
|
},
|
||||||
"alias": ["Watch", "Monitor"],
|
"alias": ["Watch", "Monitor"],
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Files"]
|
"Core Nodes":[
|
||||||
|
"Files",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.manualTrigger",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Core Nodes"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.manualTrigger/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generic": []
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { ITriggerFunctions } from 'n8n-core';
|
||||||
|
import { INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ManualTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Manual Trigger',
|
||||||
|
name: 'manualTrigger',
|
||||||
|
icon: 'fa:mouse-pointer',
|
||||||
|
group: ['trigger', 'input'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Runs the flow on clicking a button in n8n',
|
||||||
|
maxNodes: 1,
|
||||||
|
defaults: {
|
||||||
|
name: "On clicking 'execute'",
|
||||||
|
color: '#909298',
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'This node is where a manual workflow execution starts. To make one, go back to the canvas and click ‘execute workflow’',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
|
const manualTriggerFunction = async () => {
|
||||||
|
this.emit([this.helpers.returnJsonArray([{}])]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
manualTriggerFunction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Flow"]
|
"Core Nodes": [
|
||||||
|
"Flow",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Flow"]
|
"Core Nodes": [
|
||||||
|
"Flow",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Flow"]
|
"Core Nodes": [
|
||||||
|
"Flow",
|
||||||
|
"Other Trigger Nodes"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -437,6 +437,7 @@
|
||||||
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
||||||
"dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
|
"dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
|
||||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
||||||
|
"dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
|
||||||
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
||||||
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
||||||
"dist/nodes/Figma/FigmaTrigger.node.js",
|
"dist/nodes/Figma/FigmaTrigger.node.js",
|
||||||
|
@ -539,6 +540,7 @@
|
||||||
"dist/nodes/Mailjet/Mailjet.node.js",
|
"dist/nodes/Mailjet/Mailjet.node.js",
|
||||||
"dist/nodes/Mailjet/MailjetTrigger.node.js",
|
"dist/nodes/Mailjet/MailjetTrigger.node.js",
|
||||||
"dist/nodes/Mandrill/Mandrill.node.js",
|
"dist/nodes/Mandrill/Mandrill.node.js",
|
||||||
|
"dist/nodes/ManualTrigger/ManualTrigger.node.js",
|
||||||
"dist/nodes/Markdown/Markdown.node.js",
|
"dist/nodes/Markdown/Markdown.node.js",
|
||||||
"dist/nodes/Marketstack/Marketstack.node.js",
|
"dist/nodes/Marketstack/Marketstack.node.js",
|
||||||
"dist/nodes/Matrix/Matrix.node.js",
|
"dist/nodes/Matrix/Matrix.node.js",
|
||||||
|
|
|
@ -909,11 +909,22 @@ export class Workflow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is the actual "start" node
|
const startingNodeTypes = [
|
||||||
const startNodeType = 'n8n-nodes-base.start';
|
'n8n-nodes-base.manualTrigger',
|
||||||
for (const nodeName of nodeNames) {
|
'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
'n8n-nodes-base.start',
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortedNodeNames = Object.values(this.nodes)
|
||||||
|
.sort((a, b) => startingNodeTypes.indexOf(a.type) - startingNodeTypes.indexOf(b.type))
|
||||||
|
.map((n) => n.name);
|
||||||
|
|
||||||
|
for (const nodeName of sortedNodeNames) {
|
||||||
node = this.nodes[nodeName];
|
node = this.nodes[nodeName];
|
||||||
if (node.type === startNodeType) {
|
if (startingNodeTypes.includes(node.type)) {
|
||||||
|
if (node.disabled === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { INode } from './Interfaces';
|
import type { INode } from './Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for instantiating an operational error, e.g. a timeout error.
|
* Class for instantiating an operational error, e.g. a timeout error.
|
||||||
|
@ -17,3 +17,22 @@ export class WorkflowOperationError extends Error {
|
||||||
this.timestamp = Date.now();
|
this.timestamp = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SubworkflowOperationError extends WorkflowOperationError {
|
||||||
|
description = '';
|
||||||
|
|
||||||
|
cause: { message: string; stack: string };
|
||||||
|
|
||||||
|
constructor(message: string, description: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.description = description;
|
||||||
|
|
||||||
|
this.cause = {
|
||||||
|
message,
|
||||||
|
stack: this.stack as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CliWorkflowOperationError extends SubworkflowOperationError {}
|
||||||
|
|
Loading…
Reference in a new issue