From bc4857a1b3d6ea389f11fb8246a1cee33b8a008e Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 26 Nov 2024 14:39:52 +0200 Subject: [PATCH] fix(editor): Use `crypto.randomUUID()` to initialize node id if missing on new canvas (#11873) --- .../src/composables/useCanvasOperations.ts | 17 ++++++------ .../src/composables/useNodeHelpers.test.ts | 26 +++++++++++++++++++ .../src/composables/useNodeHelpers.ts | 17 ++++++++++-- .../src/composables/useWorkflowHelpers.ts | 9 +++---- .../editor-ui/src/stores/workflows.store.ts | 15 ++++++++--- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index e4400ea665..b30bab1892 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -90,7 +90,6 @@ import type { Workflow, } from 'n8n-workflow'; import { deepCopy, NodeConnectionType, NodeHelpers, TelemetryHelpers } from 'n8n-workflow'; -import { v4 as uuid } from 'uuid'; import { computed, nextTick, ref } from 'vue'; import type { useRouter } from 'vue-router'; import { useClipboard } from '@/composables/useClipboard'; @@ -759,7 +758,7 @@ export function useCanvasOperations({ router }: { router: ReturnType n.webhookId === node.webhookId, ); if (isDuplicate) { - node.webhookId = uuid(); + nodeHelpers.assignWebhookId(node); if (node.parameters.path) { node.parameters.path = node.webhookId as string; @@ -1623,13 +1622,13 @@ export function useCanvasOperations({ router }: { router: ReturnType { }, ); }); + + describe('assignNodeId()', () => { + it('should assign a unique id to the node', () => { + const { assignNodeId } = useNodeHelpers(); + const node = createTestNode({ + id: '', + }); + + assignNodeId(node); + expect(node.id).not.toBe(''); + expect(node.id).toMatch(/\w+(-\w+)+/); + }); + }); + + describe('assignWebhookId', () => { + it('should assign a unique id to the webhook', () => { + const { assignWebhookId } = useNodeHelpers(); + const webhook = createTestNode({ + id: '', + }); + + assignWebhookId(webhook); + expect(webhook.webhookId).not.toBe(''); + expect(webhook.webhookId).toMatch(/\w+(-\w+)+/); + }); + }); }); diff --git a/packages/editor-ui/src/composables/useNodeHelpers.ts b/packages/editor-ui/src/composables/useNodeHelpers.ts index ff512181e7..95ab27d7ec 100644 --- a/packages/editor-ui/src/composables/useNodeHelpers.ts +++ b/packages/editor-ui/src/composables/useNodeHelpers.ts @@ -1,6 +1,5 @@ import { ref, nextTick } from 'vue'; import { useRoute } from 'vue-router'; -import { v4 as uuid } from 'uuid'; import type { Connection, ConnectionDetachedParams } from '@jsplumb/core'; import { useHistoryStore } from '@/stores/history.store'; import { @@ -1187,7 +1186,7 @@ export function useNodeHelpers() { }; if (!newNode.id) { - newNode.id = uuid(); + assignNodeId(newNode); } nodeType = nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion); @@ -1257,6 +1256,18 @@ export function useNodeHelpers() { canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true); } + function assignNodeId(node: INodeUi) { + const id = window.crypto.randomUUID(); + node.id = id; + return id; + } + + function assignWebhookId(node: INodeUi) { + const id = window.crypto.randomUUID(); + node.webhookId = id; + return id; + } + return { hasProxyAuth, isCustomApiCallSelected, @@ -1292,5 +1303,7 @@ export function useNodeHelpers() { addPinDataConnections, removePinDataConnections, getNodeTaskData, + assignNodeId, + assignWebhookId, }; } diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index 87492ac545..23200cdc76 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -55,7 +55,6 @@ import { useTemplatesStore } from '@/stores/templates.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getSourceItems } from '@/utils/pairedItemUtils'; -import { v4 as uuid } from 'uuid'; import { useSettingsStore } from '@/stores/settings.store'; import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes'; import { useDocumentTitle } from '@/composables/useDocumentTitle'; @@ -64,13 +63,12 @@ import { useCanvasStore } from '@/stores/canvas.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { tryToParseNumber } from '@/utils/typesUtils'; import { useI18n } from '@/composables/useI18n'; -import type { useRouter } from 'vue-router'; +import type { useRouter, NavigationGuardNext } from 'vue-router'; import { useTelemetry } from '@/composables/useTelemetry'; import { useProjectsStore } from '@/stores/projects.store'; import { useTagsStore } from '@/stores/tags.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; -import type { NavigationGuardNext } from 'vue-router'; type ResolveParameterOptions = { targetItem?: TargetItem; @@ -937,7 +935,7 @@ export function useWorkflowHelpers(options: { router: ReturnType { - node.id = uuid(); + nodeHelpers.assignNodeId(node); return node; }); @@ -946,8 +944,7 @@ export function useWorkflowHelpers(options: { router: ReturnType { if (node.webhookId) { - const newId = uuid(); - node.webhookId = newId; + const newId = nodeHelpers.assignWebhookId(node); node.parameters.path = newId; changedNodes[node.name] = node.webhookId; } diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 43749bce49..f49beaed84 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -85,6 +85,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useRouter } from 'vue-router'; import { useSettingsStore } from './settings.store'; import { openPopUpWindow } from '@/utils/executionUtils'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; const defaults: Omit & { settings: NonNullable } = { name: '', @@ -117,6 +118,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { const workflowHelpers = useWorkflowHelpers({ router }); const settingsStore = useSettingsStore(); const rootStore = useRootStore(); + const nodeHelpers = useNodeHelpers(); // -1 means the backend chooses the default // 0 is the old flow @@ -1037,10 +1039,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { function setNodes(nodes: INodeUi[]): void { workflow.value.nodes = nodes; - nodeMetadata.value = nodes.reduce((acc, node) => { - acc[node.name] = { pristine: true }; - return acc; - }, {}); + nodes.forEach((node) => { + if (!node.id) { + nodeHelpers.assignNodeId(node); + } + + if (!nodeMetadata.value[node.name]) { + nodeMetadata.value[node.name] = { pristine: true }; + } + }); } function setConnections(connections: IConnections, updateWorkflow = false): void {