mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
feat(editor): Add capability to open NDV and rename node (no-changelog) (#9712)
This commit is contained in:
@@ -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 | { 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 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<INodeUi>) {
|
||||
) 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<INodeUi>) {
|
||||
// );
|
||||
// } 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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -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"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user