feat(editor): Add capability to open NDV and rename node (no-changelog) (#9712)

This commit is contained in:
Alex Grozav 2024-06-17 15:46:55 +03:00 committed by GitHub
parent 87cb199745
commit 12604fe1da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 568 additions and 97 deletions

View file

@ -45,23 +45,37 @@ export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes {
}; };
} }
export function createTestWorkflowObject(options: { export function createTestWorkflowObject({
id = uuid(),
name = 'Test Workflow',
nodes = [],
connections = {},
active = false,
nodeTypes = {},
staticData = {},
settings = {},
pinData = {},
}: {
id?: string; id?: string;
name?: string; name?: string;
nodes: INode[]; nodes?: INode[];
connections: IConnections; connections?: IConnections;
active?: boolean; active?: boolean;
nodeTypes?: INodeTypeData; nodeTypes?: INodeTypeData;
staticData?: IDataObject; staticData?: IDataObject;
settings?: IWorkflowSettings; settings?: IWorkflowSettings;
pinData?: IPinData; pinData?: IPinData;
}) { } = {}) {
return new Workflow({ return new Workflow({
...options, id,
id: options.id ?? uuid(), name,
active: options.active ?? false, nodes,
nodeTypes: createTestNodeTypes(options.nodeTypes), connections,
connections: options.connections ?? {}, active,
staticData,
settings,
pinData,
nodeTypes: createTestNodeTypes(nodeTypes),
}); });
} }

View file

