diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts
index 6b7e31a3a0..65de1a99c4 100644
--- a/packages/editor-ui/src/__tests__/mocks.ts
+++ b/packages/editor-ui/src/__tests__/mocks.ts
@@ -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;
name?: string;
- nodes: INode[];
- connections: IConnections;
+ nodes?: INode[];
+ connections?: IConnections;
active?: boolean;
nodeTypes?: INodeTypeData;
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
-}) {
+} = {}) {
return new Workflow({
- ...options,
- id: options.id ?? uuid(),
- active: options.active ?? false,
- nodeTypes: createTestNodeTypes(options.nodeTypes),
- connections: options.connections ?? {},
+ id,
+ name,
+ nodes,
+ connections,
+ active,
+ staticData,
+ settings,
+ pinData,
+ nodeTypes: createTestNodeTypes(nodeTypes),
});
}
diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue
index 84997b70f6..0adea73ade 100644
--- a/packages/editor-ui/src/components/canvas/Canvas.vue
+++ b/packages/editor-ui/src/components/canvas/Canvas.vue
@@ -14,6 +14,7 @@ const $style = useCssModule();
const emit = defineEmits<{
'update:modelValue': [elements: CanvasElement[]];
'update:node:position': [id: string, position: { x: number; y: number }];
+ 'update:node:active': [id: string];
'delete:node': [id: string];
'delete: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) {
emit('delete:node', id);
}
@@ -97,7 +102,7 @@ function onMouseLeaveEdge(event: EdgeMouseEvent) {
@connect="onConnect"
>
-
+
diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
index d02b9164be..0df937b1f1 100644
--- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
@@ -17,6 +17,7 @@ import type { NodeProps } from '@vue-flow/core';
const emit = defineEmits<{
delete: [id: string];
+ activate: [id: string];
}>();
const props = defineProps>();
@@ -97,6 +98,10 @@ provide(CanvasNodeKey, {
function onDelete() {
emit('delete', props.id);
}
+
+function onActivate() {
+ emit('activate', props.id);
+}
@@ -132,7 +137,7 @@ function onDelete() {
@delete="onDelete"
/>
-
+
diff --git a/packages/editor-ui/src/composables/useCanvasOperations.spec.ts b/packages/editor-ui/src/composables/useCanvasOperations.spec.ts
index 8d19d30b6b..ca23d162e0 100644
--- a/packages/editor-ui/src/composables/useCanvasOperations.spec.ts
+++ b/packages/editor-ui/src/composables/useCanvasOperations.spec.ts
@@ -6,14 +6,16 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
import { useHistoryStore } from '@/stores/history.store';
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 { IConnection } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
+import { useNDVStore } from '@/stores/ndv.store';
describe('useCanvasOperations', () => {
let workflowsStore: ReturnType;
let uiStore: ReturnType;
+ let ndvStore: ReturnType;
let historyStore: ReturnType;
let canvasOperations: ReturnType;
@@ -23,6 +25,7 @@ describe('useCanvasOperations', () => {
workflowsStore = useWorkflowsStore();
uiStore = useUIStore();
+ ndvStore = useNDVStore();
historyStore = useHistoryStore();
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', () => {
it('should not create a connection if source node does not exist', () => {
const addConnectionSpy = vi
diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts
index 0f7cc6d878..c3b4c52a22 100644
--- a/packages/editor-ui/src/composables/useCanvasOperations.ts
+++ b/packages/editor-ui/src/composables/useCanvasOperations.ts
@@ -6,15 +6,22 @@ import { useHistoryStore } from '@/stores/history.store';
import { useUIStore } from '@/stores/ui.store';
import { useTelemetry } from '@/composables/useTelemetry';
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 { mapCanvasConnectionToLegacyConnection } from '@/utils/canvasUtilsV2';
+import { getUniqueNodeName, mapCanvasConnectionToLegacyConnection } from '@/utils/canvasUtilsV2';
import type { IConnection } from 'n8n-workflow';
+import { useNDVStore } from '@/stores/ndv.store';
export function useCanvasOperations() {
const workflowsStore = useWorkflowsStore();
const historyStore = useHistoryStore();
const uiStore = useUIStore();
+ const ndvStore = useNDVStore();
const telemetry = useTelemetry();
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 } = {}) {
const node = workflowsStore.getNodeById(id);
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
*/
@@ -204,6 +263,10 @@ export function useCanvasOperations() {
return {
updateNodePosition,
+ setNodeActive,
+ setNodeActiveByName,
+ renameNode,
+ revertRenameNode,
deleteNode,
revertDeleteNode,
trackDeleteNode,
diff --git a/packages/editor-ui/src/stores/nodeCreator.store.ts b/packages/editor-ui/src/stores/nodeCreator.store.ts
index 0939edc0d4..fe92add496 100644
--- a/packages/editor-ui/src/stores/nodeCreator.store.ts
+++ b/packages/editor-ui/src/stores/nodeCreator.store.ts
@@ -1,16 +1,41 @@
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 {
NodeFilterType,
NodeCreatorOpenSource,
SimplifiedNodeType,
ActionsRecord,
+ ToggleNodeCreatorOptions,
+ AIAssistantConnectionInfo,
} from '@/Interface';
import { computed, ref } from 'vue';
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, () => {
+ const workflowsStore = useWorkflowsStore();
+ const ndvStore = useNDVStore();
+ const uiStore = useUIStore();
+ const nodeTypesStore = useNodeTypesStore();
+
+ const externalHooks = useExternalHooks();
+ const telemetry = useTelemetry();
+
const selectedView = ref(TRIGGER_NODE_CREATOR_VIEW);
const mergedNodes = ref([]);
const actions = ref>({});
@@ -42,6 +67,173 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
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 {
openSource,
selectedView,
@@ -53,6 +245,8 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
setOpenSource,
setActions,
setMergeNodes,
+ openNodeCreator,
+ openSelectiveNodeCreator,
allNodeCreatorNodes,
};
});
diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts
index 3c6cc81e26..8685ab24d8 100644
--- a/packages/editor-ui/src/stores/workflows.store.ts
+++ b/packages/editor-ui/src/stores/workflows.store.ts
@@ -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 {
workflow.value.nodes.forEach((node) => {
node.issues = undefined;
@@ -1650,5 +1658,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
removeNodeById,
removeNodeConnectionsById,
removeNodeExecutionDataById,
+ setNodes,
+ setConnections,
};
});
diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue
index 201dc7ac03..2e53ff78cf 100644
--- a/packages/editor-ui/src/views/NodeView.v2.vue
+++ b/packages/editor-ui/src/views/NodeView.v2.vue
@@ -12,26 +12,20 @@ import type {
AddedNodesAndConnections,
INodeUi,
ITag,
+ IUpdateInformation,
+ IWorkflowDataUpdate,
ToggleNodeCreatorOptions,
XYPosition,
} from '@/Interface';
-import useWorkflowsEEStore from '@/stores/workflows.ee.store';
import { useTagsStore } from '@/stores/tags.store';
import type { Connection } from '@vue-flow/core';
import type { CanvasElement } from '@/types';
-import {
- EnterpriseEditionFeature,
- AI_NODE_CREATOR_VIEW,
- REGULAR_NODE_CREATOR_VIEW,
- TRIGGER_NODE_CREATOR_VIEW,
- VIEWS,
-} from '@/constants';
+import { EnterpriseEditionFeature, VIEWS } from '@/constants';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
-import { useTelemetry } from '@/composables/useTelemetry';
import { useExternalHooks } from '@/composables/useExternalHooks';
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 { useToast } from '@/composables/useToast';
import { v4 as uuid } from 'uuid';
@@ -44,24 +38,27 @@ import { useCollaborationStore } from '@/stores/collaboration.store';
import { getUniqueNodeName } from '@/utils/canvasUtilsV2';
import { historyBus } from '@/models/history';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
+import { useExecutionsStore } from '@/stores/executions.store';
const NodeCreation = defineAsyncComponent(
async () => await import('@/components/Node/NodeCreation.vue'),
);
+const NodeDetailsView = defineAsyncComponent(
+ async () => await import('@/components/NodeDetailsView.vue'),
+);
+
const $style = useCssModule();
const router = useRouter();
const route = useRoute();
const i18n = useI18n();
-const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const toast = useToast();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
-const workflowsEEStore = useWorkflowsEEStore();
const tagsStore = useTagsStore();
const sourceControlStore = useSourceControlStore();
const nodeCreatorStore = useNodeCreatorStore();
@@ -71,20 +68,31 @@ const environmentsStore = useEnvironmentsStore();
const externalSecretsStore = useExternalSecretsStore();
const rootStore = useRootStore();
const collaborationStore = useCollaborationStore();
+const executionsStore = useExecutionsStore();
const { runWorkflow } = useRunWorkflow({ router });
const {
updateNodePosition,
+ renameNode,
+ revertRenameNode,
+ setNodeActive,
deleteNode,
revertDeleteNode,
createConnection,
deleteConnection,
revertDeleteConnection,
+ setNodeActiveByName,
} = useCanvasOperations();
const isLoading = ref(true);
const readOnlyNotification = ref(null);
+const isProductionExecutionPreview = ref(false);
+const isExecutionPreview = ref(false);
+
+const canOpenNDV = ref(true);
+const hideNodeIssues = ref(false);
+
const workflowId = computed(() => route.params.workflowId as string);
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);
@@ -151,6 +159,7 @@ async function initialize() {
initializeEditableWorkflow(workflowId.value);
addUndoRedoEventBindings();
+ addPostMessageEventBindings();
if (window.parent) {
window.parent.postMessage(
@@ -163,6 +172,7 @@ async function initialize() {
}
onBeforeUnmount(() => {
+ removePostMessageEventBindings();
removeUndoRedoEventBindings();
});
@@ -172,7 +182,7 @@ function addUndoRedoEventBindings() {
historyBus.on('revertRemoveNode', onRevertDeleteNode);
// historyBus.on('revertAddConnection', onRevertAddConnection);
historyBus.on('revertRemoveConnection', onRevertDeleteConnection);
- // historyBus.on('revertRenameNode', onRevertNameChange);
+ historyBus.on('revertRenameNode', onRevertRenameNode);
// historyBus.on('enableNodeToggle', onRevertEnableToggle);
}
@@ -182,10 +192,18 @@ function removeUndoRedoEventBindings() {
historyBus.off('revertRemoveNode', onRevertDeleteNode);
// historyBus.off('revertAddConnection', onRevertAddConnection);
historyBus.off('revertRemoveConnection', onRevertDeleteConnection);
- // historyBus.off('revertRenameNode', onRevertNameChange);
+ historyBus.off('revertRenameNode', onRevertRenameNode);
// historyBus.off('enableNodeToggle', onRevertEnableToggle);
}
+function addPostMessageEventBindings() {
+ window.addEventListener('message', onPostMessageReceived);
+}
+
+function removePostMessageEventBindings() {
+ window.removeEventListener('message', onPostMessageReceived);
+}
+
// @TODO Maybe move this to the store
function initializeEditableWorkflow(id: string) {
const targetWorkflow = workflowsStore.workflowsById[id];
@@ -245,11 +263,16 @@ function onRevertDeleteNode({ node }: { node: INodeUi }) {
revertDeleteNode(node);
}
+function onSetNodeActive(id: string) {
+ setNodeActive(id);
+}
+
/**
* Map new node connection format to the old one and add it to the store
*
* @param connection
*/
+
function onCreateConnection(connection: Connection) {
createConnection(connection);
}
@@ -262,53 +285,6 @@ function onRevertDeleteConnection({ connection }: { connection: [IConnection, IC
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(
{ nodes, connections }: AddedNodesAndConnections,
dragAndDrop = false,
@@ -355,23 +331,21 @@ async function onAddNodes(
});
}
- // @TODO Implement this
- // const lastAddedNode = editableWorkflow.value.nodes[editableWorkflow.value.nodes.length - 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) {
- // lastNodeInputs.slice(1).forEach((node, index) => {
- // const nodeUi = workflowsStore.getNodeByName(node.name);
- // if (!nodeUi) return;
- //
- // // onMoveNode({
- // // nodeName: nodeUi.name,
- // // position: [nodeUi.position[0], nodeUi.position[1] + 100 * (index + 1)],
- // // });
- // });
- // }
+ const lastAddedNode = editableWorkflow.value.nodes[editableWorkflow.value.nodes.length - 1];
+ const lastNodeInputs = editableWorkflowObject.value.getParentNodesByDepth(lastAddedNode.name, 1);
+
+ // If the last added node has multiple inputs, move them down
+ if (lastNodeInputs.length > 1) {
+ lastNodeInputs.slice(1).forEach((node, index) => {
+ const nodeUi = workflowsStore.getNodeByName(node.name);
+ if (!nodeUi) return;
+
+ updateNodePosition(nodeUi.id, {
+ x: nodeUi.position[0],
+ y: nodeUi.position[1] + 100 * (index + 1),
+ });
+ });
+ }
}
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...
// this.matchCredentials(node);
+ // @TODO Connect added node to last selected node
// const lastSelectedNode = uiStore.getLastSelectedNode;
// const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex;
// const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid;
@@ -496,7 +471,7 @@ async function createNodeWithDefaultCredentials(node: Partial) {
) as INodeTypeDescription;
let nodeVersion = nodeTypeDescription.defaultVersion;
- if (nodeVersion === undefined) {
+ if (typeof nodeVersion === 'undefined') {
nodeVersion = Array.isArray(nodeTypeDescription.version)
? nodeTypeDescription.version.slice(-1)[0]
: nodeTypeDescription.version;
@@ -529,7 +504,7 @@ async function createNodeWithDefaultCredentials(node: Partial) {
// );
// } catch (e) {
// console.error(
- // this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
+ // i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
// `: "${node.name}"`,
// );
// console.error(e);
@@ -654,8 +629,8 @@ async function injectNode(
//
// if (nodeTypeData === null) {
// this.showMessage({
- // title: this.$locale.baseText('nodeView.showMessage.addNodeButton.title'),
- // message: this.$locale.baseText('nodeView.showMessage.addNodeButton.message', {
+ // title: i18n.baseText('nodeView.showMessage.addNodeButton.title'),
+ // message: i18n.baseText('nodeView.showMessage.addNodeButton.message', {
// interpolate: { nodeTypeName },
// }),
// type: 'error',
@@ -897,6 +872,106 @@ function checkIfEditingIsAllowed(): boolean {
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);
+ }
+}
@@ -905,6 +980,7 @@ function checkIfEditingIsAllowed(): boolean {
:workflow="editableWorkflow"
:workflow-object="editableWorkflowObject"
@update:node:position="onUpdateNodePosition"
+ @update:node:active="onSetNodeActive"
@delete:node="onDeleteNode"
@create:connection="onCreateConnection"
@delete:connection="onDeleteConnection"
@@ -921,6 +997,20 @@ function checkIfEditingIsAllowed(): boolean {
@add-nodes="onAddNodes"
/>
+
+
+
+