fix(editor): Keep workflow pristine after load on new canvas (#11579)

This commit is contained in:
Alex Grozav 2024-11-06 12:14:23 +02:00 committed by GitHub
parent a26c0e2c3c
commit 7254359855
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 147 additions and 20 deletions

View file

@ -7,7 +7,7 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import type { CanvasNode } from '@/types';
import type { CanvasConnection, CanvasNode } from '@/types';
import { CanvasConnectionMode } from '@/types';
import type { ICredentialsResponse, INodeUi, IWorkflowDb } from '@/Interface';
import { RemoveNodeCommand } from '@/models/history';
@ -618,6 +618,48 @@ describe('useCanvasOperations', () => {
const added = await addNodes(nodes, {});
expect(added.length).toBe(2);
});
it('should mark UI state as dirty', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = useNodeTypesStore();
const nodeTypeName = 'type';
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
workflowsStore.getCurrentWorkflow.mockReturnValue(
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
await addNodes(nodes, { keepPristine: false });
expect(uiStore.stateIsDirty).toEqual(true);
});
it('should not mark UI state as dirty if keepPristine is true', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = useNodeTypesStore();
const nodeTypeName = 'type';
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
workflowsStore.getCurrentWorkflow.mockReturnValue(
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
};
const { addNodes } = useCanvasOperations({ router });
await addNodes(nodes, { keepPristine: true });
expect(uiStore.stateIsDirty).toEqual(false);
});
});
describe('revertAddNode', () => {
@ -1013,6 +1055,26 @@ describe('useCanvasOperations', () => {
],
});
});
it('should set UI state as dirty', async () => {
const uiStore = mockedStore(useUIStore);
const connections: CanvasConnection[] = [];
const { addConnections } = useCanvasOperations({ router });
await addConnections(connections, { keepPristine: false });
expect(uiStore.stateIsDirty).toBe(true);
});
it('should not set UI state as dirty if keepPristine is true', async () => {
const uiStore = mockedStore(useUIStore);
const connections: CanvasConnection[] = [];
const { addConnections } = useCanvasOperations({ router });
await addConnections(connections, { keepPristine: true });
expect(uiStore.stateIsDirty).toBe(false);
});
});
describe('createConnection', () => {
@ -1102,6 +1164,57 @@ describe('useCanvasOperations', () => {
});
expect(uiStore.stateIsDirty).toBe(true);
});
it('should not set UI state as dirty if keepPristine is true', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const nodeTypeDescription = mockNodeTypeDescription({
name: SET_NODE_TYPE,
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
});
const nodeA = createTestNode({
id: 'a',
type: nodeTypeDescription.name,
name: 'Node A',
});
const nodeB = createTestNode({
id: 'b',
type: nodeTypeDescription.name,
name: 'Node B',
});
const connection: Connection = {
source: nodeA.id,
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
target: nodeB.id,
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
};
nodeTypesStore.nodeTypes = {
node: { 1: nodeTypeDescription },
};
workflowsStore.workflow.nodes = [nodeA, nodeB];
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
createConnection(connection, { keepPristine: true });
expect(uiStore.stateIsDirty).toBe(false);
});
});
describe('revertCreateConnection', () => {

View file

@ -105,14 +105,23 @@ type AddNodeDataWithTypeVersion = AddNodeData & {
typeVersion: INodeUi['typeVersion'];
};
type AddNodeOptions = {
type AddNodesBaseOptions = {
dragAndDrop?: boolean;
openNDV?: boolean;
trackHistory?: boolean;
isAutoAdd?: boolean;
keepPristine?: boolean;
telemetry?: boolean;
};
type AddNodesOptions = AddNodesBaseOptions & {
position?: XYPosition;
trackBulk?: boolean;
};
type AddNodeOptions = AddNodesBaseOptions & {
openNDV?: boolean;
isAutoAdd?: boolean;
};
export function useCanvasOperations({ router }: { router: ReturnType<typeof useRouter> }) {
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
@ -479,17 +488,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
);
}
async function addNodes(
nodes: AddedNodesAndConnections['nodes'],
options: {
dragAndDrop?: boolean;
position?: XYPosition;
trackHistory?: boolean;
trackBulk?: boolean;
keepPristine?: boolean;
telemetry?: boolean;
} = {},
) {
async function addNodes(nodes: AddedNodesAndConnections['nodes'], options: AddNodesOptions = {}) {
let insertPosition = options.position;
let lastAddedNode: INodeUi | undefined;
const addedNodes: INodeUi[] = [];
@ -615,6 +614,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
void nextTick(() => {
workflowsStore.setNodePristine(nodeData.name, true);
if (!options.keepPristine) {
uiStore.stateIsDirty = true;
}
nodeHelpers.matchCredentials(nodeData);
nodeHelpers.updateNodeParameterIssues(nodeData);
nodeHelpers.updateNodeCredentialIssues(nodeData);
@ -1073,7 +1076,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
* Connection operations
*/
function createConnection(connection: Connection, { trackHistory = false } = {}) {
function createConnection(
connection: Connection,
{ trackHistory = false, keepPristine = false } = {},
) {
const sourceNode = workflowsStore.getNodeById(connection.source);
const targetNode = workflowsStore.getNodeById(connection.target);
if (!sourceNode || !targetNode) {
@ -1107,7 +1113,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
nodeHelpers.updateNodeInputIssues(targetNode);
});
uiStore.stateIsDirty = true;
if (!keepPristine) {
uiStore.stateIsDirty = true;
}
}
function revertCreateConnection(connection: [IConnection, IConnection]) {
@ -1316,7 +1324,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
async function addConnections(
connections: CanvasConnectionCreateData[] | CanvasConnection[],
{ trackBulk = true, trackHistory = false } = {},
{ trackBulk = true, trackHistory = false, keepPristine = false } = {},
) {
await nextTick(); // Connection creation relies on the nodes being already added to the store
@ -1325,12 +1333,16 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
}
for (const connection of connections) {
createConnection(connection, { trackHistory });
createConnection(connection, { trackHistory, keepPristine });
}
if (trackBulk && trackHistory) {
historyStore.stopRecordingUndo();
}
if (!keepPristine) {
uiStore.stateIsDirty = true;
}
}
/**
@ -1372,7 +1384,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
// Add nodes and connections
await addNodes(data.nodes, { keepPristine: true });
await addConnections(mapLegacyConnectionsToCanvasConnections(data.connections, data.nodes));
await addConnections(mapLegacyConnectionsToCanvasConnections(data.connections, data.nodes), {
keepPristine: true,
});
}
/**