mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
fix(editor): Align undo/redo functionality on new canvas (no-changelog) (#11154)
This commit is contained in:
parent
18c0c8612c
commit
ad4cb0ea0c
|
@ -87,6 +87,8 @@ describe('Canvas', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle `update:nodes:position` event', async () => {
|
it('should handle `update:nodes:position` event', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
const nodes = [createCanvasNodeElement()];
|
const nodes = [createCanvasNodeElement()];
|
||||||
const { container, emitted } = renderComponent({
|
const { container, emitted } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -110,22 +112,9 @@ describe('Canvas', () => {
|
||||||
});
|
});
|
||||||
await fireEvent.mouseUp(node, { view: window });
|
await fireEvent.mouseUp(node, { view: window });
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(250); // Event debounce time
|
||||||
|
|
||||||
expect(emitted()['update:nodes:position']).toEqual([
|
expect(emitted()['update:nodes:position']).toEqual([
|
||||||
[
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'position',
|
|
||||||
dragging: true,
|
|
||||||
from: {
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
position: { x: 80, y: 80 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,7 +29,7 @@ import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||||
import { CanvasKey } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
import { onKeyDown, onKeyUp } from '@vueuse/core';
|
import { onKeyDown, onKeyUp, useDebounceFn } from '@vueuse/core';
|
||||||
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
@ -180,21 +180,22 @@ function onClickNodeAdd(id: string, handle: string) {
|
||||||
emit('click:node:add', id, handle);
|
emit('click:node:add', id, handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateNodesPosition(events: NodePositionChange[]) {
|
// Debounced to prevent emitting too many events, necessary for undo/redo
|
||||||
|
const onUpdateNodesPosition = useDebounceFn((events: NodePositionChange[]) => {
|
||||||
emit('update:nodes:position', events);
|
emit('update:nodes:position', events);
|
||||||
}
|
}, 200);
|
||||||
|
|
||||||
function onUpdateNodePosition(id: string, position: XYPosition) {
|
function onUpdateNodePosition(id: string, position: XYPosition) {
|
||||||
emit('update:node:position', id, position);
|
emit('update:node:position', id, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNodesChange(events: NodeChange[]) {
|
const isPositionChangeEvent = (event: NodeChange): event is NodePositionChange =>
|
||||||
const isPositionChangeEvent = (event: NodeChange): event is NodePositionChange =>
|
|
||||||
event.type === 'position' && 'position' in event;
|
event.type === 'position' && 'position' in event;
|
||||||
|
|
||||||
|
function onNodesChange(events: NodeChange[]) {
|
||||||
const positionChangeEndEvents = events.filter(isPositionChangeEvent);
|
const positionChangeEndEvents = events.filter(isPositionChangeEvent);
|
||||||
if (positionChangeEndEvents.length > 0) {
|
if (positionChangeEndEvents.length > 0) {
|
||||||
onUpdateNodesPosition(positionChangeEndEvents);
|
void onUpdateNodesPosition(positionChangeEndEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,7 +164,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
updateNodePosition(id, position, { trackHistory });
|
updateNodePosition(id, position, { trackHistory });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (trackBulk) {
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,11 +198,16 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
updateNodePosition(node.id, position);
|
updateNodePosition(node.id, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameNode(currentName: string, newName: string, { trackHistory = false } = {}) {
|
async function renameNode(
|
||||||
|
currentName: string,
|
||||||
|
newName: string,
|
||||||
|
{ trackHistory = false, trackBulk = true } = {},
|
||||||
|
) {
|
||||||
if (currentName === newName) {
|
if (currentName === newName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (trackHistory) {
|
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +232,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
ndvStore.activeNodeName = newName;
|
ndvStore.activeNodeName = newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackHistory) {
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,11 +332,17 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
trackDeleteNode(id);
|
trackDeleteNode(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteNodes(ids: string[]) {
|
function deleteNodes(ids: string[], { trackHistory = true, trackBulk = true } = {}) {
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
ids.forEach((id) => deleteNode(id, { trackHistory: true, trackBulk: false }));
|
}
|
||||||
|
|
||||||
|
ids.forEach((id) => deleteNode(id, { trackHistory, trackBulk: false }));
|
||||||
|
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function revertDeleteNode(node: INodeUi) {
|
function revertDeleteNode(node: INodeUi) {
|
||||||
workflowsStore.addNode(node);
|
workflowsStore.addNode(node);
|
||||||
|
@ -401,18 +412,15 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
uiStore.lastSelectedNode = node.name;
|
uiStore.lastSelectedNode = node.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNodesDisabled(
|
function toggleNodesDisabled(ids: string[], { trackHistory = true, trackBulk = true } = {}) {
|
||||||
ids: string[],
|
if (trackHistory && trackBulk) {
|
||||||
{ trackHistory = true, trackBulk = true }: { trackHistory?: boolean; trackBulk?: boolean } = {},
|
|
||||||
) {
|
|
||||||
if (trackBulk) {
|
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = workflowsStore.getNodesByIds(ids);
|
const nodes = workflowsStore.getNodesByIds(ids);
|
||||||
nodeHelpers.disableNodes(nodes, trackHistory);
|
nodeHelpers.disableNodes(nodes, { trackHistory, trackBulk: false });
|
||||||
|
|
||||||
if (trackBulk) {
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,8 +432,14 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNodesPinned(ids: string[], source: PinDataSource) {
|
function toggleNodesPinned(
|
||||||
|
ids: string[],
|
||||||
|
source: PinDataSource,
|
||||||
|
{ trackHistory = true, trackBulk = true } = {},
|
||||||
|
) {
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
|
}
|
||||||
|
|
||||||
const nodes = workflowsStore.getNodesByIds(ids);
|
const nodes = workflowsStore.getNodesByIds(ids);
|
||||||
const nextStatePinned = nodes.some((node) => !workflowsStore.pinDataByNodeName(node.name));
|
const nextStatePinned = nodes.some((node) => !workflowsStore.pinDataByNodeName(node.name));
|
||||||
|
@ -442,8 +456,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function requireNodeTypeDescription(
|
function requireNodeTypeDescription(
|
||||||
type: INodeUi['type'],
|
type: INodeUi['type'],
|
||||||
|
@ -478,10 +494,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
let insertPosition = options.position;
|
let insertPosition = options.position;
|
||||||
let lastAddedNode: INodeUi | undefined;
|
let lastAddedNode: INodeUi | undefined;
|
||||||
|
|
||||||
if (options.trackBulk) {
|
|
||||||
historyStore.startRecordingUndo();
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodesWithTypeVersion = nodes.map((node) => {
|
const nodesWithTypeVersion = nodes.map((node) => {
|
||||||
const typeVersion =
|
const typeVersion =
|
||||||
node.typeVersion ?? resolveNodeVersion(requireNodeTypeDescription(node.type));
|
node.typeVersion ?? resolveNodeVersion(requireNodeTypeDescription(node.type));
|
||||||
|
@ -493,7 +505,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
await loadNodeTypesProperties(nodesWithTypeVersion);
|
await loadNodeTypesProperties(nodesWithTypeVersion);
|
||||||
|
|
||||||
if (options.trackBulk) {
|
if (options.trackHistory && options.trackBulk) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +544,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
updatePositionForNodeWithMultipleInputs(lastAddedNode);
|
updatePositionForNodeWithMultipleInputs(lastAddedNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.trackBulk) {
|
if (options.trackHistory && options.trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,11 +600,11 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
workflowsStore.addNode(nodeData);
|
workflowsStore.addNode(nodeData);
|
||||||
|
|
||||||
void nextTick(() => {
|
|
||||||
if (options.trackHistory) {
|
if (options.trackHistory) {
|
||||||
historyStore.pushCommandToUndo(new AddNodeCommand(nodeData));
|
historyStore.pushCommandToUndo(new AddNodeCommand(nodeData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void nextTick(() => {
|
||||||
workflowsStore.setNodePristine(nodeData.name, true);
|
workflowsStore.setNodePristine(nodeData.name, true);
|
||||||
|
|
||||||
nodeHelpers.matchCredentials(nodeData);
|
nodeHelpers.matchCredentials(nodeData);
|
||||||
|
@ -1376,7 +1388,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
connections: CanvasConnectionCreateData[] | CanvasConnection[],
|
connections: CanvasConnectionCreateData[] | CanvasConnection[],
|
||||||
{ trackBulk = true, trackHistory = false } = {},
|
{ trackBulk = true, trackHistory = false } = {},
|
||||||
) {
|
) {
|
||||||
if (trackBulk) {
|
if (trackBulk && trackHistory) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1404,7 +1416,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackBulk) {
|
if (trackBulk && trackHistory) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1614,11 +1626,11 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the nodes with the changed node names, expressions and connections
|
// Add the nodes with the changed node names, expressions and connections
|
||||||
if (trackBulk) {
|
if (trackBulk && trackHistory) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
await addNodes(Object.values(tempWorkflow.nodes), { trackBulk: false, trackHistory: true });
|
await addNodes(Object.values(tempWorkflow.nodes), { trackBulk: false, trackHistory });
|
||||||
addConnections(
|
addConnections(
|
||||||
mapLegacyConnectionsToCanvasConnections(
|
mapLegacyConnectionsToCanvasConnections(
|
||||||
tempWorkflow.connectionsBySourceNode,
|
tempWorkflow.connectionsBySourceNode,
|
||||||
|
@ -1627,7 +1639,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
{ trackBulk: false, trackHistory },
|
{ trackBulk: false, trackHistory },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (trackBulk) {
|
if (trackBulk && trackHistory) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1643,6 +1655,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
workflowData: IWorkflowDataUpdate,
|
workflowData: IWorkflowDataUpdate,
|
||||||
source: string,
|
source: string,
|
||||||
importTags = true,
|
importTags = true,
|
||||||
|
{ trackBulk = true, trackHistory = true } = {},
|
||||||
): Promise<IWorkflowDataUpdate> {
|
): Promise<IWorkflowDataUpdate> {
|
||||||
uiStore.resetLastInteractedWith();
|
uiStore.resetLastInteractedWith();
|
||||||
|
|
||||||
|
@ -1731,7 +1744,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value),
|
NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
await addImportedNodesToWorkflow(workflowData);
|
await addImportedNodesToWorkflow(workflowData, { trackBulk, trackHistory });
|
||||||
|
|
||||||
if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
|
if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
|
||||||
await importWorkflowTags(workflowData);
|
await importWorkflowTags(workflowData);
|
||||||
|
|
|
@ -652,10 +652,10 @@ export function useNodeHelpers() {
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableNodes(nodes: INodeUi[], trackHistory = false) {
|
function disableNodes(nodes: INodeUi[], { trackHistory = false, trackBulk = true } = {}) {
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
if (trackHistory) {
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,7 +690,8 @@ export function useNodeHelpers() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackHistory) {
|
|
||||||
|
if (trackHistory && trackBulk) {
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1534,7 +1534,7 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nodeHelpers.disableNodes(nodes, true);
|
this.nodeHelpers.disableNodes(nodes, { trackHistory: true, trackBulk: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePinNodes(nodes: INode[], source: 'keyboard-shortcut' | 'context-menu') {
|
togglePinNodes(nodes: INode[], source: 'keyboard-shortcut' | 'context-menu') {
|
||||||
|
|
Loading…
Reference in a new issue