From 9244ea617e0c5bd5163d70310ffcf4799f1293f1 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 9 Apr 2024 16:04:30 +0300 Subject: [PATCH 01/22] feat: initial canvas v2 commit --- packages/editor-ui/src/api/workflows.ts | 4 +- .../src/components/canvas/Canvas.vue | 222 ++++++++++++++++++ .../src/components/canvas/plugins/index.ts | 9 + packages/editor-ui/src/constants.ts | 1 + packages/editor-ui/src/router.ts | 13 + .../src/stores/workflows.store.v2.ts | 34 +++ packages/editor-ui/src/types/canvas.ts | 41 ++++ packages/editor-ui/src/types/index.ts | 1 + packages/editor-ui/src/utils/canvasUtilsV2.ts | 69 ++++++ packages/editor-ui/src/views/NodeView.v2.vue | 64 +++++ 10 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 packages/editor-ui/src/components/canvas/Canvas.vue create mode 100644 packages/editor-ui/src/components/canvas/plugins/index.ts create mode 100644 packages/editor-ui/src/stores/workflows.store.v2.ts create mode 100644 packages/editor-ui/src/types/canvas.ts create mode 100644 packages/editor-ui/src/utils/canvasUtilsV2.ts create mode 100644 packages/editor-ui/src/views/NodeView.v2.vue diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts index 82ae637c4247b..a1a28712b2c9b 100644 --- a/packages/editor-ui/src/api/workflows.ts +++ b/packages/editor-ui/src/api/workflows.ts @@ -1,4 +1,4 @@ -import type { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface'; +import type { IExecutionsCurrentSummaryExtended, IRestApiContext, IWorkflowDb } from '@/Interface'; import type { ExecutionFilters, ExecutionOptions, IDataObject } from 'n8n-workflow'; import { makeRestApiRequest } from '@/utils/apiUtils'; @@ -14,7 +14,7 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) { export async function getWorkflow(context: IRestApiContext, id: string, filter?: object) { const sendData = filter ? { filter } : undefined; - return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData); + return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData); } export async function getWorkflows(context: IRestApiContext, filter?: object) { diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue new file mode 100644 index 0000000000000..000e3db104b6e --- /dev/null +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/packages/editor-ui/src/components/canvas/plugins/index.ts b/packages/editor-ui/src/components/canvas/plugins/index.ts new file mode 100644 index 0000000000000..1c38ed369733c --- /dev/null +++ b/packages/editor-ui/src/components/canvas/plugins/index.ts @@ -0,0 +1,9 @@ +import type { CanvasPlugin } from '@/types'; + +export const canvasPan: CanvasPlugin = (ctx) => { + ctx.instance.bind('pan', (e) => { + console.log('pan', e); + }); +}; + +export const canvasZoom: CanvasPlugin = () => {}; diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index c8d119f14f594..42beef1efb303 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -595,6 +595,7 @@ export const enum STORES { UI = 'ui', USERS = 'users', WORKFLOWS = 'workflows', + WORKFLOWS_V2 = 'workflowsV2', WORKFLOWS_EE = 'workflowsEE', NDV = 'ndv', TEMPLATES = 'templates', diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 356ab3f57a550..edade1cd87448 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -26,6 +26,7 @@ const ForgotMyPasswordView = async () => await import('./views/ForgotMyPasswordV const MainHeader = async () => await import('@/components/MainHeader/MainHeader.vue'); const MainSidebar = async () => await import('@/components/MainSidebar.vue'); const NodeView = async () => await import('@/views/NodeView.vue'); +const NodeViewV2 = async () => await import('@/views/NodeView.v2.vue'); const WorkflowExecutionsList = async () => await import('@/components/ExecutionsView/ExecutionsList.vue'); const ExecutionsLandingPage = async () => @@ -387,6 +388,18 @@ export const routes = [ path: '/workflow', redirect: '/workflow/new', }, + { + path: '/workflow-v2/:workflowId', + name: VIEWS.WORKFLOW, + components: { + default: NodeViewV2, + header: MainHeader, + sidebar: MainSidebar, + }, + meta: { + middleware: ['authenticated'], + }, + }, { path: '/signin', name: VIEWS.SIGNIN, diff --git a/packages/editor-ui/src/stores/workflows.store.v2.ts b/packages/editor-ui/src/stores/workflows.store.v2.ts new file mode 100644 index 0000000000000..e7a3a605b86ba --- /dev/null +++ b/packages/editor-ui/src/stores/workflows.store.v2.ts @@ -0,0 +1,34 @@ +import { defineStore } from 'pinia'; +import { STORES } from '@/constants'; +import { computed, ref } from 'vue'; +import type { IWorkflowDb } from '@/Interface'; +import { useRootStore } from '@/stores/n8nRoot.store'; +import { getWorkflow } from '@/api/workflows'; + +export const useWorkflowsStoreV2 = defineStore(STORES.WORKFLOWS_V2, () => { + const workflowsById = ref>({}); + const workflows = computed(() => Object.values(workflowsById.value)); + + function addWorkflow(workflow: IWorkflowDb) { + workflowsById.value[workflow.id] = workflow; + } + + function removeWorkflow(id: string) { + delete workflowsById.value[id]; + } + + async function fetchWorkflow(id: string) { + const rootStore = useRootStore(); + const workflow = await getWorkflow(rootStore.getRestApiContext, id); + + addWorkflow(workflow); + } + + return { + workflowsById, + workflows, + addWorkflow, + removeWorkflow, + fetchWorkflow, + }; +}); diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts new file mode 100644 index 0000000000000..1a42fdfa3d8a4 --- /dev/null +++ b/packages/editor-ui/src/types/canvas.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +import type { ConnectionTypes } from 'n8n-workflow'; +import { JsPlumbInstance } from '@jsplumb/core'; +import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; + +export type CanvasElementType = 'node' | 'note'; + +export type CanvasConnectionEndpointType = ConnectionTypes; + +export type CanvasElementEndpoint = { + type: CanvasConnectionEndpointType; + port: number; +}; + +export interface CanvasElement { + id: string; + type: CanvasElementType; + position: [number, number]; + metadata: unknown; + inputs: CanvasElementEndpoint[]; + outputs: CanvasElementEndpoint[]; +} + +export interface CanvasConnectionEndpoint { + id: string; + type: CanvasConnectionEndpointType; + port: number; +} + +export interface CanvasConnection { + source: CanvasConnectionEndpoint; + target: CanvasConnectionEndpoint; +} + +export interface CanvasPluginContext { + instance: BrowserJsPlumbInstance; +} + +export interface CanvasPlugin { + (ctx: CanvasPluginContext): void; +} diff --git a/packages/editor-ui/src/types/index.ts b/packages/editor-ui/src/types/index.ts index 7bd3ad2db7474..7c2e3d7fb4804 100644 --- a/packages/editor-ui/src/types/index.ts +++ b/packages/editor-ui/src/types/index.ts @@ -1 +1,2 @@ +export * from './canvas'; export * from './externalHooks'; diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts new file mode 100644 index 0000000000000..8a845a0ac48ab --- /dev/null +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -0,0 +1,69 @@ +import type { IConnections, INodeTypeDescription } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { + CanvasConnection, + CanvasConnectionEndpointType, + CanvasElementEndpoint, +} from '@/types'; + +export function mapLegacyConnections( + legacyConnections: IConnections, + nodes: INodeUi[], +): CanvasConnection[] { + const mappedConnections: CanvasConnection[] = []; + + Object.keys(legacyConnections).forEach((fromNodeName) => { + const fromId = nodes.find((node) => node.name === fromNodeName)?.id; + + Object.keys(legacyConnections[fromNodeName]).forEach((fromConnectionType) => { + legacyConnections[fromNodeName][fromConnectionType].forEach((portTargets, fromPort) => { + portTargets.forEach((portTarget) => { + const toId = nodes.find((node) => node.name === portTarget.node)?.id; + const toPort = portTarget.index; + const toConnectionType = portTarget.type; + + if (fromId && toId) { + mappedConnections.push({ + source: { + id: fromId, + port: fromPort, + type: fromConnectionType as CanvasConnectionEndpointType, + }, + target: { + id: toId, + port: toPort, + type: toConnectionType as CanvasConnectionEndpointType, + }, + }); + } + }); + }); + }); + }); + + return mappedConnections; +} + +export function normalizeElementEndpoints( + endpoints: INodeTypeDescription['inputs'], +): CanvasElementEndpoint[] { + // @TODO Handle string case + if (typeof endpoints === 'string') { + return []; + } + + return endpoints.map((endpoint, index) => { + if (typeof endpoint === 'string') { + const port = endpoints.slice(0, index).filter((e) => e === endpoint).length; + return { + type: endpoint, + port, + }; + } else { + return { + type: endpoint.type, + port: 0, + }; + } + }); +} diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue new file mode 100644 index 0000000000000..d2c9a4d0d413e --- /dev/null +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -0,0 +1,64 @@ + + + + + From a7b66bcf5b5f2aa8cf2e173088dc195cbd157d43 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 12 Apr 2024 10:40:14 +0300 Subject: [PATCH 02/22] feat: add VueFlow for canvas rendering --- packages/editor-ui/package.json | 1 + .../src/components/canvas/Canvas.vue | 221 +++-------------- .../src/components/canvas/CanvasJsPlumb.vue | 222 ++++++++++++++++++ .../components/canvas/elements/CanvasNode.vue | 103 ++++++++ packages/editor-ui/src/constants.ts | 1 + packages/editor-ui/src/main.ts | 3 + packages/editor-ui/src/router.ts | 2 +- .../src/stores/workflows.store.v2.ts | 145 +++++++++++- packages/editor-ui/src/types/canvas.ts | 40 ++-- packages/editor-ui/src/utils/canvasUtilsV2.ts | 38 +-- packages/editor-ui/src/views/NodeView.v2.vue | 46 +++- pnpm-lock.yaml | 93 +++++++- 12 files changed, 670 insertions(+), 245 deletions(-) create mode 100644 packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue create mode 100644 packages/editor-ui/src/components/canvas/elements/CanvasNode.vue diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 6aaf9ef007471..de3fcaf133424 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -45,6 +45,7 @@ "@n8n/chat": "workspace:*", "@n8n/codemirror-lang-sql": "^1.0.2", "@n8n/permissions": "workspace:*", + "@vue-flow/core": "^1.33.5", "@vueuse/components": "^10.5.0", "@vueuse/core": "^10.5.0", "axios": "1.6.7", diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 000e3db104b6e..5cf345a3bca68 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -1,192 +1,59 @@ @@ -197,26 +64,4 @@ function addConnection(connection: CanvasConnection) { position: relative; display: block; } - -.canvas-anchor { - position: fixed; -} - -.canvas { - position: relative; - width: 100%; - height: 100%; - transform-origin: 0 0; - z-index: -1; -} - -.element { - position: absolute; -} - -.box { - width: 100px; - height: 100px; - background: gray; -} diff --git a/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue b/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue new file mode 100644 index 0000000000000..382f1c932ad8e --- /dev/null +++ b/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/packages/editor-ui/src/components/canvas/elements/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/CanvasNode.vue new file mode 100644 index 0000000000000..d6b2b4e2f4c36 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/elements/CanvasNode.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 42beef1efb303..1649b24aa2033 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -442,6 +442,7 @@ export const enum VIEWS { VARIABLES = 'VariablesView', NEW_WORKFLOW = 'NodeViewNew', WORKFLOW = 'NodeViewExisting', + WORKFLOW_V2 = 'NodeViewV2', DEMO = 'WorkflowDemo', TEMPLATE_IMPORT = 'WorkflowTemplate', WORKFLOW_ONBOARDING = 'WorkflowOnboarding', diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 33cf110c2a818..047a9afb3793d 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -1,5 +1,8 @@ import { createApp } from 'vue'; +import '@vue-flow/core/dist/style.css'; +import '@vue-flow/core/dist/theme-default.css'; + import 'vue-json-pretty/lib/styles.css'; import '@jsplumb/browser-ui/css/jsplumbtoolkit.css'; import 'n8n-design-system/css/index.scss'; diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index edade1cd87448..0b3350a20b3eb 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -390,7 +390,7 @@ export const routes = [ }, { path: '/workflow-v2/:workflowId', - name: VIEWS.WORKFLOW, + name: VIEWS.WORKFLOW_V2, components: { default: NodeViewV2, header: MainHeader, diff --git a/packages/editor-ui/src/stores/workflows.store.v2.ts b/packages/editor-ui/src/stores/workflows.store.v2.ts index e7a3a605b86ba..17b709f9fb8d4 100644 --- a/packages/editor-ui/src/stores/workflows.store.v2.ts +++ b/packages/editor-ui/src/stores/workflows.store.v2.ts @@ -1,14 +1,50 @@ import { defineStore } from 'pinia'; -import { STORES } from '@/constants'; +import { + ERROR_TRIGGER_NODE_TYPE, + PLACEHOLDER_EMPTY_WORKFLOW_ID, + START_NODE_TYPE, + STORES, +} from '@/constants'; import { computed, ref } from 'vue'; import type { IWorkflowDb } from '@/Interface'; import { useRootStore } from '@/stores/n8nRoot.store'; -import { getWorkflow } from '@/api/workflows'; +import * as workflowsApi from '@/api/workflows'; +import type { INodeType, INodeTypes } from 'n8n-workflow'; +import { deepCopy, Workflow } from 'n8n-workflow'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; + +const defaultWorkflowSettings: Omit & { + settings: NonNullable; +} = { + name: '', + active: false, + createdAt: -1, + updatedAt: -1, + connections: {}, + nodes: [], + settings: { + executionOrder: 'v1', + }, + tags: [], + pinData: {}, + versionId: '', + usedCredentials: [], +}; export const useWorkflowsStoreV2 = defineStore(STORES.WORKFLOWS_V2, () => { const workflowsById = ref>({}); const workflows = computed(() => Object.values(workflowsById.value)); + const workflowInstancesCacheById = ref< + Record< + string, + { + key: string; + workflow: Workflow; + } + > + >({}); + function addWorkflow(workflow: IWorkflowDb) { workflowsById.value[workflow.id] = workflow; } @@ -19,16 +55,119 @@ export const useWorkflowsStoreV2 = defineStore(STORES.WORKFLOWS_V2, () => { async function fetchWorkflow(id: string) { const rootStore = useRootStore(); - const workflow = await getWorkflow(rootStore.getRestApiContext, id); + const workflow = await workflowsApi.getWorkflow(rootStore.getRestApiContext, id); addWorkflow(workflow); } + function createNodeTypesForWorkflowObject(): INodeTypes { + return { + nodeTypes: {}, + init: async (): Promise => {}, + getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { + const description = useNodeTypesStore().getNodeType(nodeType, version); + if (description === null) { + return undefined; + } + + // As we do not have the trigger/poll functions available in the frontend + // we use the information available to figure out what are trigger nodes + const trigger = ((![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && + description.inputs.length === 0 && + !description.webhooks) || + undefined) as INodeType['trigger']; + + return { + description, + trigger, + }; + }, + } as unknown as INodeTypes; + } + + function createWorkflowInstance( + id: IWorkflowDb['id'], + name: IWorkflowDb['name'], + nodes: IWorkflowDb['nodes'], + connections: IWorkflowDb['connections'], + settings: IWorkflowDb['settings'], + pinData: IWorkflowDb['pinData'], + copyData?: boolean, + ): Workflow { + const nodeTypes = createNodeTypesForWorkflowObject(); + let workflowId: string | undefined = id; + if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + workflowId = undefined; + } + + return new Workflow({ + id: workflowId, + name, + nodes: copyData ? deepCopy(nodes) : nodes, + connections: copyData ? deepCopy(connections) : connections, + active: false, + nodeTypes, + settings, + pinData, + }); + } + + function getWorkflowInstance(id: string, copyData?: boolean): Workflow { + const nodes = getWorkflowNodes(id); + const connections = getWorkflowConnections(id); + const cacheKey = JSON.stringify({ nodes, connections }); + + const cacheEntry = workflowInstancesCacheById.value[id]; + if (!copyData && cacheEntry && cacheKey === cacheEntry.key) { + return cacheEntry.workflow; + } + + const name = getWorkflowName(id); + const settings = getWorkflowSettings(id); + const pinData = getWorkflowPinData(id); + const workflowObject = createWorkflowInstance( + id, + name, + nodes, + connections, + settings, + pinData, + copyData, + ); + workflowInstancesCacheById.value[id] = { key: cacheKey, workflow: workflowObject }; + return workflowObject; + } + + // Returns a shallow copy of the nodes which means that all the data on the lower + // levels still only gets referenced but the top level object is a different one. + // This has the advantage that it is very fast and does not cause problems with vuex + // when the workflow replaces the node-parameters. + function getWorkflowNodes(id: string): IWorkflowDb['nodes'] { + return workflowsById.value[id].nodes.map((node) => ({ ...node })); + } + + function getWorkflowConnections(id: string): IWorkflowDb['connections'] { + return { ...workflowsById.value[id].connections }; + } + + function getWorkflowSettings(id: string): IWorkflowDb['settings'] { + return workflowsById.value[id].settings ?? { ...defaultWorkflowSettings.settings }; + } + + function getWorkflowName(id: string): IWorkflowDb['name'] { + return workflowsById.value[id].name; + } + + function getWorkflowPinData(id: string): IWorkflowDb['pinData'] { + return workflowsById.value[id].pinData; + } + return { workflowsById, workflows, addWorkflow, removeWorkflow, fetchWorkflow, + getWorkflowObject: getWorkflowInstance, }; }); diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts index 1a42fdfa3d8a4..cc919e1e4518d 100644 --- a/packages/editor-ui/src/types/canvas.ts +++ b/packages/editor-ui/src/types/canvas.ts @@ -1,37 +1,39 @@ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */ import type { ConnectionTypes } from 'n8n-workflow'; -import { JsPlumbInstance } from '@jsplumb/core'; import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; +import type { DefaultEdge, Node, Position } from '@vue-flow/core'; +import type { INodeUi } from '@/Interface'; export type CanvasElementType = 'node' | 'note'; -export type CanvasConnectionEndpointType = ConnectionTypes; +export type CanvasConnectionPortType = ConnectionTypes; -export type CanvasElementEndpoint = { - type: CanvasConnectionEndpointType; - port: number; +export type CanvasConnectionPort = { + type: CanvasConnectionPortType; + index: number; }; -export interface CanvasElement { - id: string; - type: CanvasElementType; - position: [number, number]; - metadata: unknown; - inputs: CanvasElementEndpoint[]; - outputs: CanvasElementEndpoint[]; +export interface CanvasElementPortWithPosition extends CanvasConnectionPort { + position: Position; + offset?: { top?: number | string; left?: number | string }; } -export interface CanvasConnectionEndpoint { - id: string; - type: CanvasConnectionEndpointType; - port: number; +export interface CanvasElementData { + type: INodeUi['type']; + typeVersion: INodeUi['typeVersion']; + inputs: CanvasConnectionPort[]; + outputs: CanvasConnectionPort[]; } -export interface CanvasConnection { - source: CanvasConnectionEndpoint; - target: CanvasConnectionEndpoint; +export type CanvasElement = Node; + +export interface CanvasConnectionData { + source: CanvasConnectionPort; + target: CanvasConnectionPort; } +export type CanvasConnection = DefaultEdge; + export interface CanvasPluginContext { instance: BrowserJsPlumbInstance; } diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts index 8a845a0ac48ab..ef0c499082142 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -1,10 +1,7 @@ import type { IConnections, INodeTypeDescription } from 'n8n-workflow'; import type { INodeUi } from '@/Interface'; -import type { - CanvasConnection, - CanvasConnectionEndpointType, - CanvasElementEndpoint, -} from '@/types'; +import type { CanvasConnection, CanvasConnectionPortType, CanvasConnectionPort } from '@/types'; +import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; export function mapLegacyConnections( legacyConnections: IConnections, @@ -24,15 +21,20 @@ export function mapLegacyConnections( if (fromId && toId) { mappedConnections.push({ - source: { - id: fromId, - port: fromPort, - type: fromConnectionType as CanvasConnectionEndpointType, - }, - target: { - id: toId, - port: toPort, - type: toConnectionType as CanvasConnectionEndpointType, + id: `[${fromId}/${fromConnectionType}/${fromPort}][${toId}/${toConnectionType}/${toPort}]`, + source: fromId, + target: toId, + sourceHandle: `outputs/${fromConnectionType}/${fromPort}`, + targetHandle: `inputs/${toConnectionType}/${toPort}`, + data: { + source: { + index: fromPort, + type: fromConnectionType as CanvasConnectionPortType, + }, + target: { + index: toPort, + type: toConnectionType as CanvasConnectionPortType, + }, }, }); } @@ -46,9 +48,9 @@ export function mapLegacyConnections( export function normalizeElementEndpoints( endpoints: INodeTypeDescription['inputs'], -): CanvasElementEndpoint[] { - // @TODO Handle string case +): CanvasConnectionPort[] { if (typeof endpoints === 'string') { + console.warn('Node endpoints have not been evaluated', endpoints); return []; } @@ -57,12 +59,12 @@ export function normalizeElementEndpoints( const port = endpoints.slice(0, index).filter((e) => e === endpoint).length; return { type: endpoint, - port, + index: port, }; } else { return { type: endpoint.type, - port: 0, + index: 0, }; } }); diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index d2c9a4d0d413e..92f069b54ffc1 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -3,11 +3,11 @@ import Canvas from '@/components/canvas/Canvas.vue'; import { computed, onMounted } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useWorkflowsStoreV2 } from '@/stores/workflows.store.v2'; -import type { CanvasConnection, CanvasElement } from '@/types'; +import type { CanvasConnection, CanvasElement, CanvasElementData } from '@/types'; import { mapLegacyConnections, normalizeElementEndpoints } from '@/utils/canvasUtilsV2'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import { NodeHelpers } from 'n8n-workflow'; -const router = useRouter(); const route = useRoute(); const nodeTypesStore = useNodeTypesStore(); @@ -15,6 +15,7 @@ const workflowsStoreV2 = useWorkflowsStoreV2(); const workflowId = computed(() => route.params.workflowId as string); const workflow = computed(() => workflowsStoreV2.workflowsById[workflowId.value]); +const workflowObject = computed(() => workflowsStoreV2.getWorkflowObject(workflowId.value)); const connections = computed(() => mapLegacyConnections(workflow.value?.connections ?? [], workflow.value?.nodes ?? []), @@ -22,15 +23,44 @@ const connections = computed(() => const elements = computed(() => [ ...workflow.value?.nodes.map((node) => { - const nodeType = nodeTypesStore.getNodeType(node.type); + const nodeTypeDescription = nodeTypesStore.getNodeType(node.type); + const workflowObjectNode = workflowObject.value.getNode(node.name); + + const inputs = + workflowObjectNode && nodeTypeDescription + ? normalizeElementEndpoints( + NodeHelpers.getNodeInputs( + workflowObject.value, + workflowObjectNode, + nodeTypeDescription, + ), + ) + : []; + + const outputs = + workflowObjectNode && nodeTypeDescription + ? normalizeElementEndpoints( + NodeHelpers.getNodeOutputs( + workflowObject.value, + workflowObjectNode, + nodeTypeDescription, + ), + ) + : []; + + const data: CanvasElementData = { + type: node.type, + typeVersion: node.typeVersion, + inputs, + outputs, + }; return { id: node.id, - type: 'node', // @TODO Handle "n8n-nodes-base.stickyNote" type - position: node.position, - metadata: node, - inputs: normalizeElementEndpoints(nodeType?.inputs ?? []), - outputs: normalizeElementEndpoints(nodeType?.outputs ?? []), + label: 'node', + type: 'canvas-node', + position: { x: node.position[0], y: node.position[1] }, + data, }; }), ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7abbcb9eee3d..fb5f898a5586e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1053,6 +1053,9 @@ importers: '@n8n/permissions': specifier: workspace:* version: link:../@n8n/permissions + '@vue-flow/core': + specifier: ^1.33.5 + version: 1.33.5(vue@3.4.21) '@vueuse/components': specifier: ^10.5.0 version: 10.5.0(vue@3.4.21) @@ -8985,7 +8988,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.4.2) - vue-component-type-helpers: 2.0.7 + vue-component-type-helpers: 2.0.12 transitivePeerDependencies: - encoding - supports-color @@ -10332,6 +10335,20 @@ packages: path-browserify: 1.0.1 dev: true + /@vue-flow/core@1.33.5(vue@3.4.21): + resolution: {integrity: sha512-Obo+KHmcww/NYGARMqVH1dhd42QeFzV+TNwytrjVgYCoMVCNjs/blCh437TYTsNy4vgX1NKpNwTbQrS+keurgA==} + peerDependencies: + vue: ^3.3.0 + dependencies: + '@vueuse/core': 10.5.0(vue@3.4.21) + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.4.21(typescript@5.4.2) + transitivePeerDependencies: + - '@vue/composition-api' + dev: false + /@vue/compiler-core@3.3.4: resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} dependencies: @@ -12804,11 +12821,6 @@ packages: dependencies: safe-buffer: 5.2.1 - /content-type@1.0.4: - resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} - engines: {node: '>= 0.6'} - dev: false - /content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -13242,6 +13254,24 @@ packages: yauzl: 2.10.0 dev: true + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + /d3-dsv@2.0.0: resolution: {integrity: sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==} hasBin: true @@ -13251,6 +13281,53 @@ packages: rw: 1.3.3 dev: false + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -25663,8 +25740,8 @@ packages: resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==} dev: true - /vue-component-type-helpers@2.0.7: - resolution: {integrity: sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==} + /vue-component-type-helpers@2.0.12: + resolution: {integrity: sha512-iVJugClQdu3ZyF0N4CF3Egi+gWYfnxlIPPGtFXZG29rF3kQIuziP+k7rVGCCHiibIOQ1SlspKjrh+LRYzMpwTA==} dev: true /vue-demi@0.14.5(vue@3.4.21): From ee990833349f799084e0784266c5d1b137bd0890 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 12 Apr 2024 10:41:35 +0300 Subject: [PATCH 03/22] chore: remove deprecated JSPlumb migration --- .../src/components/canvas/CanvasJsPlumb.vue | 222 ------------------ .../src/components/canvas/plugins/index.ts | 9 - 2 files changed, 231 deletions(-) delete mode 100644 packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue delete mode 100644 packages/editor-ui/src/components/canvas/plugins/index.ts diff --git a/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue b/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue deleted file mode 100644 index 382f1c932ad8e..0000000000000 --- a/packages/editor-ui/src/components/canvas/CanvasJsPlumb.vue +++ /dev/null @@ -1,222 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/canvas/plugins/index.ts b/packages/editor-ui/src/components/canvas/plugins/index.ts deleted file mode 100644 index 1c38ed369733c..0000000000000 --- a/packages/editor-ui/src/components/canvas/plugins/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CanvasPlugin } from '@/types'; - -export const canvasPan: CanvasPlugin = (ctx) => { - ctx.instance.bind('pan', (e) => { - console.log('pan', e); - }); -}; - -export const canvasZoom: CanvasPlugin = () => {}; From e8dd041822faafa9e0c52158fe9eae96b24a322c Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 12 Apr 2024 10:50:15 +0300 Subject: [PATCH 04/22] fix: migrate to css modules --- packages/editor-ui/src/components/canvas/Canvas.vue | 13 ++++++++----- .../src/components/canvas/elements/CanvasNode.vue | 12 +++++++----- packages/editor-ui/src/views/NodeView.v2.vue | 10 ++++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 5cf345a3bca68..9201494a419bb 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -1,10 +1,13 @@ - diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 047a9afb3793d..591bd01d4fa0d 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -2,6 +2,8 @@ import { createApp } from 'vue'; import '@vue-flow/core/dist/style.css'; import '@vue-flow/core/dist/theme-default.css'; +import '@vue-flow/controls/dist/style.css'; +import '@vue-flow/minimap/dist/style.css'; import 'vue-json-pretty/lib/styles.css'; import '@jsplumb/browser-ui/css/jsplumbtoolkit.css'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb5f898a5586e..cb06c986ae96a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1053,9 +1053,18 @@ importers: '@n8n/permissions': specifier: workspace:* version: link:../@n8n/permissions + '@vue-flow/background': + specifier: ^1.3.0 + version: 1.3.0(@vue-flow/core@1.33.5)(vue@3.4.21) + '@vue-flow/controls': + specifier: ^1.1.1 + version: 1.1.1(@vue-flow/core@1.33.5)(vue@3.4.21) '@vue-flow/core': specifier: ^1.33.5 version: 1.33.5(vue@3.4.21) + '@vue-flow/minimap': + specifier: ^1.4.0 + version: 1.4.0(@vue-flow/core@1.33.5)(vue@3.4.21) '@vueuse/components': specifier: ^10.5.0 version: 10.5.0(vue@3.4.21) @@ -8988,7 +8997,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.4.2) - vue-component-type-helpers: 2.0.12 + vue-component-type-helpers: 2.0.13 transitivePeerDependencies: - encoding - supports-color @@ -10335,6 +10344,26 @@ packages: path-browserify: 1.0.1 dev: true + /@vue-flow/background@1.3.0(@vue-flow/core@1.33.5)(vue@3.4.21): + resolution: {integrity: sha512-fu/8s9wzSOQIitnSTI10XT3bzTtagh4h8EF2SWwtlDklOZjAaKy75lqv4htHa3wigy/r4LGCOGwLw3Pk88/AxA==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + dependencies: + '@vue-flow/core': 1.33.5(vue@3.4.21) + vue: 3.4.21(typescript@5.4.2) + dev: false + + /@vue-flow/controls@1.1.1(@vue-flow/core@1.33.5)(vue@3.4.21): + resolution: {integrity: sha512-TCoRD5aYZQsM/N7QlPJcIILg1Gxm0O/zoUikxaeadcom1OlKFHutY72agsySJEWM6fTlyb7w8DYCbB4T8YbFoQ==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + dependencies: + '@vue-flow/core': 1.33.5(vue@3.4.21) + vue: 3.4.21(typescript@5.4.2) + dev: false + /@vue-flow/core@1.33.5(vue@3.4.21): resolution: {integrity: sha512-Obo+KHmcww/NYGARMqVH1dhd42QeFzV+TNwytrjVgYCoMVCNjs/blCh437TYTsNy4vgX1NKpNwTbQrS+keurgA==} peerDependencies: @@ -10349,6 +10378,18 @@ packages: - '@vue/composition-api' dev: false + /@vue-flow/minimap@1.4.0(@vue-flow/core@1.33.5)(vue@3.4.21): + resolution: {integrity: sha512-GetmN8uOQxIx4ja85VDnIwpZO5SnGIOfYSqUw8MRMbc8CdACun2M2e3FRaMZRaWapclU2ssXms4xHMWrA3YWpw==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + dependencies: + '@vue-flow/core': 1.33.5(vue@3.4.21) + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.4.21(typescript@5.4.2) + dev: false + /@vue/compiler-core@3.3.4: resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} dependencies: @@ -25740,8 +25781,8 @@ packages: resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==} dev: true - /vue-component-type-helpers@2.0.12: - resolution: {integrity: sha512-iVJugClQdu3ZyF0N4CF3Egi+gWYfnxlIPPGtFXZG29rF3kQIuziP+k7rVGCCHiibIOQ1SlspKjrh+LRYzMpwTA==} + /vue-component-type-helpers@2.0.13: + resolution: {integrity: sha512-xNO5B7DstNWETnoYflLkVgh8dK8h2ZDgxY1M2O0zrqGeBNq5yAZ8a10yCS9+HnixouNGYNX+ggU9MQQq86HTpg==} dev: true /vue-demi@0.14.5(vue@3.4.21): From cd41d08d7ac43b66e003ddfc77198b4ee1705ad1 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 12 Apr 2024 11:53:01 +0300 Subject: [PATCH 06/22] fix: remove unused function --- packages/editor-ui/src/components/canvas/Canvas.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 2781889cb0e83..fef7b60770fd1 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -38,10 +38,6 @@ function onNodesChange(e: NodeChange[]) { function onConnectionsChange(e: EdgeChange[]) { console.log('onConnectionsChange', e); } - -async function resetViewport() { - await setViewport({ x: 0, y: 0, zoom: 1 }); -}