feat(editor): Add "Go to Sub-workflow" menu context action to Workflow Tool (#15396)

This commit is contained in:
Ricardo Espinoza
2025-05-15 08:27:02 -04:00
committed by GitHub
parent b11008880b
commit 2b05dbea8c
5 changed files with 305 additions and 145 deletions

View File

@@ -191,128 +191,6 @@ exports[`useContextMenu > Read-only mode > should return the correct actions whe
]
`;
exports[`useContextMenu > should include "Open Sub-workflow" action when node is "Execute Workflow" with a set workflow 1`] = `
[
{
"id": "open",
"label": "Open...",
"shortcut": {
"keys": [
"↵",
],
},
},
{
"disabled": false,
"id": "execute",
"label": "Execute step",
},
{
"disabled": false,
"id": "rename",
"label": "Rename",
"shortcut": {
"keys": [
"Space",
],
},
},
{
"disabled": false,
"id": "open_sub_workflow",
"label": "Open Sub-workflow",
"shortcut": {
"keys": [
"O",
],
"metaKey": true,
"shiftKey": true,
},
},
{
"disabled": false,
"id": "toggle_activation",
"label": "Deactivate",
"shortcut": {
"keys": [
"D",
],
},
},
{
"disabled": true,
"id": "toggle_pin",
"label": "Pin",
"shortcut": {
"keys": [
"p",
],
},
},
{
"id": "copy",
"label": "Copy",
"shortcut": {
"keys": [
"C",
],
"metaKey": true,
},
},
{
"disabled": true,
"id": "duplicate",
"label": "Duplicate",
"shortcut": {
"keys": [
"D",
],
"metaKey": true,
},
},
{
"divided": true,
"id": "tidy_up",
"label": "Tidy up workflow",
"shortcut": {
"altKey": true,
"keys": [
"T",
],
"shiftKey": true,
},
},
{
"disabled": false,
"divided": true,
"id": "select_all",
"label": "Select all",
"shortcut": {
"keys": [
"A",
],
"metaKey": true,
},
},
{
"disabled": false,
"id": "deselect_all",
"label": "Clear selection",
},
{
"disabled": false,
"divided": true,
"id": "delete",
"label": "Delete",
"shortcut": {
"keys": [
"Del",
],
},
},
]
`;
exports[`useContextMenu > should return the correct actions opening the menu from the button 1`] = `
[
{
@@ -614,7 +492,7 @@ exports[`useContextMenu > should return the correct actions when right clicking
]
`;
exports[`useContextMenu > should show "Open Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow 1`] = `
exports[`useContextMenu > should show "Go to Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow 1`] = `
[
{
"id": "open",
@@ -643,7 +521,7 @@ exports[`useContextMenu > should show "Open Sub-workflow" action (disabled) when
{
"disabled": true,
"id": "open_sub_workflow",
"label": "Open Sub-workflow",
"label": "Go to Sub-workflow",
"shortcut": {
"keys": [
"O",
@@ -736,7 +614,129 @@ exports[`useContextMenu > should show "Open Sub-workflow" action (disabled) when
]
`;
exports[`useContextMenu > should show "Open Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow 1`] = `
exports[`useContextMenu > should show "Go to Sub-workflow" action (disabled) when node is "Workflow Tool" without a set workflow 1`] = `
[
{
"id": "open",
"label": "Open...",
"shortcut": {
"keys": [
"↵",
],
},
},
{
"disabled": false,
"id": "execute",
"label": "Execute step",
},
{
"disabled": false,
"id": "rename",
"label": "Rename",
"shortcut": {
"keys": [
"Space",
],
},
},
{
"disabled": true,
"id": "open_sub_workflow",
"label": "Go to Sub-workflow",
"shortcut": {
"keys": [
"O",
],
"metaKey": true,
"shiftKey": true,
},
},
{
"disabled": false,
"id": "toggle_activation",
"label": "Deactivate",
"shortcut": {
"keys": [
"D",
],
},
},
{
"disabled": true,
"id": "toggle_pin",
"label": "Pin",
"shortcut": {
"keys": [
"p",
],
},
},
{
"id": "copy",
"label": "Copy",
"shortcut": {
"keys": [
"C",
],
"metaKey": true,
},
},
{
"disabled": true,
"id": "duplicate",
"label": "Duplicate",
"shortcut": {
"keys": [
"D",
],
"metaKey": true,
},
},
{
"divided": true,
"id": "tidy_up",
"label": "Tidy up workflow",
"shortcut": {
"altKey": true,
"keys": [
"T",
],
"shiftKey": true,
},
},
{
"disabled": false,
"divided": true,
"id": "select_all",
"label": "Select all",
"shortcut": {
"keys": [
"A",
],
"metaKey": true,
},
},
{
"disabled": false,
"id": "deselect_all",
"label": "Clear selection",
},
{
"disabled": false,
"divided": true,
"id": "delete",
"label": "Delete",
"shortcut": {
"keys": [
"Del",
],
},
},
]
`;
exports[`useContextMenu > should show "Go to Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow 1`] = `
[
{
"id": "open",
@@ -765,7 +765,129 @@ exports[`useContextMenu > should show "Open Sub-workflow" action (enabled) when
{
"disabled": false,
"id": "open_sub_workflow",
"label": "Open Sub-workflow",
"label": "Go to Sub-workflow",
"shortcut": {
"keys": [
"O",
],
"metaKey": true,
"shiftKey": true,
},
},
{
"disabled": false,
"id": "toggle_activation",
"label": "Deactivate",
"shortcut": {
"keys": [
"D",
],
},
},
{
"disabled": true,
"id": "toggle_pin",
"label": "Pin",
"shortcut": {
"keys": [
"p",
],
},
},
{
"id": "copy",
"label": "Copy",
"shortcut": {
"keys": [
"C",
],
"metaKey": true,
},
},
{
"disabled": true,
"id": "duplicate",
"label": "Duplicate",
"shortcut": {
"keys": [
"D",
],
"metaKey": true,
},
},
{
"divided": true,
"id": "tidy_up",
"label": "Tidy up workflow",
"shortcut": {
"altKey": true,
"keys": [
"T",
],
"shiftKey": true,
},
},
{
"disabled": false,
"divided": true,
"id": "select_all",
"label": "Select all",
"shortcut": {
"keys": [
"A",
],
"metaKey": true,
},
},
{
"disabled": false,
"id": "deselect_all",
"label": "Clear selection",
},
{
"disabled": false,
"divided": true,
"id": "delete",
"label": "Delete",
"shortcut": {
"keys": [
"Del",
],
},
},
]
`;
exports[`useContextMenu > should show "Go to Sub-workflow" action (enabled) when node is "Workflow Tool" with a set workflow 1`] = `
[
{
"id": "open",
"label": "Open...",
"shortcut": {
"keys": [
"↵",
],
},
},
{
"disabled": false,
"id": "execute",
"label": "Execute step",
},
{
"disabled": false,
"id": "rename",
"label": "Rename",
"shortcut": {
"keys": [
"Space",
],
},
},
{
"disabled": false,
"id": "open_sub_workflow",
"label": "Go to Sub-workflow",
"shortcut": {
"keys": [
"O",

View File

@@ -6,7 +6,12 @@ import { createPinia, setActivePinia } from 'pinia';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { EXECUTE_WORKFLOW_NODE_TYPE, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import {
EXECUTE_WORKFLOW_NODE_TYPE,
NodeConnectionTypes,
NodeHelpers,
WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
} from 'n8n-workflow';
const nodeFactory = (data: Partial<INodeUi> = {}): INodeUi => ({
id: faker.string.uuid(),
@@ -92,7 +97,7 @@ describe('useContextMenu', () => {
expect(targetNodeIds.value).toEqual([sticky.id]);
});
it('should show "Open Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow', () => {
it('should show "Go to Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const executeWorkflow = nodeFactory({
type: EXECUTE_WORKFLOW_NODE_TYPE,
@@ -113,7 +118,7 @@ describe('useContextMenu', () => {
expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
});
it('should show "Open Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow', () => {
it('should show "Go to Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const executeWorkflow = nodeFactory({
type: EXECUTE_WORKFLOW_NODE_TYPE,
@@ -129,6 +134,43 @@ describe('useContextMenu', () => {
expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
});
it('should show "Go to Sub-workflow" action (enabled) when node is "Workflow Tool" with a set workflow', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const executeWorkflow = nodeFactory({
type: WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
parameters: {
workflowId: {
__rl: true,
value: 'qseYRPbw6joqU7RC',
mode: 'list',
cachedResultName: '',
},
},
});
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(executeWorkflow);
open(mockEvent, { source: 'node-right-click', nodeId: executeWorkflow.id });
expect(isOpen.value).toBe(true);
expect(actions.value).toMatchSnapshot();
expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
});
it('should show "Go to Sub-workflow" action (disabled) when node is "Workflow Tool" without a set workflow', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const executeWorkflow = nodeFactory({
type: WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
parameters: {
workflowId: {},
},
});
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(executeWorkflow);
open(mockEvent, { source: 'node-right-click', nodeId: executeWorkflow.id });
expect(isOpen.value).toBe(true);
expect(actions.value).toMatchSnapshot();
expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
});
it('should disable pinning for node that has other inputs then "main"', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const basicChain = nodeFactory({ type: BASIC_CHAIN_NODE_TYPE });

View File

@@ -1,9 +1,5 @@
import type { ActionDropdownItem, XYPosition, INodeUi } from '@/Interface';
import {
NOT_DUPLICATABLE_NODE_TYPES,
STICKY_NODE_TYPE,
EXECUTE_WORKFLOW_NODE_TYPE,
} from '@/constants';
import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
@@ -70,9 +66,10 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
if (targetNodes.value.length !== 1) return false;
const node = targetNodes.value[0];
if (node.type !== EXECUTE_WORKFLOW_NODE_TYPE) return false;
return NodeHelpers.getSubworkflowId(node);
if (!NodeHelpers.isNodeWithWorkflowSelector(node)) return false;
return !!NodeHelpers.getSubworkflowId(node);
});
const targetNodeIds = computed(() => {
@@ -236,7 +233,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
].filter(Boolean) as ActionDropdownItem[];
if (nodes.length === 1) {
const isExecuteWorkflowNode = nodes[0].type === EXECUTE_WORKFLOW_NODE_TYPE;
const singleNodeActions: ActionDropdownItem[] = onlyStickies
? [
{
@@ -270,7 +266,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
},
];
if (isExecuteWorkflowNode) {
if (NodeHelpers.isNodeWithWorkflowSelector(nodes[0])) {
singleNodeActions.push({
id: 'open_sub_workflow',
label: i18n.baseText('contextMenu.openSubworkflow'),

View File

@@ -1513,7 +1513,7 @@
"contextMenu.open": "Open...",
"contextMenu.test": "Execute step",
"contextMenu.rename": "Rename",
"contextMenu.openSubworkflow": "Open Sub-workflow",
"contextMenu.openSubworkflow": "Go to Sub-workflow",
"contextMenu.copy": "Copy | Copy {count} {subject}",
"contextMenu.deactivate": "Deactivate | Deactivate {count} {subject}",
"contextMenu.activate": "Activate | Activate {count} nodes",

View File

@@ -6,7 +6,7 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import { EXECUTE_WORKFLOW_NODE_TYPE } from './Constants';
import { EXECUTE_WORKFLOW_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE } from './Constants';
import { ApplicationError } from './errors/application.error';
import { NodeConnectionTypes } from './Interfaces';
import type {
@@ -1565,15 +1565,15 @@ export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INod
);
}
export function isNodeWithWorkflowSelector(node: INode) {
return [EXECUTE_WORKFLOW_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE].includes(node.type);
}
/**
* Attempts to retrieve the ID of a subworkflow from a execute workflow node.
*/
export function getSubworkflowId(node: INode): string | undefined {
if (
node &&
node.type === EXECUTE_WORKFLOW_NODE_TYPE &&
isResourceLocatorValue(node.parameters.workflowId)
) {
if (isNodeWithWorkflowSelector(node) && isResourceLocatorValue(node.parameters.workflowId)) {
return node.parameters.workflowId.value as string;
}
return;