diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index 48416bd255c..f67baac731f 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -4815,6 +4815,11 @@ describe('useCanvasOperations', () => { canvasOperations.importWorkflowData(workflowDataToImport, 'paste'), ).resolves.not.toThrow(); expect(toast.showError).not.toHaveBeenCalled(); + expect(toast.showMessage).toHaveBeenCalledWith({ + type: 'error', + title: 'Could not insert node', + message: `Only one '${EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE}' node is allowed in a workflow`, + }); }); it('should remove connections for nodes filtered out during import', async () => { @@ -5368,6 +5373,39 @@ describe('useCanvasOperations', () => { expect(duplicatedNodeIds).not.toContain('2'); }); + it('should show max node type error when duplicating nodes that exceed maxNodes limit', async () => { + const toast = useToast(); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeDescription = mockNodeTypeDescription({ + name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + maxNodes: 1, + }); + const node = createTestNode({ + id: '1', + name: 'Execute Workflow Trigger', + type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + }); + + nodeTypesStore.nodeTypes = { + [EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE]: { 1: nodeTypeDescription }, + }; + workflowDocumentStoreInstance.allNodes = [node]; + vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue([node]); + vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({}); + + const workflowObject = createTestWorkflowObject({ nodes: [], connections: {} }); + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject); + + const canvasOperations = useCanvasOperations(); + await canvasOperations.duplicateNodes(['1']); + + expect(toast.showMessage).toHaveBeenCalledWith({ + type: 'error', + title: 'Could not insert node', + message: `Only one '${EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE}' node is allowed in a workflow`, + }); + }); + it('should not crash when TelemetryHelpers.generateNodesGraph throws error', async () => { const telemetry = useTelemetry(); const nodeTypesStore = useNodeTypesStore(); diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index 0fbf68ec147..50b6ec16b4c 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -881,6 +881,17 @@ export function useCanvasOperations() { } } + function showMaxNodeTypeErrorToast(nodeTypeDescription: INodeTypeDescription) { + toast.showMessage({ + type: 'error', + title: i18n.baseText('nodeView.showMessage.showMaxNodeTypeError.title'), + message: i18n.baseText('nodeView.showMessage.showMaxNodeTypeError.message', { + adjustToNumber: nodeTypeDescription.maxNodes, + interpolate: { nodeTypeDataDisplayName: nodeTypeDescription.displayName }, + }), + }); + } + function addNode( node: AddNodeDataWithTypeVersion, nodeTypeDescription: INodeTypeDescription, @@ -2431,6 +2442,7 @@ export function useCanvasOperations() { trackHistory = false, viewport = DEFAULT_VIEWPORT_BOUNDARIES, setStateDirty = true, + showMaxNodeTypeError = false, } = {}, ): Promise { // Because nodes with the same name maybe already exist, it could @@ -2456,6 +2468,7 @@ export function useCanvasOperations() { let oldName: string; let newName: string; const createNodes: INode[] = []; + const skippedMaxNodeTypes = new Map(); await nodeHelpers.loadNodesProperties( data.nodes.map((node) => ({ name: node.type, version: node.typeVersion })), @@ -2469,6 +2482,12 @@ export function useCanvasOperations() { // add the name of the existing node // that this one gets linked up instead. nodeNameTable[node.name] = nodeTypesCount[node.type].nodeNames[0]; + if (showMaxNodeTypeError) { + const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion); + if (nodeTypeDescription) { + skippedMaxNodeTypes.set(node.type, nodeTypeDescription); + } + } return; } else { // Node can be created but increment the @@ -2491,6 +2510,10 @@ export function useCanvasOperations() { createNodes.push(node); }); + if (showMaxNodeTypeError) { + skippedMaxNodeTypes.forEach(showMaxNodeTypeErrorToast); + } + // Get only the connections of the nodes that get created const newConnections: IConnections = {}; const currentConnections = data.connections ?? {}; @@ -2800,6 +2823,7 @@ export function useCanvasOperations() { trackHistory, viewport, setStateDirty, + showMaxNodeTypeError: source === 'paste' || source === 'duplicate', }); applyImportedNodeGroups(