mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Allow jumping into sub-workflow with shortkey (#15200)
This commit is contained in:
@@ -23,7 +23,7 @@ import type {
|
||||
IParameterLabel,
|
||||
NodeParameterValueType,
|
||||
} from 'n8n-workflow';
|
||||
import { CREDENTIAL_EMPTY_VALUE, NodeHelpers } from 'n8n-workflow';
|
||||
import { CREDENTIAL_EMPTY_VALUE, isResourceLocatorValue, NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
||||
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
||||
@@ -38,7 +38,6 @@ import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
|
||||
import TextEdit from '@/components/TextEdit.vue';
|
||||
|
||||
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
|
||||
import {
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
|
||||
@@ -12,9 +12,9 @@ import { useToast } from '@/composables/useToast';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { getMappedResult } from '@/utils/mappingUtils';
|
||||
import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import {
|
||||
isResourceLocatorValue,
|
||||
type INodeProperties,
|
||||
type IParameterLabel,
|
||||
type NodeParameterValueType,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import {
|
||||
isResourceLocatorValue,
|
||||
type INodeProperties,
|
||||
type NodeParameterValueType,
|
||||
} from 'n8n-workflow';
|
||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { computed } from 'vue';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
@@ -18,19 +18,19 @@ import {
|
||||
getMainAuthField,
|
||||
hasOnlyListMode as hasOnlyListModeUtil,
|
||||
} from '@/utils/nodeTypesUtils';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import stringify from 'fast-json-stable-stringify';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type {
|
||||
INode,
|
||||
INodeListSearchItems,
|
||||
INodeParameterResourceLocator,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodePropertyMode,
|
||||
INodePropertyModeTypeOptions,
|
||||
NodeParameterValue,
|
||||
import {
|
||||
isResourceLocatorValue,
|
||||
type INode,
|
||||
type INodeListSearchItems,
|
||||
type INodeParameterResourceLocator,
|
||||
type INodeParameters,
|
||||
type INodeProperties,
|
||||
type INodePropertyMode,
|
||||
type INodePropertyModeTypeOptions,
|
||||
type NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
computed,
|
||||
|
||||
@@ -59,7 +59,7 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [elements: CanvasNode[]];
|
||||
'update:node:position': [id: string, position: XYPosition];
|
||||
'update:nodes:position': [events: CanvasNodeMoveEvent[]];
|
||||
'update:node:activated': [id: string];
|
||||
'update:node:activated': [id: string, event?: MouseEvent];
|
||||
'update:node:deactivated': [id: string];
|
||||
'update:node:enabled': [id: string];
|
||||
'update:node:selected': [id?: string];
|
||||
@@ -95,6 +95,7 @@ const emit = defineEmits<{
|
||||
'create:workflow': [];
|
||||
'drag-and-drop': [position: XYPosition, event: DragEvent];
|
||||
'tidy-up': [CanvasLayoutEvent];
|
||||
'open:sub-workflow': [nodeId: string];
|
||||
}>();
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -266,6 +267,7 @@ function selectUpstreamNodes(id: string) {
|
||||
|
||||
const keyMap = computed(() => {
|
||||
const readOnlyKeymap = {
|
||||
ctrl_shift_o: emitWithLastSelectedNode((id) => emit('open:sub-workflow', id)),
|
||||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||
enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)),
|
||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||
@@ -368,9 +370,9 @@ function onSelectionEnd() {
|
||||
}
|
||||
}
|
||||
|
||||
function onSetNodeActivated(id: string) {
|
||||
function onSetNodeActivated(id: string, event?: MouseEvent) {
|
||||
props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:activated' });
|
||||
emit('update:node:activated', id);
|
||||
emit('update:node:activated', id, event);
|
||||
}
|
||||
|
||||
function onSetNodeDeactivated(id: string) {
|
||||
@@ -661,6 +663,9 @@ async function onContextMenuAction(action: ContextMenuAction, nodeIds: string[])
|
||||
return props.eventBus.emit('nodes:action', { ids: nodeIds, action: 'update:sticky:color' });
|
||||
case 'tidy_up':
|
||||
return await onTidyUp({ source: 'context-menu' });
|
||||
case 'open_sub_workflow': {
|
||||
return emit('open:sub-workflow', nodeIds[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const emit = defineEmits<{
|
||||
run: [id: string];
|
||||
select: [id: string, selected: boolean];
|
||||
toggle: [id: string];
|
||||
activate: [id: string];
|
||||
activate: [id: string, event: MouseEvent];
|
||||
deactivate: [id: string];
|
||||
'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click'];
|
||||
update: [id: string, parameters: Record<string, unknown>];
|
||||
@@ -240,8 +240,8 @@ function onDisabledToggle() {
|
||||
emit('toggle', props.id);
|
||||
}
|
||||
|
||||
function onActivate() {
|
||||
emit('activate', props.id);
|
||||
function onActivate(id: string, event: MouseEvent) {
|
||||
emit('activate', id, event);
|
||||
}
|
||||
|
||||
function onDeactivate() {
|
||||
|
||||
@@ -12,7 +12,7 @@ const i18n = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'open:contextmenu': [event: MouseEvent];
|
||||
activate: [id: string];
|
||||
activate: [id: string, event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const { initialized, viewport } = useCanvas();
|
||||
@@ -130,8 +130,8 @@ function openContextMenu(event: MouseEvent) {
|
||||
emit('open:contextmenu', event);
|
||||
}
|
||||
|
||||
function onActivate() {
|
||||
emit('activate', id.value);
|
||||
function onActivate(event: MouseEvent) {
|
||||
emit('activate', id.value, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -191,6 +191,128 @@ 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": "Test 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`] = `
|
||||
[
|
||||
{
|
||||
@@ -492,6 +614,250 @@ 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`] = `
|
||||
[
|
||||
{
|
||||
"id": "open",
|
||||
"label": "Open...",
|
||||
"shortcut": {
|
||||
"keys": [
|
||||
"↵",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"id": "execute",
|
||||
"label": "Test step",
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"id": "rename",
|
||||
"label": "Rename",
|
||||
"shortcut": {
|
||||
"keys": [
|
||||
"Space",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"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 show "Open Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "open",
|
||||
"label": "Open...",
|
||||
"shortcut": {
|
||||
"keys": [
|
||||
"↵",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"id": "execute",
|
||||
"label": "Test 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 support opening and closing (default = right click on canvas) 1`] = `
|
||||
[
|
||||
{
|
||||
|
||||
@@ -2008,6 +2008,15 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
workflowsStore.addToWorkflowMetadata({ templateId: `${id}` });
|
||||
}
|
||||
|
||||
function tryToOpenSubworkflowInNewTab(nodeId: string): boolean {
|
||||
const node = workflowsStore.getNodeById(nodeId);
|
||||
if (!node) return false;
|
||||
const subWorkflowId = NodeHelpers.getSubworkflowId(node);
|
||||
if (!subWorkflowId) return false;
|
||||
window.open(`${rootStore.baseUrl}workflow/${subWorkflowId}`, '_blank');
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
lastClickPosition,
|
||||
editableWorkflow,
|
||||
@@ -2058,5 +2067,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
openExecution,
|
||||
toggleChatOpen,
|
||||
importTemplate,
|
||||
tryToOpenSubworkflowInNewTab,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
|
||||
import { EXECUTE_WORKFLOW_NODE_TYPE, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
const nodeFactory = (data: Partial<INodeUi> = {}): INodeUi => ({
|
||||
id: faker.string.uuid(),
|
||||
@@ -92,6 +92,43 @@ 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', () => {
|
||||
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
|
||||
const executeWorkflow = nodeFactory({
|
||||
type: EXECUTE_WORKFLOW_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 "Open 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,
|
||||
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 });
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ActionDropdownItem, XYPosition, INodeUi } from '@/Interface';
|
||||
import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
|
||||
import {
|
||||
NOT_DUPLICATABLE_NODE_TYPES,
|
||||
STICKY_NODE_TYPE,
|
||||
EXECUTE_WORKFLOW_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
@@ -33,6 +37,7 @@ export type ContextMenuAction =
|
||||
| 'add_node'
|
||||
| 'add_sticky'
|
||||
| 'change_color'
|
||||
| 'open_sub_workflow'
|
||||
| 'tidy_up';
|
||||
|
||||
const position = ref<XYPosition>([0, 0]);
|
||||
@@ -61,6 +66,15 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||
workflowsStore.workflow.isArchived,
|
||||
);
|
||||
|
||||
const canOpenSubworkflow = computed(() => {
|
||||
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);
|
||||
});
|
||||
|
||||
const targetNodeIds = computed(() => {
|
||||
if (!isOpen.value || !target.value) return [];
|
||||
|
||||
@@ -128,6 +142,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||
|
||||
const nodes = targetNodes.value;
|
||||
const onlyStickies = nodes.every((node) => node.type === STICKY_NODE_TYPE);
|
||||
|
||||
const i18nOptions = {
|
||||
adjustToNumber: nodes.length,
|
||||
interpolate: {
|
||||
@@ -221,7 +236,8 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||
].filter(Boolean) as ActionDropdownItem[];
|
||||
|
||||
if (nodes.length === 1) {
|
||||
const singleNodeActions = onlyStickies
|
||||
const isExecuteWorkflowNode = nodes[0].type === EXECUTE_WORKFLOW_NODE_TYPE;
|
||||
const singleNodeActions: ActionDropdownItem[] = onlyStickies
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
@@ -253,6 +269,15 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||
disabled: isReadOnly.value,
|
||||
},
|
||||
];
|
||||
|
||||
if (isExecuteWorkflowNode) {
|
||||
singleNodeActions.push({
|
||||
id: 'open_sub_workflow',
|
||||
label: i18n.baseText('contextMenu.openSubworkflow'),
|
||||
shortcut: { shiftKey: true, metaKey: true, keys: ['O'] },
|
||||
disabled: !canOpenSubworkflow.value,
|
||||
});
|
||||
}
|
||||
// Add actions only available for a single node
|
||||
menuActions.unshift(...singleNodeActions);
|
||||
}
|
||||
|
||||
@@ -1516,6 +1516,7 @@
|
||||
"contextMenu.open": "Open...",
|
||||
"contextMenu.test": "Test step",
|
||||
"contextMenu.rename": "Rename",
|
||||
"contextMenu.openSubworkflow": "Open Sub-workflow",
|
||||
"contextMenu.copy": "Copy | Copy {count} {subject}",
|
||||
"contextMenu.deactivate": "Deactivate | Deactivate {count} {subject}",
|
||||
"contextMenu.activate": "Activate | Activate {count} nodes",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from 'n8n-workflow';
|
||||
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
||||
import { isExpression } from './expressions';
|
||||
|
||||
const validJsIdNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
||||
|
||||
@@ -16,17 +16,17 @@ import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import { isJsonKeyObject } from '@/utils/typesUtils';
|
||||
import type {
|
||||
IDataObject,
|
||||
INodeCredentialDescription,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValueType,
|
||||
ResourceMapperField,
|
||||
Themed,
|
||||
import {
|
||||
isResourceLocatorValue,
|
||||
type IDataObject,
|
||||
type INodeCredentialDescription,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type INodeTypeDescription,
|
||||
type NodeParameterValueType,
|
||||
type ResourceMapperField,
|
||||
type Themed,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
INodeParameterResourceLocator,
|
||||
INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
TriggerPanelDefinition,
|
||||
@@ -27,10 +26,6 @@ export const checkExhaustive = (value: never): never => {
|
||||
throw new Error(`Unhandled value: ${value}`);
|
||||
};
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
return Boolean(typeof value === 'object' && value && 'mode' in value && 'value' in value);
|
||||
}
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ const {
|
||||
setNodeActiveByName,
|
||||
clearNodeActive,
|
||||
addConnections,
|
||||
tryToOpenSubworkflowInNewTab,
|
||||
importWorkflowData,
|
||||
fetchWorkflowDataFromUrl,
|
||||
resetWorkspace,
|
||||
@@ -665,10 +666,22 @@ function onClickNode() {
|
||||
closeNodeCreator();
|
||||
}
|
||||
|
||||
function onSetNodeActivated(id: string) {
|
||||
function onSetNodeActivated(id: string, event?: MouseEvent) {
|
||||
// Handle Ctrl/Cmd + Double Click case
|
||||
if (event?.metaKey || event?.ctrlKey) {
|
||||
const didOpen = tryToOpenSubworkflowInNewTab(id);
|
||||
if (didOpen) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setNodeActive(id);
|
||||
}
|
||||
|
||||
function onOpenSubWorkflow(id: string) {
|
||||
tryToOpenSubworkflowInNewTab(id);
|
||||
}
|
||||
|
||||
function onSetNodeDeactivated() {
|
||||
clearNodeActive();
|
||||
}
|
||||
@@ -1888,6 +1901,7 @@ onBeforeUnmount(() => {
|
||||
@update:node:parameters="onUpdateNodeParameters"
|
||||
@update:node:inputs="onUpdateNodeInputs"
|
||||
@update:node:outputs="onUpdateNodeOutputs"
|
||||
@open:sub-workflow="onOpenSubWorkflow"
|
||||
@click:node="onClickNode"
|
||||
@click:node:add="onClickNodeAdd"
|
||||
@run:node="onRunWorkflowToNode"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import get from 'lodash/get';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { EXECUTE_WORKFLOW_NODE_TYPE } from './Constants';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
import { NodeConnectionTypes } from './Interfaces';
|
||||
import type {
|
||||
@@ -39,6 +40,7 @@ import type {
|
||||
import { validateFilterParameter } from './NodeParameters/FilterParameter';
|
||||
import {
|
||||
isFilterValue,
|
||||
isResourceLocatorValue,
|
||||
isResourceMapperValue,
|
||||
isValidResourceLocatorParameterValue,
|
||||
} from './type-guards';
|
||||
@@ -1562,3 +1564,17 @@ export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INod
|
||||
isTriggerNode(nodeTypeData)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
) {
|
||||
return node.parameters.workflowId.value as string;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,25 +22,19 @@ import type {
|
||||
ITaskData,
|
||||
IWorkflowDataProxyAdditionalKeys,
|
||||
IWorkflowDataProxyData,
|
||||
INodeParameterResourceLocator,
|
||||
NodeParameterValueType,
|
||||
WorkflowExecuteMode,
|
||||
ProxyInput,
|
||||
INode,
|
||||
} from './Interfaces';
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
import { isResourceLocatorValue } from './type-guards';
|
||||
import { deepCopy, isObjectEmpty } from './utils';
|
||||
import type { Workflow } from './Workflow';
|
||||
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||
import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
return Boolean(
|
||||
typeof value === 'object' && value && 'mode' in value && 'value' in value && '__rl' in value,
|
||||
);
|
||||
}
|
||||
|
||||
const isScriptingNode = (nodeName: string, workflow: Workflow) => {
|
||||
const node = workflow.getNode(nodeName);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export {
|
||||
isINodePropertyCollectionList,
|
||||
isINodePropertyOptionsList,
|
||||
isResourceMapperValue,
|
||||
isResourceLocatorValue,
|
||||
isFilterValue,
|
||||
} from './type-guards';
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import type {
|
||||
FilterValue,
|
||||
} from './Interfaces';
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
return Boolean(
|
||||
typeof value === 'object' && value && 'mode' in value && 'value' in value && '__rl' in value,
|
||||
);
|
||||
}
|
||||
|
||||
export const isINodeProperties = (
|
||||
item: INodePropertyOptions | INodeProperties | INodePropertyCollection,
|
||||
): item is INodeProperties => 'name' in item && 'type' in item && !('value' in item);
|
||||
|
||||
Reference in New Issue
Block a user