mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
fix(editor): Keep workflow pristine after load on new canvas (#11579)
This commit is contained in:
parent
a26c0e2c3c
commit
7254359855
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue