fix(editor): Prevent keyboard shortcuts when ndv is open in new canvas (no-changelog) (#10601)

This commit is contained in:
Alex Grozav 2024-08-29 15:25:49 +03:00 committed by GitHub
parent 95da4d4797
commit 78f34f66c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 133 additions and 24 deletions

View file

@ -73,6 +73,7 @@ const props = withDefaults(
controlsPosition?: PanelPosition; controlsPosition?: PanelPosition;
eventBus?: EventBus<CanvasEventBusEvents>; eventBus?: EventBus<CanvasEventBusEvents>;
readOnly?: boolean; readOnly?: boolean;
keyBindings?: boolean;
}>(), }>(),
{ {
id: 'canvas', id: 'canvas',
@ -81,6 +82,7 @@ const props = withDefaults(
controlsPosition: PanelPosition.BottomLeft, controlsPosition: PanelPosition.BottomLeft,
eventBus: () => createEventBus(), eventBus: () => createEventBus(),
readOnly: false, readOnly: false,
keyBindings: true,
}, },
); );
@ -101,27 +103,36 @@ const {
findNode, findNode,
} = useVueFlow({ id: props.id, deleteKeyCode: null }); } = useVueFlow({ id: props.id, deleteKeyCode: null });
useKeybindings({ /**
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)), * Key bindings
ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)), */
'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)), const disableKeyBindings = computed(() => !props.keyBindings);
d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')), useKeybindings(
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)), {
f2: emitWithLastSelectedNode((id) => emit('update:node:name', id)), ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
tab: () => emit('create:node', 'tab'), ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)),
shift_s: () => emit('create:sticky'), 'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
ctrl_alt_n: () => emit('create:workflow'), ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)),
ctrl_enter: () => emit('run:workflow'), d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
ctrl_s: () => emit('save:workflow'), p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')),
ctrl_a: () => addSelectedNodes(graphNodes.value), enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
'+|=': async () => await onZoomIn(), f2: emitWithLastSelectedNode((id) => emit('update:node:name', id)),
'-|_': async () => await onZoomOut(), tab: () => emit('create:node', 'tab'),
0: async () => await onResetZoom(), shift_s: () => emit('create:sticky'),
1: async () => await onFitView(), ctrl_alt_n: () => emit('create:workflow'),
// @TODO implement arrow key shortcuts to modify selection ctrl_enter: () => emit('run:workflow'),
}); ctrl_s: () => emit('save:workflow'),
ctrl_a: () => addSelectedNodes(graphNodes.value),
'+|=': async () => await onZoomIn(),
'-|_': async () => await onZoomOut(),
0: async () => await onResetZoom(),
1: async () => await onFitView(),
// @TODO implement arrow key shortcuts to modify selection
},
{ disabled: disableKeyBindings },
);
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();

View file

@ -0,0 +1,85 @@
import { useKeybindings } from '@/composables/useKeybindings';
import { ref } from 'vue';
describe('useKeybindings', () => {
it('should call the correct handler for a single key press', async () => {
const handler = vi.fn();
const keymap = ref({ a: handler });
useKeybindings(keymap);
const event = new KeyboardEvent('keydown', { key: 'a' });
document.dispatchEvent(event);
expect(handler).toHaveBeenCalled();
});
it('should call the correct handler for a combination key press', async () => {
const handler = vi.fn();
const keymap = ref({ 'ctrl+a': handler });
useKeybindings(keymap);
const event = new KeyboardEvent('keydown', { key: 'a', ctrlKey: true });
document.dispatchEvent(event);
expect(handler).toHaveBeenCalled();
});
it('should not call handler if key press is ignored', async () => {
const handler = vi.fn();
const keymap = ref({ a: handler });
useKeybindings(keymap);
const input = document.createElement('input');
document.body.appendChild(input);
input.focus();
const event = new KeyboardEvent('keydown', { key: 'a' });
document.dispatchEvent(event);
expect(handler).not.toHaveBeenCalled();
document.body.removeChild(input);
});
it('should not call handler if disabled', async () => {
const handler = vi.fn();
const keymap = ref({ a: handler });
const disabled = ref(true);
useKeybindings(keymap, { disabled });
const event = new KeyboardEvent('keydown', { key: 'a' });
document.dispatchEvent(event);
expect(handler).not.toHaveBeenCalled();
});
it('should normalize shortcut strings correctly', async () => {
const handler = vi.fn();
const keymap = ref({ 'ctrl+shift+a': handler });
useKeybindings(keymap);
const event = new KeyboardEvent('keydown', { key: 'A', ctrlKey: true, shiftKey: true });
document.dispatchEvent(event);
expect(handler).toHaveBeenCalled();
});
it('should normalize shortcut string alternatives correctly', async () => {
const handler = vi.fn();
const keymap = ref({ 'a|b': handler });
useKeybindings(keymap);
const eventA = new KeyboardEvent('keydown', { key: 'A' });
document.dispatchEvent(eventA);
expect(handler).toHaveBeenCalled();
const eventB = new KeyboardEvent('keydown', { key: 'B' });
document.dispatchEvent(eventB);
expect(handler).toHaveBeenCalledTimes(2);
});
});

View file

@ -1,13 +1,21 @@
import { useActiveElement, useEventListener } from '@vueuse/core'; import { useActiveElement, useEventListener } from '@vueuse/core';
import { useDeviceSupport } from 'n8n-design-system'; import { useDeviceSupport } from 'n8n-design-system';
import { computed, toValue, type MaybeRefOrGetter } from 'vue'; import type { MaybeRef } from 'vue';
import { computed, toValue, type MaybeRefOrGetter, unref } from 'vue';
type KeyMap = Record<string, (event: KeyboardEvent) => void>; type KeyMap = Record<string, (event: KeyboardEvent) => void>;
export const useKeybindings = (keymap: MaybeRefOrGetter<KeyMap>) => { export const useKeybindings = (
keymap: MaybeRefOrGetter<KeyMap>,
options?: {
disabled: MaybeRef<boolean>;
},
) => {
const activeElement = useActiveElement(); const activeElement = useActiveElement();
const { isCtrlKeyPressed } = useDeviceSupport(); const { isCtrlKeyPressed } = useDeviceSupport();
const isDisabled = computed(() => unref(options?.disabled));
const ignoreKeyPresses = computed(() => { const ignoreKeyPresses = computed(() => {
if (!activeElement.value) return false; if (!activeElement.value) return false;
@ -60,7 +68,7 @@ export const useKeybindings = (keymap: MaybeRefOrGetter<KeyMap>) => {
} }
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
if (ignoreKeyPresses.value) return; if (ignoreKeyPresses.value || isDisabled.value) return;
const shortcutString = toShortcutString(event); const shortcutString = toShortcutString(event);

View file

@ -221,6 +221,10 @@ const fallbackNodes = computed<INodeUi[]>(() =>
], ],
); );
const keyBindingsEnabled = computed(() => {
return !ndvStore.activeNode;
});
/** /**
* Initialization * Initialization
*/ */
@ -1483,6 +1487,7 @@ onDeactivated(() => {
:fallback-nodes="fallbackNodes" :fallback-nodes="fallbackNodes"
:event-bus="canvasEventBus" :event-bus="canvasEventBus"
:read-only="isCanvasReadOnly" :read-only="isCanvasReadOnly"
:key-bindings="keyBindingsEnabled"
@update:nodes:position="onUpdateNodesPosition" @update:nodes:position="onUpdateNodesPosition"
@update:node:position="onUpdateNodePosition" @update:node:position="onUpdateNodePosition"
@update:node:active="onSetNodeActive" @update:node:active="onSetNodeActive"