diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 8fe9b64379e3f..07a0b273e2410 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -637,8 +637,7 @@ export default defineComponent({ // so we only update it when necessary (when node is mounted and when it's opened and closed (isActive)) try { const nodeSubtitle = - this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.getCurrentWorkflow()) || - ''; + this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.workflow) || ''; this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle; } catch (e) { diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index 28b27ea3383c0..712add0cd2361 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -23,6 +23,8 @@ export const i18nInstance = createI18n({ }); export class I18nClass { + private baseTextCache = new Map(); + private get i18n() { return i18nInstance.global; } @@ -50,11 +52,25 @@ export class I18nClass { key: BaseTextKey, options?: { adjustToNumber?: number; interpolate?: { [key: string]: string } }, ): string { + // Create a unique cache key + const cacheKey = `${key}-${JSON.stringify(options)}`; + + // Check if the result is already cached + if (this.baseTextCache.has(cacheKey)) { + return this.baseTextCache.get(cacheKey) ?? key; + } + + let result: string; if (options?.adjustToNumber !== undefined) { - return this.i18n.tc(key, options.adjustToNumber, options?.interpolate).toString(); + result = this.i18n.tc(key, options.adjustToNumber, options?.interpolate ?? {}).toString(); + } else { + result = this.i18n.t(key, options?.interpolate ?? {}).toString(); } - return this.i18n.t(key, options?.interpolate).toString(); + // Store the result in the cache + this.baseTextCache.set(cacheKey, result); + + return result; } /** diff --git a/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointRenderer.ts b/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointRenderer.ts index c7102d755219a..54d4d98695598 100644 --- a/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointRenderer.ts +++ b/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointRenderer.ts @@ -70,7 +70,6 @@ export const register = () => { container.appendChild(unconnectedGroup); container.appendChild(defaultGroup); - endpointInstance.setupOverlays(); endpointInstance.setVisible(false); return container; diff --git a/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointType.ts b/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointType.ts index d80697fe8d21a..4bd54ae486b58 100644 --- a/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointType.ts +++ b/packages/editor-ui/src/plugins/jsplumb/N8nAddInputEndpointType.ts @@ -33,11 +33,6 @@ export class N8nAddInputEndpoint extends EndpointRepresentation { this.endpoint.getOverlays()[overlay].setVisible(visible); }); - this.setVisible(visible); - // Re-trigger the success state if label is set if (visible && this.label) { this.setSuccessOutput(this.label); } - this.instance.setSuspendDrawing(false); } setSuccessOutput(label: string) { diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 5818dd4f5098a..a217641863410 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -894,8 +894,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, }; } - - this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData); }, resetAllNodesIssues(): boolean { @@ -1130,7 +1128,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }; } this.workflowExecutionData.data!.resultData.runData[pushData.nodeName].push(pushData.data); - this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData); }, clearNodeExecutionData(nodeName: string): void { if (!this.workflowExecutionData?.data) { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 88058420803d0..62a587743e1b0 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -75,6 +75,7 @@ @removeNode="(name) => removeNode(name, true)" :key="`${stickyData.id}_sticky`" :name="stickyData.name" + :workflow="currentWorkflowObject" :isReadOnly="isReadOnlyRoute || readOnlyEnv" :instance="instance" :isActive="!!activeNode && activeNode.name === stickyData.name" @@ -732,6 +733,7 @@ export default defineComponent({ showTriggerMissingTooltip: false, workflowData: null as INewWorkflowData | null, activeConnection: null as null | Connection, + isInsertingNodes: false, isProductionExecutionPreview: false, enterTimer: undefined as undefined | ReturnType, exitTimer: undefined as undefined | ReturnType, @@ -2735,15 +2737,6 @@ export default defineComponent({ }); }, ); - setTimeout(() => { - NodeViewUtils.addConnectionTestData( - info.source, - info.target, - info.connection?.connector?.hasOwnProperty('canvas') - ? (info.connection.connector.canvas as HTMLElement) - : undefined, - ); - }, 0); const endpointArrow = NodeViewUtils.getOverlay( info.connection, @@ -2763,14 +2756,44 @@ export default defineComponent({ if (!this.suspendRecordingDetachedConnections) { this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData)); } - this.nodeHelpers.updateNodesInputIssues(); - this.resetEndpointsErrors(); + // When we add multiple nodes, this event could be fired hundreds of times for large workflows. + // And because the updateNodesInputIssues() method is quite expensive, we only call it if not in insert mode + if (!this.isInsertingNodes) { + this.nodeHelpers.updateNodesInputIssues(); + this.resetEndpointsErrors(); + setTimeout(() => { + NodeViewUtils.addConnectionTestData( + info.source, + info.target, + info.connection?.connector?.hasOwnProperty('canvas') + ? (info.connection.connector.canvas as HTMLElement) + : undefined, + ); + }, 0); + } } } catch (e) { console.error(e); } }, + addConectionsTestData() { + this.instance.connections.forEach((connection) => { + NodeViewUtils.addConnectionTestData( + connection.source, + connection.target, + connection?.connector?.hasOwnProperty('canvas') + ? (connection?.connector.canvas as HTMLElement) + : undefined, + ); + }); + }, onDragMove() { + const totalNodes = this.nodes.length; + void this.callDebounced('updateConnectionsOverlays', { + debounceTime: totalNodes > 20 ? 200 : 0, + }); + }, + updateConnectionsOverlays() { this.instance?.connections.forEach((connection) => { NodeViewUtils.showOrHideItemsLabel(connection); NodeViewUtils.showOrHideMidpointArrow(connection); @@ -3899,7 +3922,7 @@ export default defineComponent({ if (!nodes?.length) { return; } - + this.isInsertingNodes = true; // Before proceeding we must check if all nodes contain the `properties` attribute. // Nodes are loaded without this information so we must make sure that all nodes // being added have this information. @@ -3957,60 +3980,64 @@ export default defineComponent({ // check and match credentials, apply new format if old is used this.matchCredentials(node); - this.workflowsStore.addNode(node); if (trackHistory) { this.historyStore.pushCommandToUndo(new AddNodeCommand(node)); } }); - // Wait for the node to be rendered + // Wait for the nodes to be rendered await this.$nextTick(); - // Suspend drawing this.instance?.setSuspendDrawing(true); - // Load the connections - if (connections !== undefined) { - let connectionData; - for (const sourceNode of Object.keys(connections)) { - for (const type of Object.keys(connections[sourceNode])) { - for ( - let sourceIndex = 0; - sourceIndex < connections[sourceNode][type].length; - sourceIndex++ - ) { - const outwardConnections = connections[sourceNode][type][sourceIndex]; - if (!outwardConnections) { - continue; - } + if (connections) { + await this.addConnections(connections); + } + // Add the node issues at the end as the node-connections are required + this.nodeHelpers.refreshNodeIssues(); + this.nodeHelpers.updateNodesInputIssues(); + this.resetEndpointsErrors(); + this.isInsertingNodes = false; + + // Now it can draw again + this.instance?.setSuspendDrawing(false, true); + }, + async addConnections(connections: IConnections) { + const batchedConnectionData: Array<[IConnection, IConnection]> = []; + + for (const sourceNode in connections) { + for (const type in connections[sourceNode]) { + connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => { + if (outwardConnections) { outwardConnections.forEach((targetData) => { - connectionData = [ - { - node: sourceNode, - type, - index: sourceIndex, - }, - { - node: targetData.node, - type: targetData.type, - index: targetData.index, - }, - ] as [IConnection, IConnection]; - - this.__addConnection(connectionData); + batchedConnectionData.push([ + { node: sourceNode, type, index: sourceIndex }, + { node: targetData.node, type: targetData.type, index: targetData.index }, + ]); }); } - } + }); } } - // Add the node issues at the end as the node-connections are required - void this.nodeHelpers.refreshNodeIssues(); + // Process the connections in batches + await this.processConnectionBatch(batchedConnectionData); + setTimeout(this.addConectionsTestData, 0); + }, - // Now it can draw again - this.instance?.setSuspendDrawing(false, true); + async processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) { + const batchSize = 100; + + for (let i = 0; i < batchedConnectionData.length; i += batchSize) { + const batch = batchedConnectionData.slice(i, i + batchSize); + + batch.forEach((connectionData) => { + this.__addConnection(connectionData); + }); + } }, + async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise { // Because nodes with the same name maybe already exist, it could // be needed that they have to be renamed. Also could it be possible