mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Fix keyboard shortcut bugs in the log view (#16393)
This commit is contained in:
@@ -6,7 +6,7 @@ import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover';
|
|||||||
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
||||||
import type { ContextMenuAction, ContextMenuTarget } from '@/composables/useContextMenu';
|
import type { ContextMenuAction, ContextMenuTarget } from '@/composables/useContextMenu';
|
||||||
import { useContextMenu } from '@/composables/useContextMenu';
|
import { useContextMenu } from '@/composables/useContextMenu';
|
||||||
import { useKeybindings } from '@/composables/useKeybindings';
|
import { type KeyMap, useKeybindings } from '@/composables/useKeybindings';
|
||||||
import type { PinDataSource } from '@/composables/usePinnedData';
|
import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
import { CanvasKey } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
import type { NodeCreatorOpenSource } from '@/Interface';
|
import type { NodeCreatorOpenSource } from '@/Interface';
|
||||||
@@ -54,6 +54,7 @@ import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
|||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import { useViewportAutoAdjust } from '@/components/canvas/composables/useViewportAutoAdjust';
|
import { useViewportAutoAdjust } from '@/components/canvas/composables/useViewportAutoAdjust';
|
||||||
|
import { isOutsideSelected } from '@/utils/htmlUtils';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
@@ -278,9 +279,12 @@ function selectUpstreamNodes(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyMap = computed(() => {
|
const keyMap = computed(() => {
|
||||||
const readOnlyKeymap = {
|
const readOnlyKeymap: KeyMap = {
|
||||||
ctrl_shift_o: emitWithLastSelectedNode((id) => emit('open:sub-workflow', id)),
|
ctrl_shift_o: emitWithLastSelectedNode((id) => emit('open:sub-workflow', id)),
|
||||||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
ctrl_c: {
|
||||||
|
disabled: () => isOutsideSelected(viewportRef.value),
|
||||||
|
run: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||||
|
},
|
||||||
enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)),
|
enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)),
|
||||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||||
// Support both key and code for zooming in and out
|
// Support both key and code for zooming in and out
|
||||||
@@ -301,7 +305,7 @@ const keyMap = computed(() => {
|
|||||||
|
|
||||||
if (props.readOnly) return readOnlyKeymap;
|
if (props.readOnly) return readOnlyKeymap;
|
||||||
|
|
||||||
const fullKeymap = {
|
const fullKeymap: KeyMap = {
|
||||||
...readOnlyKeymap,
|
...readOnlyKeymap,
|
||||||
ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)),
|
ctrl_x: emitWithSelectedNodes((ids) => emit('cut:nodes', ids)),
|
||||||
'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
|
'delete|backspace': emitWithSelectedNodes((ids) => emit('delete:nodes', ids)),
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
|||||||
import type { MaybeRef, Ref } from 'vue';
|
import type { MaybeRef, Ref } from 'vue';
|
||||||
import { computed, unref } from 'vue';
|
import { computed, unref } from 'vue';
|
||||||
|
|
||||||
type KeyMap = Record<string, (event: KeyboardEvent) => void>;
|
type KeyboardEventHandler =
|
||||||
|
| ((event: KeyboardEvent) => void)
|
||||||
|
| { disabled: () => boolean; run: (event: KeyboardEvent) => void };
|
||||||
|
|
||||||
|
export type KeyMap = Partial<Record<string, KeyboardEventHandler>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds a `keydown` event to `document` and calls the approriate
|
* Binds a `keydown` event to `document` and calls the approriate
|
||||||
@@ -134,11 +138,13 @@ export const useKeybindings = (
|
|||||||
// - Dvorak works correctly
|
// - Dvorak works correctly
|
||||||
// - Non-ansi layouts work correctly
|
// - Non-ansi layouts work correctly
|
||||||
const handler = normalizedKeymap.value[byKey] ?? normalizedKeymap.value[byCode];
|
const handler = normalizedKeymap.value[byKey] ?? normalizedKeymap.value[byCode];
|
||||||
|
const run =
|
||||||
|
typeof handler === 'function' ? handler : handler?.disabled() ? undefined : handler?.run;
|
||||||
|
|
||||||
if (handler) {
|
if (run) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handler(event);
|
run(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { useLogsTreeExpand } from '@/features/logs/composables/useLogsTreeExpand
|
|||||||
import { type LogEntry } from '@/features/logs/logs.types';
|
import { type LogEntry } from '@/features/logs/logs.types';
|
||||||
import { useLogsStore } from '@/stores/logs.store';
|
import { useLogsStore } from '@/stores/logs.store';
|
||||||
import { useLogsPanelLayout } from '@/features/logs/composables/useLogsPanelLayout';
|
import { useLogsPanelLayout } from '@/features/logs/composables/useLogsPanelLayout';
|
||||||
|
import { type KeyMap, useKeybindings } from '@/composables/useKeybindings';
|
||||||
|
import { useActiveElement } from '@vueuse/core';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false });
|
const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false });
|
||||||
|
|
||||||
@@ -77,6 +79,25 @@ const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$p
|
|||||||
onToggleOpen,
|
onToggleOpen,
|
||||||
onToggleSyncSelection: logsStore.toggleLogSelectionSync,
|
onToggleSyncSelection: logsStore.toggleLogSelectionSync,
|
||||||
}));
|
}));
|
||||||
|
const activeElement = useActiveElement();
|
||||||
|
const isBlurred = computed(
|
||||||
|
() =>
|
||||||
|
!activeElement.value ||
|
||||||
|
!container.value ||
|
||||||
|
(!container.value.contains(activeElement.value) && container.value !== activeElement.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const localKeyMap = computed<KeyMap>(() => ({
|
||||||
|
j: selectNext,
|
||||||
|
k: selectPrev,
|
||||||
|
Escape: () => select(undefined),
|
||||||
|
ArrowDown: selectNext,
|
||||||
|
ArrowUp: selectPrev,
|
||||||
|
Space: () => selected.value && toggleExpanded(selected.value),
|
||||||
|
Enter: () => selected.value && handleOpenNdv(selected.value),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useKeybindings(localKeyMap, { disabled: isBlurred });
|
||||||
|
|
||||||
function handleResizeOverviewPanelEnd() {
|
function handleResizeOverviewPanelEnd() {
|
||||||
if (isOverviewPanelFullWidth.value) {
|
if (isOverviewPanelFullWidth.value) {
|
||||||
@@ -86,10 +107,10 @@ function handleResizeOverviewPanelEnd() {
|
|||||||
onOverviewPanelResizeEnd();
|
onOverviewPanelResizeEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOpenNdv(treeNode: LogEntry) {
|
function handleOpenNdv(treeNode: LogEntry) {
|
||||||
ndvStore.setActiveNodeName(treeNode.node.name);
|
ndvStore.setActiveNodeName(treeNode.node.name);
|
||||||
|
|
||||||
await nextTick(() => {
|
void nextTick(() => {
|
||||||
const source = treeNode.runData?.source[0];
|
const source = treeNode.runData?.source[0];
|
||||||
const inputBranch = source?.previousNodeOutput ?? 0;
|
const inputBranch = source?.previousNodeOutput ?? 0;
|
||||||
|
|
||||||
@@ -112,18 +133,7 @@ async function handleOpenNdv(treeNode: LogEntry) {
|
|||||||
@resize="onResize"
|
@resize="onResize"
|
||||||
@resizeend="onResizeEnd"
|
@resizeend="onResizeEnd"
|
||||||
>
|
>
|
||||||
<div
|
<div ref="container" :class="$style.container" tabindex="-1">
|
||||||
ref="container"
|
|
||||||
:class="$style.container"
|
|
||||||
tabindex="-1"
|
|
||||||
@keydown.esc.exact.stop="select(undefined)"
|
|
||||||
@keydown.j.exact.stop="selectNext"
|
|
||||||
@keydown.down.exact.stop.prevent="selectNext"
|
|
||||||
@keydown.k.exact.stop="selectPrev"
|
|
||||||
@keydown.up.exact.stop.prevent="selectPrev"
|
|
||||||
@keydown.space.exact.stop="selected && toggleExpanded(selected)"
|
|
||||||
@keydown.enter.exact.stop="selected && handleOpenNdv(selected)"
|
|
||||||
>
|
|
||||||
<N8nResizeWrapper
|
<N8nResizeWrapper
|
||||||
v-if="hasChat && (!props.isReadOnly || messages.length > 0)"
|
v-if="hasChat && (!props.isReadOnly || messages.length > 0)"
|
||||||
:supported-directions="['right']"
|
:supported-directions="['right']"
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||||||
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
||||||
// Has to wait for longer than SlideTransition duration
|
// Has to wait for longer than SlideTransition duration
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (!window) {
|
||||||
|
return; // for unit testing
|
||||||
|
}
|
||||||
|
|
||||||
uiStore.appGridDimensions = {
|
uiStore.appGridDimensions = {
|
||||||
...uiStore.appGridDimensions,
|
...uiStore.appGridDimensions,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ALLOWED_HTML_ATTRIBUTES, ALLOWED_HTML_TAGS } from '@/constants';
|
|||||||
/*
|
/*
|
||||||
Constants and utility functions that help in HTML, CSS and DOM manipulation
|
Constants and utility functions that help in HTML, CSS and DOM manipulation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function sanitizeHtml(dirtyHtml: string) {
|
export function sanitizeHtml(dirtyHtml: string) {
|
||||||
const sanitizedHtml = xss(dirtyHtml, {
|
const sanitizedHtml = xss(dirtyHtml, {
|
||||||
onTagAttr: (tag, name, value) => {
|
onTagAttr: (tag, name, value) => {
|
||||||
@@ -48,21 +47,6 @@ export const sanitizeIfString = <T>(message: T): string | T => {
|
|||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertRemToPixels(rem: string) {
|
|
||||||
return parseInt(rem, 10) * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isChildOf(parent: Element, child: Element): boolean {
|
|
||||||
if (child.parentElement === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (child.parentElement === parent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isChildOf(parent, child.parentElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const capitalizeFirstLetter = (text: string): string => {
|
export const capitalizeFirstLetter = (text: string): string => {
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
};
|
};
|
||||||
@@ -74,3 +58,18 @@ export const getBannerRowHeight = async (): Promise<number> => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isOutsideSelected(el: HTMLElement | null) {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
|
||||||
|
if (!selection?.anchorNode || !selection.focusNode || !el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!el.contains(selection.anchorNode) &&
|
||||||
|
!el.contains(selection.focusNode) &&
|
||||||
|
(selection.anchorNode !== selection.focusNode ||
|
||||||
|
selection.anchorOffset !== selection.focusOffset)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user