fix(editor): Use crypto.randomUUID() to initialize node id if missing on new canvas (#11873)

This commit is contained in:
Alex Grozav 2024-11-26 14:39:52 +02:00 committed by GitHub
parent 3320436a6f
commit bc4857a1b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 63 additions and 21 deletions

View file

@ -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<typeof useR
node: AddNodeDataWithTypeVersion,
nodeTypeDescription: INodeTypeDescription,
) {
const id = node.id ?? uuid();
const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
const name = node.name ?? (nodeTypeDescription.defaults.name as string);
const type = nodeTypeDescription.name;
const typeVersion = node.typeVersion;
@ -1027,7 +1026,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function resolveNodeWebhook(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
if (nodeTypeDescription.webhooks?.length && !node.webhookId) {
node.webhookId = uuid();
nodeHelpers.assignWebhookId(node);
}
// if it's a webhook and the path is empty set the UUID as the default path
@ -1613,7 +1612,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
(n) => 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<typeof useR
}
}
// set all new ids when pasting/importing workflows
// Set all new ids when pasting/importing workflows
if (node.id) {
const newId = uuid();
nodeIdMap[newId] = node.id;
node.id = newId;
const previousId = node.id;
const newId = nodeHelpers.assignNodeId(node);
nodeIdMap[newId] = previousId;
} else {
node.id = uuid();
nodeHelpers.assignNodeId(node);
}
});
}

View file

@ -208,4 +208,30 @@ describe('useNodeHelpers()', () => {
},
);
});
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+)+/);
});
});
});

View file

@ -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,
};
}

View file

@ -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<typeof useRoute
if (resetNodeIds) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
node.id = uuid();
nodeHelpers.assignNodeId(node);
return node;
});
@ -946,8 +944,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
if (resetWebhookUrls) {
workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => {
if (node.webhookId) {
const newId = uuid();
node.webhookId = newId;
const newId = nodeHelpers.assignWebhookId(node);
node.parameters.path = newId;
changedNodes[node.name] = node.webhookId;
}

View file

@ -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<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
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<NodeMetadataMap>((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 {