@ -14,6 +14,7 @@ const $style = useCssModule();
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [elements: CanvasElement[]]; 'update:modelValue': [elements: CanvasElement[]];
'update:node:position': [id: string, position: { x: number; y: number }]; 'update:node:position': [id: string, position: { x: number; y: number }];
'update:node:active': [id: string];
'delete:node': [id: string]; 'delete:node': [id: string];
'delete:connection': [connection: Connection]; 'delete:connection': [connection: Connection];
'create:connection': [connection: Connection]; 'create:connection': [connection: Connection];
@ -52,6 +53,10 @@ function onNodeDragStop(e: NodeDragEvent) {
}); });
} }
function onSetNodeActive(id: string) {
emit('update:node:active', id);
}
function onDeleteNode(id: string) { function onDeleteNode(id: string) {
emit('delete:node', id); emit('delete:node', id);
} }
@ -97,7 +102,7 @@ function onMouseLeaveEdge(event: EdgeMouseEvent) {
@connect="onConnect" @connect="onConnect"
> >
<template #node-canvas-node="canvasNodeProps"> <template #node-canvas-node="canvasNodeProps">
<CanvasNode v-bind="canvasNodeProps" @delete="onDeleteNode" /> <CanvasNode v-bind="canvasNodeProps" @delete="onDeleteNode" @activate="onSetNodeActive" />
</template> </template>
<template #edge-canvas-edge="canvasEdgeProps"> <template #edge-canvas-edge="canvasEdgeProps">

View file

@ -17,6 +17,7 @@ import type { NodeProps } from '@vue-flow/core';
const emit = defineEmits<{ const emit = defineEmits<{
delete: [id: string]; delete: [id: string];
activate: [id: string];
}>(); }>();
const props = defineProps<NodeProps<CanvasElementData>>(); const props = defineProps<NodeProps<CanvasElementData>>();
@ -97,6 +98,10 @@ provide(CanvasNodeKey, {
function onDelete() { function onDelete() {
emit('delete', props.id); emit('delete', props.id);
} }
function onActivate() {
emit('activate', props.id);
}
</script> </script>
<template> <template>
@ -132,7 +137,7 @@ function onDelete() {
@delete="onDelete" @delete="onDelete"
/> />
<CanvasNodeRenderer v-if="nodeType"> <CanvasNodeRenderer v-if="nodeType" @dblclick="onActivate">
<NodeIcon :node-type="nodeType" :size="40" :shrink="false" /> <NodeIcon :node-type="nodeType" :size="40" :shrink="false" />
<!-- :color-default="iconColorDefault"--> <!-- :color-default="iconColorDefault"-->
<!-- :disabled="data.disabled"--> <!-- :disabled="data.disabled"-->

View file

@ -6,14 +6,16 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useHistoryStore } from '@/stores/history.store'; import { useHistoryStore } from '@/stores/history.store';
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { createTestNode } from '@/__tests__/mocks'; import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import type { IConnection } from 'n8n-workflow'; import type { IConnection } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv.store';
describe('useCanvasOperations', () => { describe('useCanvasOperations', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>; let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let uiStore: ReturnType<typeof useUIStore>; let uiStore: ReturnType<typeof useUIStore>;
let ndvStore: ReturnType<typeof useNDVStore>;
let historyStore: ReturnType<typeof useHistoryStore>; let historyStore: ReturnType<typeof useHistoryStore>;
let canvasOperations: ReturnType<typeof useCanvasOperations>; let canvasOperations: ReturnType<typeof useCanvasOperations>;
@ -23,6 +25,7 @@ describe('useCanvasOperations', () => {
workflowsStore = useWorkflowsStore(); workflowsStore = useWorkflowsStore();
uiStore = useUIStore(); uiStore = useUIStore();
ndvStore = useNDVStore();
historyStore = useHistoryStore(); historyStore = useHistoryStore();
canvasOperations = useCanvasOperations(); canvasOperations = useCanvasOperations();
}); });
@ -134,6 +137,93 @@ describe('useCanvasOperations', () => {
}); });
}); });
describe('renameNode', () => {
it('should rename node', async () => {
const oldName = 'Old Node';
const newName = 'New Node';
const workflowObject = createTestWorkflowObject();
workflowObject.renameNode = vi.fn();
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue(workflowObject);
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
await canvasOperations.renameNode(oldName, newName);
expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
expect(ndvStore.activeNodeName).toBe(newName);
});
it('should not rename node when new name is same as old name', async () => {
const oldName = 'Old Node';
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
await canvasOperations.renameNode(oldName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
});
});
describe('revertRenameNode', () => {
it('should revert node renaming', async () => {
const oldName = 'Old Node';
const currentName = 'New Node';
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName });
ndvStore.activeNodeName = currentName;
await canvasOperations.revertRenameNode(currentName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
});
it('should not revert node renaming when old name is same as new name', async () => {
const oldName = 'Old Node';
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName;
await canvasOperations.revertRenameNode(oldName, oldName);
expect(ndvStore.activeNodeName).toBe(oldName);
});
});
describe('setNodeActive', () => {
it('should set active node name when node exists', () => {
const nodeId = 'node1';
const nodeName = 'Node 1';
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
ndvStore.activeNodeName = '';
canvasOperations.setNodeActive(nodeId);
expect(ndvStore.activeNodeName).toBe(nodeName);
});
it('should not change active node name when node does not exist', () => {
const nodeId = 'node1';
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
ndvStore.activeNodeName = 'Existing Node';
canvasOperations.setNodeActive(nodeId);
expect(ndvStore.activeNodeName).toBe('Existing Node');
});
});
describe('setNodeActiveByName', () => {
it('should set active node name', () => {
const nodeName = 'Node 1';
ndvStore.activeNodeName = '';
canvasOperations.setNodeActiveByName(nodeName);
expect(ndvStore.activeNodeName).toBe(nodeName);
});
});
describe('createConnection', () => { describe('createConnection', () => {
it('should not create a connection if source node does not exist', () => { it('should not create a connection if source node does not exist', () => {
const addConnectionSpy = vi const addConnectionSpy = vi

View file

@ -6,15 +6,22 @@ import { useHistoryStore } from '@/stores/history.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { MoveNodeCommand, RemoveConnectionCommand, RemoveNodeCommand } from '@/models/history'; import {
MoveNodeCommand,
RemoveConnectionCommand,
RemoveNodeCommand,
RenameNodeCommand,
} from '@/models/history';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import { mapCanvasConnectionToLegacyConnection } from '@/utils/canvasUtilsV2'; import { getUniqueNodeName, mapCanvasConnectionToLegacyConnection } from '@/utils/canvasUtilsV2';
import type { IConnection } from 'n8n-workflow'; import type { IConnection } from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv.store';
export function useCanvasOperations() { export function useCanvasOperations() {
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const historyStore = useHistoryStore(); const historyStore = useHistoryStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const ndvStore = useNDVStore();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
@ -51,6 +58,45 @@ export function useCanvasOperations() {
} }
} }
async function renameNode(currentName: string, newName: string, { trackHistory = false } = {}) {
if (currentName === newName) {
return;
}
if (trackHistory) {
historyStore.startRecordingUndo();
}
newName = getUniqueNodeName(newName, workflowsStore.canvasNames);
// Rename the node and update the connections
const workflow = workflowsStore.getCurrentWorkflow(true);
workflow.renameNode(currentName, newName);
if (trackHistory) {
historyStore.pushCommandToUndo(new RenameNodeCommand(currentName, newName));
}
// Update also last selected node and execution data
workflowsStore.renameNodeSelectedAndExecution({ old: currentName, new: newName });
workflowsStore.setNodes(Object.values(workflow.nodes));
workflowsStore.setConnections(workflow.connectionsBySourceNode);
const isRenamingActiveNode = ndvStore.activeNodeName === currentName;
if (isRenamingActiveNode) {
ndvStore.activeNodeName = newName;
}
if (trackHistory) {
historyStore.stopRecordingUndo();
}
}
async function revertRenameNode(currentName: string, previousName: string) {
await renameNode(currentName, previousName);
}
function deleteNode(id: string, { trackHistory = false, trackBulk = true } = {}) { function deleteNode(id: string, { trackHistory = false, trackBulk = true } = {}) {
const node = workflowsStore.getNodeById(id); const node = workflowsStore.getNodeById(id);
if (!node) { if (!node) {
@ -100,6 +146,19 @@ export function useCanvasOperations() {
} }
} }
function setNodeActive(id: string) {
const node = workflowsStore.getNodeById(id);
if (!node) {
return;
}
ndvStore.activeNodeName = node.name;
}
function setNodeActiveByName(name: string) {
ndvStore.activeNodeName = name;
}
/** /**
* Connection operations * Connection operations
*/ */
@ -204,6 +263,10 @@ export function useCanvasOperations() {
return { return {
updateNodePosition, updateNodePosition,
setNodeActive,
setNodeActiveByName,
renameNode,
revertRenameNode,
deleteNode, deleteNode,
revertDeleteNode, revertDeleteNode,
trackDeleteNode, trackDeleteNode,

View file

@ -1,16 +1,41 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { STORES, TRIGGER_NODE_CREATOR_VIEW } from '@/constants'; import {
AI_NODE_CREATOR_VIEW,
NODE_CREATOR_OPEN_SOURCES,
REGULAR_NODE_CREATOR_VIEW,
STORES,
TRIGGER_NODE_CREATOR_VIEW,
} from '@/constants';
import type { import type {
NodeFilterType, NodeFilterType,
NodeCreatorOpenSource, NodeCreatorOpenSource,
SimplifiedNodeType, SimplifiedNodeType,
ActionsRecord, ActionsRecord,
ToggleNodeCreatorOptions,
AIAssistantConnectionInfo,
} from '@/Interface'; } from '@/Interface';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { transformNodeType } from '@/components/Node/NodeCreator/utils'; import { transformNodeType } from '@/components/Node/NodeCreator/utils';
import type { INodeInputConfiguration } from 'n8n-workflow';
import { NodeConnectionType, nodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry';
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
const workflowsStore = useWorkflowsStore();
const ndvStore = useNDVStore();
const uiStore = useUIStore();
const nodeTypesStore = useNodeTypesStore();
const externalHooks = useExternalHooks();
const telemetry = useTelemetry();
const selectedView = ref<NodeFilterType>(TRIGGER_NODE_CREATOR_VIEW); const selectedView = ref<NodeFilterType>(TRIGGER_NODE_CREATOR_VIEW);
const mergedNodes = ref<SimplifiedNodeType[]>([]); const mergedNodes = ref<SimplifiedNodeType[]>([]);
const actions = ref<ActionsRecord<typeof mergedNodes.value>>({}); const actions = ref<ActionsRecord<typeof mergedNodes.value>>({});
@ -42,6 +67,173 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
openSource.value = view; openSource.value = view;
} }
function openNodeCreator({
source,
createNodeActive,
nodeCreatorView,
}: ToggleNodeCreatorOptions) {
if (createNodeActive === uiStore.isCreateNodeActive) {
return;
}
if (!nodeCreatorView) {
nodeCreatorView =
workflowsStore.workflowTriggerNodes.length > 0
? REGULAR_NODE_CREATOR_VIEW
: TRIGGER_NODE_CREATOR_VIEW;
}
// Default to the trigger tab in node creator if there's no trigger node yet
setSelectedView(nodeCreatorView);
let mode;
switch (selectedView.value) {
case AI_NODE_CREATOR_VIEW:
mode = 'ai';
break;
case REGULAR_NODE_CREATOR_VIEW:
mode = 'regular';
break;
default:
mode = 'regular';
}
uiStore.isCreateNodeActive = createNodeActive;
if (createNodeActive && source) {
setOpenSource(source);
}
void externalHooks.run('nodeView.createNodeActiveChanged', {
source,
mode,
createNodeActive,
});
trackNodesPanelActiveChanged({
source,
mode,
createNodeActive,
workflowId: workflowsStore.workflowId,
});
}
function trackNodesPanelActiveChanged({
source,
mode,
createNodeActive,
workflowId,
}: {
source?: string;
mode?: string;
createNodeActive?: boolean;
workflowId?: string;
}) {
telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', {
source,
mode,
createNodeActive,
workflow_id: workflowId,
});
}
function openSelectiveNodeCreator({
connectionType,
node,
creatorView,
}: {
connectionType: NodeConnectionType;
node: string;
creatorView?: NodeFilterType;
}) {
const nodeName = node ?? ndvStore.activeNodeName;
const nodeData = nodeName ? workflowsStore.getNodeByName(nodeName) : null;
ndvStore.activeNodeName = null;
setTimeout(() => {
if (creatorView) {
openNodeCreator({
createNodeActive: true,
nodeCreatorView: creatorView,
});
} else if (connectionType && nodeData) {
insertNodeAfterSelected({
index: 0,
endpointUuid: `${nodeData.id}-input${connectionType}0`,
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
outputType: connectionType,
sourceId: nodeData.id,
});
}
});
}
function insertNodeAfterSelected(info: AIAssistantConnectionInfo) {
const type = info.outputType ?? NodeConnectionType.Main;
// Get the node and set it as active that new nodes
// which get created get automatically connected
// to it.
const sourceNode = workflowsStore.getNodeById(info.sourceId);
if (!sourceNode) {
return;
}
uiStore.lastSelectedNode = sourceNode.name;
uiStore.lastSelectedNodeEndpointUuid =
info.endpointUuid ?? info.connection?.target.jtk?.endpoint.uuid;
uiStore.lastSelectedNodeOutputIndex = info.index;
// canvasStore.newNodeInsertPosition = null;
// @TODO Add connection to store
// if (info.connection) {
// canvasStore.setLastSelectedConnection(info.connection);
// }
openNodeCreator({
source: info.eventSource,
createNodeActive: true,
nodeCreatorView: info.nodeCreatorView,
});
// TODO: The animation is a bit glitchy because we're updating view stack immediately
// after the node creator is opened
const isOutput = info.connection?.endpoints[0].parameters.connection === 'source';
const isScopedConnection =
type !== NodeConnectionType.Main && nodeConnectionTypes.includes(type);
if (isScopedConnection) {
useViewStacks()
.gotoCompatibleConnectionView(type, isOutput, getNodeCreatorFilter(sourceNode.name, type))
.catch(() => {});
}
}
function getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
let filter;
const workflow = workflowsStore.getCurrentWorkflow();
const workflowNode = workflow.getNode(nodeName);
if (!workflowNode) return { nodes: [] };
const nodeType = nodeTypesStore.getNodeType(workflowNode?.type, workflowNode.typeVersion);
if (nodeType) {
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
const filterFound = inputs.filter((input) => {
if (typeof input === 'string' || input.type !== outputType || !input.filter) {
// No filters defined or wrong connection type
return false;
}
return true;
}) as INodeInputConfiguration[];
if (filterFound.length) {
filter = filterFound[0].filter;
}
}
return filter;
}
return { return {
openSource, openSource,
selectedView, selectedView,
@ -53,6 +245,8 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
setOpenSource, setOpenSource,
setActions, setActions,
setMergeNodes, setMergeNodes,
openNodeCreator,
openSelectiveNodeCreator,
allNodeCreatorNodes, allNodeCreatorNodes,
}; };
}); });

View file

@ -938,6 +938,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
} }
function setNodes(nodes: INodeUi[]): void {
workflow.value.nodes = nodes;
}
function setConnections(connections: IConnections): void {
workflow.value.connections = connections;
}
function resetAllNodesIssues(): boolean { function resetAllNodesIssues(): boolean {
workflow.value.nodes.forEach((node) => { workflow.value.nodes.forEach((node) => {
node.issues = undefined; node.issues = undefined;
@ -1650,5 +1658,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
removeNodeById, removeNodeById,
removeNodeConnectionsById, removeNodeConnectionsById,
removeNodeExecutionDataById, removeNodeExecutionDataById,
setNodes,
setConnections,
}; };
}); });

