diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts
index e904d891b1a53..4182c75507b21 100644
--- a/cypress/e2e/10-undo-redo.cy.ts
+++ b/cypress/e2e/10-undo-redo.cy.ts
@@ -1,12 +1,14 @@
import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants';
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
+import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv';
// Suite-specific constants
const CODE_NODE_NEW_NAME = 'Something else';
const WorkflowPage = new WorkflowPageClass();
+const messageBox = new MessageBoxClass();
const ndv = new NDV();
describe('Undo/Redo', () => {
@@ -354,4 +356,36 @@ describe('Undo/Redo', () => {
.should('have.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
});
+
+ it('should not undo/redo when NDV or a modal is open', () => {
+ WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: true });
+ // Try while NDV is open
+ WorkflowPage.actions.hitUndo();
+ WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
+ ndv.getters.backToCanvas().click();
+ // Try while modal is open
+ cy.getByTestId('menu-item').contains('About n8n').click({ force: true });
+ cy.getByTestId('about-modal').should('be.visible');
+ WorkflowPage.actions.hitUndo();
+ WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
+ cy.getByTestId('close-about-modal-button').click();
+ // Should work now
+ WorkflowPage.actions.hitUndo();
+ WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
+ });
+
+ it('should not undo/redo when NDV or a prompt is open', () => {
+ WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: false });
+ WorkflowPage.getters.workflowMenu().click();
+ WorkflowPage.getters.workflowMenuItemImportFromURLItem().should('be.visible');
+ WorkflowPage.getters.workflowMenuItemImportFromURLItem().click();
+ // Try while prompt is open
+ messageBox.getters.header().click();
+ WorkflowPage.actions.hitUndo();
+ WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
+ // Close prompt and try again
+ messageBox.actions.cancel();
+ WorkflowPage.actions.hitUndo();
+ WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
+ });
});
diff --git a/packages/editor-ui/src/components/AboutModal.vue b/packages/editor-ui/src/components/AboutModal.vue
index 2be4c4faa69dc..4893e4e62ee3b 100644
--- a/packages/editor-ui/src/components/AboutModal.vue
+++ b/packages/editor-ui/src/components/AboutModal.vue
@@ -47,7 +47,12 @@
-
+
diff --git a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
index 9176fd9d2e5d8..a035654b1f20c 100644
--- a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
+++ b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts
@@ -22,7 +22,13 @@ vi.mock('@/stores/history.store', () => {
}),
};
});
-vi.mock('@/stores/ui.store');
+vi.mock('@/stores/ui.store', () => {
+ return {
+ useUIStore: () => ({
+ isAnyModalOpen: false,
+ }),
+ };
+});
vi.mock('vue-router', () => ({
useRoute: () => ({}),
}));
diff --git a/packages/editor-ui/src/composables/useHistoryHelper.ts b/packages/editor-ui/src/composables/useHistoryHelper.ts
index a221586654339..599154f3f96f5 100644
--- a/packages/editor-ui/src/composables/useHistoryHelper.ts
+++ b/packages/editor-ui/src/composables/useHistoryHelper.ts
@@ -5,13 +5,14 @@ import { BulkCommand, Command } from '@/models/history';
import { useHistoryStore } from '@/stores/history.store';
import { useUIStore } from '@/stores/ui.store';
-import { ref, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
+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();
@@ -24,8 +25,6 @@ export function useHistoryHelper(activeRoute: Route) {
const { callDebounced } = useDebounceHelper();
const { isCtrlKeyPressed } = useDeviceSupport();
- const isNDVOpen = ref(ndvStore.activeNodeName !== null);
-
const undo = async () =>
callDebounced(
async () => {
@@ -95,29 +94,43 @@ export function useHistoryHelper(activeRoute: Route) {
}
}
- function trackUndoAttempt(event: KeyboardEvent) {
- if (isNDVOpen.value && !event.shiftKey) {
- const activeNode = ndvStore.activeNode;
- if (activeNode) {
- telemetry?.track('User hit undo in NDV', { node_type: activeNode.type });
- }
+ 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 (isCtrlKeyPressed(event) && event.key.toLowerCase() === 'z') {
+ if (isNDVOpen || isAnyModalOpen) {
+ if (isNDVOpen && undoKeysPressed && !event.shiftKey) {
+ trackUndoAttempt();
+ }
+ return;
+ }
+ if (undoKeysPressed) {
event.preventDefault();
- if (!isNDVOpen.value) {
- if (event.shiftKey) {
- void redo();
- } else {
- void undo();
- }
- } else if (!event.shiftKey) {
- trackUndoAttempt(event);
+ if (event.shiftKey) {
+ void redo();
+ } else {
+ void undo();
}
}
}
diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts
index 5ca6dca6d8b6b..062cf05ac6da7 100644
--- a/packages/editor-ui/src/stores/ndv.store.ts
+++ b/packages/editor-ui/src/stores/ndv.store.ts
@@ -139,6 +139,9 @@ export const useNDVStore = defineStore(STORES.NDV, {
return null;
},
+ isNDVOpen(): boolean {
+ return this.activeNodeName !== null;
+ },
},
actions: {
setActiveNodeName(nodeName: string | null): void {
diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts
index 6eeb2f1a5b2e2..9667f80e5c690 100644
--- a/packages/editor-ui/src/stores/ui.store.ts
+++ b/packages/editor-ui/src/stores/ui.store.ts
@@ -410,6 +410,9 @@ export const useUIStore = defineStore(STORES.UI, {
const style = getComputedStyle(document.body);
return Number(style.getPropertyValue('--header-height'));
},
+ isAnyModalOpen(): boolean {
+ return this.modalStack.length > 0;
+ },
},
actions: {
setTheme(theme: ThemeOption): void {