mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
## Summary Preventing canvas undo/redo while NDV or any modal is open. We already had a NDV open check in place but looks like it was broken by unreactive ref inside `useHistoryHelper` composable. This PR fixes this by using store getter directly inside the helper method and adds modal open check. ## Related tickets and issues Fixes ADO-657 ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests.
151 lines
4.4 KiB
TypeScript
151 lines
4.4 KiB
TypeScript
import { MAIN_HEADER_TABS } from '@/constants';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import type { Undoable } from '@/models/history';
|
|
import { BulkCommand, Command } from '@/models/history';
|
|
import { useHistoryStore } from '@/stores/history.store';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
|
|
import { onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
|
|
import { useDebounceHelper } from './useDebounce';
|
|
import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport';
|
|
import { getNodeViewTab } from '@/utils/canvasUtils';
|
|
import type { Route } from 'vue-router';
|
|
|
|
const UNDO_REDO_DEBOUNCE_INTERVAL = 100;
|
|
const ELEMENT_UI_OVERLAY_SELECTOR = '.el-overlay';
|
|
|
|
export function useHistoryHelper(activeRoute: Route) {
|
|
const instance = getCurrentInstance();
|
|
const telemetry = instance?.proxy.$telemetry;
|
|
|
|
const ndvStore = useNDVStore();
|
|
const historyStore = useHistoryStore();
|
|
const uiStore = useUIStore();
|
|
|
|
const { callDebounced } = useDebounceHelper();
|
|
const { isCtrlKeyPressed } = useDeviceSupport();
|
|
|
|
const undo = async () =>
|
|
callDebounced(
|
|
async () => {
|
|
const command = historyStore.popUndoableToUndo();
|
|
if (!command) {
|
|
return;
|
|
}
|
|
if (command instanceof BulkCommand) {
|
|
historyStore.bulkInProgress = true;
|
|
const commands = command.commands;
|
|
const reverseCommands: Command[] = [];
|
|
for (let i = commands.length - 1; i >= 0; i--) {
|
|
await commands[i].revert();
|
|
reverseCommands.push(commands[i].getReverseCommand());
|
|
}
|
|
historyStore.pushUndoableToRedo(new BulkCommand(reverseCommands));
|
|
await nextTick();
|
|
historyStore.bulkInProgress = false;
|
|
}
|
|
if (command instanceof Command) {
|
|
await command.revert();
|
|
historyStore.pushUndoableToRedo(command.getReverseCommand());
|
|
uiStore.stateIsDirty = true;
|
|
}
|
|
trackCommand(command, 'undo');
|
|
},
|
|
{ debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL },
|
|
);
|
|
|
|
const redo = async () =>
|
|
callDebounced(
|
|
async () => {
|
|
const command = historyStore.popUndoableToRedo();
|
|
if (!command) {
|
|
return;
|
|
}
|
|
if (command instanceof BulkCommand) {
|
|
historyStore.bulkInProgress = true;
|
|
const commands = command.commands;
|
|
const reverseCommands = [];
|
|
for (let i = commands.length - 1; i >= 0; i--) {
|
|
await commands[i].revert();
|
|
reverseCommands.push(commands[i].getReverseCommand());
|
|
}
|
|
historyStore.pushBulkCommandToUndo(new BulkCommand(reverseCommands), false);
|
|
await nextTick();
|
|
historyStore.bulkInProgress = false;
|
|
}
|
|
if (command instanceof Command) {
|
|
await command.revert();
|
|
historyStore.pushCommandToUndo(command.getReverseCommand(), false);
|
|
uiStore.stateIsDirty = true;
|
|
}
|
|
trackCommand(command, 'redo');
|
|
},
|
|
{ debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL },
|
|
);
|
|
|
|
function trackCommand(command: Undoable, type: 'undo' | 'redo'): void {
|
|
if (command instanceof Command) {
|
|
telemetry?.track(`User hit ${type}`, { commands_length: 1, commands: [command.name] });
|
|
} else if (command instanceof BulkCommand) {
|
|
telemetry?.track(`User hit ${type}`, {
|
|
commands_length: command.commands.length,
|
|
commands: command.commands.map((c) => c.name),
|
|
});
|
|
}
|
|
}
|
|
|
|
function trackUndoAttempt() {
|
|
const activeNode = ndvStore.activeNode;
|
|
if (activeNode) {
|
|
telemetry?.track('User hit undo in NDV', { node_type: activeNode.type });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if there is a Element UI dialog open by querying
|
|
* for the visible overlay element.
|
|
*/
|
|
function isMessageDialogOpen(): boolean {
|
|
return (
|
|
document.querySelector(`${ELEMENT_UI_OVERLAY_SELECTOR}:not([style*="display: none"])`) !==
|
|
null
|
|
);
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
const currentNodeViewTab = getNodeViewTab(activeRoute);
|
|
const isNDVOpen = ndvStore.isNDVOpen;
|
|
const isAnyModalOpen = uiStore.isAnyModalOpen || isMessageDialogOpen();
|
|
const undoKeysPressed = isCtrlKeyPressed(event) && event.key.toLowerCase() === 'z';
|
|
|
|
if (event.repeat || currentNodeViewTab !== MAIN_HEADER_TABS.WORKFLOW) return;
|
|
if (isNDVOpen || isAnyModalOpen) {
|
|
if (isNDVOpen && undoKeysPressed && !event.shiftKey) {
|
|
trackUndoAttempt();
|
|
}
|
|
return;
|
|
}
|
|
if (undoKeysPressed) {
|
|
event.preventDefault();
|
|
if (event.shiftKey) {
|
|
void redo();
|
|
} else {
|
|
void undo();
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
});
|
|
|
|
return {
|
|
undo,
|
|
redo,
|
|
};
|
|
}
|