fix(editor): Show error feedback when copying non-duplicatable triggers (#31104)

This commit is contained in:
Michael Kret 2026-06-03 08:57:00 +03:00 committed by GitHub
parent ec44980689
commit 151fd83e0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 0 deletions

View File

@ -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();

View File

@ -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<WorkflowDataUpdate> {
// 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<string, INodeTypeDescription>();
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(