View file

@ -12,26 +12,20 @@ import type {
AddedNodesAndConnections, AddedNodesAndConnections,
INodeUi, INodeUi,
ITag, ITag,
IUpdateInformation,
IWorkflowDataUpdate,
ToggleNodeCreatorOptions, ToggleNodeCreatorOptions,
XYPosition, XYPosition,
} from '@/Interface'; } from '@/Interface';
import useWorkflowsEEStore from '@/stores/workflows.ee.store';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import type { CanvasElement } from '@/types'; import type { CanvasElement } from '@/types';
import { import { EnterpriseEditionFeature, VIEWS } from '@/constants';
EnterpriseEditionFeature,
AI_NODE_CREATOR_VIEW,
REGULAR_NODE_CREATOR_VIEW,
TRIGGER_NODE_CREATOR_VIEW,
VIEWS,
} from '@/constants';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import * as NodeViewUtils from '@/utils/nodeViewUtils'; import * as NodeViewUtils from '@/utils/nodeViewUtils';
import type { IConnection, INodeTypeDescription } from 'n8n-workflow'; import type { ExecutionSummary, IConnection, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -44,24 +38,27 @@ import { useCollaborationStore } from '@/stores/collaboration.store';
import { getUniqueNodeName } from '@/utils/canvasUtilsV2'; import { getUniqueNodeName } from '@/utils/canvasUtilsV2';
import { historyBus } from '@/models/history'; import { historyBus } from '@/models/history';
import { useCanvasOperations } from '@/composables/useCanvasOperations'; import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useExecutionsStore } from '@/stores/executions.store';
const NodeCreation = defineAsyncComponent( const NodeCreation = defineAsyncComponent(
async () => await import('@/components/Node/NodeCreation.vue'), async () => await import('@/components/Node/NodeCreation.vue'),
); );
const NodeDetailsView = defineAsyncComponent(
async () => await import('@/components/NodeDetailsView.vue'),
);
const $style = useCssModule(); const $style = useCssModule();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const i18n = useI18n(); const i18n = useI18n();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const toast = useToast(); const toast = useToast();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const workflowsEEStore = useWorkflowsEEStore();
const tagsStore = useTagsStore(); const tagsStore = useTagsStore();
const sourceControlStore = useSourceControlStore(); const sourceControlStore = useSourceControlStore();
const nodeCreatorStore = useNodeCreatorStore(); const nodeCreatorStore = useNodeCreatorStore();
@ -71,20 +68,31 @@ const environmentsStore = useEnvironmentsStore();
const externalSecretsStore = useExternalSecretsStore(); const externalSecretsStore = useExternalSecretsStore();
const rootStore = useRootStore(); const rootStore = useRootStore();
const collaborationStore = useCollaborationStore(); const collaborationStore = useCollaborationStore();
const executionsStore = useExecutionsStore();
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
const { const {
updateNodePosition, updateNodePosition,
renameNode,
revertRenameNode,
setNodeActive,
deleteNode, deleteNode,
revertDeleteNode, revertDeleteNode,
createConnection, createConnection,
deleteConnection, deleteConnection,
revertDeleteConnection, revertDeleteConnection,
setNodeActiveByName,
} = useCanvasOperations(); } = useCanvasOperations();
const isLoading = ref(true); const isLoading = ref(true);
const readOnlyNotification = ref<null | { visible: boolean }>(null); const readOnlyNotification = ref<null | { visible: boolean }>(null);
const isProductionExecutionPreview = ref(false);
const isExecutionPreview = ref(false);
const canOpenNDV = ref(true);
const hideNodeIssues = ref(false);
const workflowId = computed<string>(() => route.params.workflowId as string); const workflowId = computed<string>(() => route.params.workflowId as string);
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]); const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);
@ -151,6 +159,7 @@ async function initialize() {
initializeEditableWorkflow(workflowId.value); initializeEditableWorkflow(workflowId.value);
addUndoRedoEventBindings(); addUndoRedoEventBindings();
addPostMessageEventBindings();
if (window.parent) { if (window.parent) {
window.parent.postMessage( window.parent.postMessage(
@ -163,6 +172,7 @@ async function initialize() {
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
removePostMessageEventBindings();
removeUndoRedoEventBindings(); removeUndoRedoEventBindings();
}); });
@ -172,7 +182,7 @@ function addUndoRedoEventBindings() {
historyBus.on('revertRemoveNode', onRevertDeleteNode); historyBus.on('revertRemoveNode', onRevertDeleteNode);
// historyBus.on('revertAddConnection', onRevertAddConnection); // historyBus.on('revertAddConnection', onRevertAddConnection);
historyBus.on('revertRemoveConnection', onRevertDeleteConnection); historyBus.on('revertRemoveConnection', onRevertDeleteConnection);
// historyBus.on('revertRenameNode', onRevertNameChange); historyBus.on('revertRenameNode', onRevertRenameNode);
// historyBus.on('enableNodeToggle', onRevertEnableToggle); // historyBus.on('enableNodeToggle', onRevertEnableToggle);
} }
@ -182,10 +192,18 @@ function removeUndoRedoEventBindings() {
historyBus.off('revertRemoveNode', onRevertDeleteNode); historyBus.off('revertRemoveNode', onRevertDeleteNode);
// historyBus.off('revertAddConnection', onRevertAddConnection); // historyBus.off('revertAddConnection', onRevertAddConnection);
historyBus.off('revertRemoveConnection', onRevertDeleteConnection); historyBus.off('revertRemoveConnection', onRevertDeleteConnection);
// historyBus.off('revertRenameNode', onRevertNameChange); historyBus.off('revertRenameNode', onRevertRenameNode);
// historyBus.off('enableNodeToggle', onRevertEnableToggle); // historyBus.off('enableNodeToggle', onRevertEnableToggle);
} }
function addPostMessageEventBindings() {
window.addEventListener('message', onPostMessageReceived);
}
function removePostMessageEventBindings() {
window.removeEventListener('message', onPostMessageReceived);
}
// @TODO Maybe move this to the store // @TODO Maybe move this to the store
function initializeEditableWorkflow(id: string) { function initializeEditableWorkflow(id: string) {
const targetWorkflow = workflowsStore.workflowsById[id]; const targetWorkflow = workflowsStore.workflowsById[id];
@ -245,11 +263,16 @@ function onRevertDeleteNode({ node }: { node: INodeUi }) {
revertDeleteNode(node); revertDeleteNode(node);
} }
function onSetNodeActive(id: string) {
setNodeActive(id);
}
/** /**
* Map new node connection format to the old one and add it to the store * Map new node connection format to the old one and add it to the store
* *
* @param connection * @param connection
*/ */
function onCreateConnection(connection: Connection) { function onCreateConnection(connection: Connection) {
createConnection(connection); createConnection(connection);
} }
@ -262,53 +285,6 @@ function onRevertDeleteConnection({ connection }: { connection: [IConnection, IC
revertDeleteConnection(connection); revertDeleteConnection(connection);
} }
function onToggleNodeCreator({
source,
createNodeActive,
nodeCreatorView,
}: ToggleNodeCreatorOptions) {
if (createNodeActive === uiStore.isCreateNodeActive) {
return;
}
if (!nodeCreatorView) {
nodeCreatorView =
triggerNodes.value.length > 0 ? REGULAR_NODE_CREATOR_VIEW : TRIGGER_NODE_CREATOR_VIEW;
}
// Default to the trigger tab in node creator if there's no trigger node yet
nodeCreatorStore.setSelectedView(nodeCreatorView);
let mode;
switch (nodeCreatorStore.selectedView) {
case AI_NODE_CREATOR_VIEW:
mode = 'ai';
break;
case REGULAR_NODE_CREATOR_VIEW:
mode = 'regular';
break;
default:
mode = 'regular';
}
uiStore.isCreateNodeActive = createNodeActive;
if (createNodeActive && source) {
nodeCreatorStore.setOpenSource(source);
}
void externalHooks.run('nodeView.createNodeActiveChanged', {
source,
mode,
createNodeActive,
});
telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', {
source,
mode,
createNodeActive,
workflow_id: workflowId.value,
});
}
async function onAddNodes( async function onAddNodes(
{ nodes, connections }: AddedNodesAndConnections, { nodes, connections }: AddedNodesAndConnections,
dragAndDrop = false, dragAndDrop = false,
@ -355,23 +331,21 @@ async function onAddNodes(
}); });
} }
// @TODO Implement this const lastAddedNode = editableWorkflow.value.nodes[editableWorkflow.value.nodes.length - 1];
// const lastAddedNode = editableWorkflow.value.nodes[editableWorkflow.value.nodes.length - 1]; const lastNodeInputs = editableWorkflowObject.value.getParentNodesByDepth(lastAddedNode.name, 1);
// const workflow = editableWorkflowObject.value;
// const lastNodeInputs = workflow.getParentNodesByDepth(lastAddedNode.name, 1); // If the last added node has multiple inputs, move them down
// if (lastNodeInputs.length > 1) {
// // If the last added node has multiple inputs, move them down lastNodeInputs.slice(1).forEach((node, index) => {
// if (lastNodeInputs.length > 1) { const nodeUi = workflowsStore.getNodeByName(node.name);
// lastNodeInputs.slice(1).forEach((node, index) => { if (!nodeUi) return;
// const nodeUi = workflowsStore.getNodeByName(node.name);
// if (!nodeUi) return; updateNodePosition(nodeUi.id, {
// x: nodeUi.position[0],
// // onMoveNode({ y: nodeUi.position[1] + 100 * (index + 1),
// // nodeName: nodeUi.name, });
// // position: [nodeUi.position[0], nodeUi.position[1] + 100 * (index + 1)], });
// // }); }
// });
// }
} }
type AddNodeData = { type AddNodeData = {
@ -408,6 +382,7 @@ async function onNodeCreate(node: AddNodeData, _options: AddNodeOptions = {}): P
// @TODO Figure out why this is needed and if we can do better... // @TODO Figure out why this is needed and if we can do better...
// this.matchCredentials(node); // this.matchCredentials(node);
// @TODO Connect added node to last selected node
// const lastSelectedNode = uiStore.getLastSelectedNode; // const lastSelectedNode = uiStore.getLastSelectedNode;
// const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex; // const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex;
// const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid; // const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid;
@ -496,7 +471,7 @@ async function createNodeWithDefaultCredentials(node: Partial<INodeUi>) {
) as INodeTypeDescription; ) as INodeTypeDescription;
let nodeVersion = nodeTypeDescription.defaultVersion; let nodeVersion = nodeTypeDescription.defaultVersion;
if (nodeVersion === undefined) { if (typeof nodeVersion === 'undefined') {
nodeVersion = Array.isArray(nodeTypeDescription.version) nodeVersion = Array.isArray(nodeTypeDescription.version)
? nodeTypeDescription.version.slice(-1)[0] ? nodeTypeDescription.version.slice(-1)[0]
: nodeTypeDescription.version; : nodeTypeDescription.version;
@ -529,7 +504,7 @@ async function createNodeWithDefaultCredentials(node: Partial<INodeUi>) {
// ); // );
// } catch (e) { // } catch (e) {
// console.error( // console.error(
// this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') + // i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
// `: "${node.name}"`, // `: "${node.name}"`,
// ); // );
// console.error(e); // console.error(e);
@ -654,8 +629,8 @@ async function injectNode(
// //
// if (nodeTypeData === null) { // if (nodeTypeData === null) {
// this.showMessage({ // this.showMessage({
// title: this.$locale.baseText('nodeView.showMessage.addNodeButton.title'), // title: i18n.baseText('nodeView.showMessage.addNodeButton.title'),
// message: this.$locale.baseText('nodeView.showMessage.addNodeButton.message', { // message: i18n.baseText('nodeView.showMessage.addNodeButton.message', {
// interpolate: { nodeTypeName }, // interpolate: { nodeTypeName },
// }), // }),
// type: 'error', // type: 'error',
@ -897,6 +872,106 @@ function checkIfEditingIsAllowed(): boolean {
return true; return true;
} }
async function onPostMessageReceived(message: MessageEvent) {
if (!message || typeof message.data !== 'string' || !message.data?.includes?.('"command"')) {
return;
}
try {
const json = JSON.parse(message.data);
if (json && json.command === 'openWorkflow') {
try {
await importWorkflowExact(json.data);
canOpenNDV.value = json.canOpenNDV ?? true;
hideNodeIssues.value = json.hideNodeIssues ?? false;
isExecutionPreview.value = false;
} catch (e) {
if (window.top) {
window.top.postMessage(
JSON.stringify({
command: 'error',
message: i18n.baseText('openWorkflow.workflowImportError'),
}),
'*',
);
}
toast.showMessage({
title: i18n.baseText('openWorkflow.workflowImportError'),
message: (e as Error).message,
type: 'error',
});
}
} else if (json && json.command === 'openExecution') {
try {
// If this NodeView is used in preview mode (in iframe) it will not have access to the main app store
// so everything it needs has to be sent using post messages and passed down to child components
isProductionExecutionPreview.value = json.executionMode !== 'manual';
await openExecution(json.executionId);
canOpenNDV.value = json.canOpenNDV ?? true;
hideNodeIssues.value = json.hideNodeIssues ?? false;
isExecutionPreview.value = true;
} catch (e) {
if (window.top) {
window.top.postMessage(
JSON.stringify({
command: 'error',
message: i18n.baseText('nodeView.showError.openExecution.title'),
}),
'*',
);
}
toast.showMessage({
title: i18n.baseText('nodeView.showError.openExecution.title'),
message: (e as Error).message,
type: 'error',
});
}
} else if (json?.command === 'setActiveExecution') {
executionsStore.activeExecution = (await executionsStore.fetchExecution(
json.executionId,
)) as ExecutionSummary;
}
} catch (e) {}
}
async function onSwitchSelectedNode(nodeName: string) {
setNodeActiveByName(nodeName);
}
async function onOpenConnectionNodeCreator(node: string, connectionType: NodeConnectionType) {
nodeCreatorStore.openSelectiveNodeCreator({ node, connectionType });
}
function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
nodeCreatorStore.openNodeCreator(options);
}
async function openExecution(_executionId: string) {
// @TODO
}
async function importWorkflowExact(_workflow: IWorkflowDataUpdate) {
// @TODO
}
async function onRevertRenameNode({
currentName,
newName,
}: {
currentName: string;
newName: string;
}) {
await revertRenameNode(currentName, newName);
}
function onUpdateNodeValue(parameterData: IUpdateInformation) {
if (parameterData.name === 'name' && parameterData.oldValue) {
// The name changed so we have to take care that
// the connections get changed.
void renameNode(parameterData.oldValue as string, parameterData.value as string);
}
}
</script> </script>
<template> <template>
@ -905,6 +980,7 @@ function checkIfEditingIsAllowed(): boolean {
:workflow="editableWorkflow" :workflow="editableWorkflow"
:workflow-object="editableWorkflowObject" :workflow-object="editableWorkflowObject"
@update:node:position="onUpdateNodePosition" @update:node:position="onUpdateNodePosition"
@update:node:active="onSetNodeActive"
@delete:node="onDeleteNode" @delete:node="onDeleteNode"
@create:connection="onCreateConnection" @create:connection="onCreateConnection"
@delete:connection="onDeleteConnection" @delete:connection="onDeleteConnection"
@ -921,6 +997,20 @@ function checkIfEditingIsAllowed(): boolean {
@add-nodes="onAddNodes" @add-nodes="onAddNodes"
/> />
</Suspense> </Suspense>
<Suspense>
<NodeDetailsView
:read-only="isReadOnlyRoute || isReadOnlyEnvironment"
:is-production-execution-preview="isProductionExecutionPreview"
@value-changed="onUpdateNodeValue"
@switch-selected-node="onSwitchSelectedNode"
@open-connection-node-creator="onOpenConnectionNodeCreator"
/>
<!--
:renaming="renamingActive"
@stop-execution="stopExecution"
@save-keyboard-shortcut="onSaveKeyboardShortcut"
-->
</Suspense>
</WorkflowCanvas> </WorkflowCanvas>
</template> </template>