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';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
import type { CanvasNode } from '@/types';
|
import type { CanvasConnection, CanvasNode } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type { ICredentialsResponse, INodeUi, IWorkflowDb } from '@/Interface';
|
import type { ICredentialsResponse, INodeUi, IWorkflowDb } from '@/Interface';
|
||||||
import { RemoveNodeCommand } from '@/models/history';
|
import { RemoveNodeCommand } from '@/models/history';
|
||||||
|
@ -618,6 +618,48 @@ describe('useCanvasOperations', () => {
|
||||||
const added = await addNodes(nodes, {});
|
const added = await addNodes(nodes, {});
|
||||||
expect(added.length).toBe(2);
|
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', () => {
|
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', () => {
|
describe('createConnection', () => {
|
||||||
|
@ -1102,6 +1164,57 @@ describe('useCanvasOperations', () => {
|
||||||
});
|
});
|
||||||
expect(uiStore.stateIsDirty).toBe(true);
|
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', () => {
|
describe('revertCreateConnection', () => {
|
||||||
|
|
|
@ -105,14 +105,23 @@ type AddNodeDataWithTypeVersion = AddNodeData & {
|
||||||
typeVersion: INodeUi['typeVersion'];
|
typeVersion: INodeUi['typeVersion'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AddNodeOptions = {
|
type AddNodesBaseOptions = {
|
||||||
dragAndDrop?: boolean;
|
dragAndDrop?: boolean;
|
||||||
openNDV?: boolean;
|
|
||||||
trackHistory?: boolean;
|
trackHistory?: boolean;
|
||||||
isAutoAdd?: boolean;
|
keepPristine?: boolean;
|
||||||
telemetry?: 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> }) {
|
export function useCanvasOperations({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
@ -479,17 +488,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addNodes(
|
async function addNodes(nodes: AddedNodesAndConnections['nodes'], options: AddNodesOptions = {}) {
|
||||||
nodes: AddedNodesAndConnections['nodes'],
|
|
||||||
options: {
|
|
||||||
dragAndDrop?: boolean;
|
|
||||||
position?: XYPosition;
|
|
||||||
trackHistory?: boolean;
|
|
||||||
trackBulk?: boolean;
|
|
||||||
keepPristine?: boolean;
|
|
||||||
telemetry?: boolean;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
let insertPosition = options.position;
|
let insertPosition = options.position;
|
||||||
let lastAddedNode: INodeUi | undefined;
|
let lastAddedNode: INodeUi | undefined;
|
||||||
const addedNodes: INodeUi[] = [];
|
const addedNodes: INodeUi[] = [];
|
||||||
|
@ -615,6 +614,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
workflowsStore.setNodePristine(nodeData.name, true);
|
workflowsStore.setNodePristine(nodeData.name, true);
|
||||||
|
|
||||||
|
if (!options.keepPristine) {
|
||||||
|
uiStore.stateIsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
nodeHelpers.matchCredentials(nodeData);
|
nodeHelpers.matchCredentials(nodeData);
|
||||||
nodeHelpers.updateNodeParameterIssues(nodeData);
|
nodeHelpers.updateNodeParameterIssues(nodeData);
|
||||||
nodeHelpers.updateNodeCredentialIssues(nodeData);
|
nodeHelpers.updateNodeCredentialIssues(nodeData);
|
||||||
|
@ -1073,7 +1076,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
* Connection operations
|
* Connection operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function createConnection(connection: Connection, { trackHistory = false } = {}) {
|
function createConnection(
|
||||||
|
connection: Connection,
|
||||||
|
{ trackHistory = false, keepPristine = false } = {},
|
||||||
|
) {
|
||||||
const sourceNode = workflowsStore.getNodeById(connection.source);
|
const sourceNode = workflowsStore.getNodeById(connection.source);
|
||||||
const targetNode = workflowsStore.getNodeById(connection.target);
|
const targetNode = workflowsStore.getNodeById(connection.target);
|
||||||
if (!sourceNode || !targetNode) {
|
if (!sourceNode || !targetNode) {
|
||||||
|
@ -1107,7 +1113,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
nodeHelpers.updateNodeInputIssues(targetNode);
|
nodeHelpers.updateNodeInputIssues(targetNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
uiStore.stateIsDirty = true;
|
if (!keepPristine) {
|
||||||
|
uiStore.stateIsDirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function revertCreateConnection(connection: [IConnection, IConnection]) {
|
function revertCreateConnection(connection: [IConnection, IConnection]) {
|
||||||
|
@ -1316,7 +1324,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
async function addConnections(
|
async function addConnections(
|
||||||
connections: CanvasConnectionCreateData[] | CanvasConnection[],
|
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
|
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) {
|
for (const connection of connections) {
|
||||||
createConnection(connection, { trackHistory });
|
createConnection(connection, { trackHistory, keepPristine });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackBulk && trackHistory) {
|
if (trackBulk && trackHistory) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!keepPristine) {
|
||||||
|
uiStore.stateIsDirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1372,7 +1384,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
// Add nodes and connections
|
// Add nodes and connections
|
||||||
await addNodes(data.nodes, { keepPristine: true });
|
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