mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +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 type { ContextMenuAction, ContextMenuTarget } 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 { CanvasKey } from '@/constants';
|
||||
import type { NodeCreatorOpenSource } from '@/Interface';
|
||||
@@ -54,6 +54,7 @@ import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||
import Edge from './elements/edges/CanvasEdge.vue';
|
||||
import Node from './elements/nodes/CanvasNode.vue';
|
||||
import { useViewportAutoAdjust } from '@/components/canvas/composables/useViewportAutoAdjust';
|
||||
import { isOutsideSelected } from '@/utils/htmlUtils';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
@@ -278,9 +279,12 @@ function selectUpstreamNodes(id: string) {
|
||||
}
|
||||
|
||||
const keyMap = computed(() => {
|
||||
const readOnlyKeymap = {
|
||||
const readOnlyKeymap: KeyMap = {
|
||||
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)),
|
||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||
// Support both key and code for zooming in and out
|
||||
@@ -301,7 +305,7 @@ const keyMap = computed(() => {
|
||||
|
||||
if (props.readOnly) return readOnlyKeymap;
|
||||
|
||||
const fullKeymap = {
|
||||
const fullKeymap: KeyMap = {
|
||||
...readOnlyKeymap,
|
||||
ctrl_x: emitWithSelectedNodes((ids) => emit('cut: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 { 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
|
||||
@@ -134,11 +138,13 @@ export const useKeybindings = (
|
||||
// - Dvorak works correctly
|
||||
// - Non-ansi layouts work correctly
|
||||
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.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 { useLogsStore } from '@/stores/logs.store';
|
||||
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 });
|
||||
|
||||
@@ -77,6 +79,25 @@ const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$p
|
||||
onToggleOpen,
|
||||
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() {
|
||||
if (isOverviewPanelFullWidth.value) {
|
||||
@@ -86,10 +107,10 @@ function handleResizeOverviewPanelEnd() {
|
||||
onOverviewPanelResizeEnd();
|
||||
}
|
||||
|
||||
async function handleOpenNdv(treeNode: LogEntry) {
|
||||
function handleOpenNdv(treeNode: LogEntry) {
|
||||
ndvStore.setActiveNodeName(treeNode.node.name);
|
||||
|
||||
await nextTick(() => {
|
||||
void nextTick(() => {
|
||||
const source = treeNode.runData?.source[0];
|
||||
const inputBranch = source?.previousNodeOutput ?? 0;
|
||||
|
||||
@@ -112,18 +133,7 @@ async function handleOpenNdv(treeNode: LogEntry) {
|
||||
@resize="onResize"
|
||||
@resizeend="onResizeEnd"
|
||||
>
|
||||
<div
|
||||
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)"
|
||||
>
|
||||
<div ref="container" :class="$style.container" tabindex="-1">
|
||||
<N8nResizeWrapper
|
||||
v-if="hasChat && (!props.isReadOnly || messages.length > 0)"
|
||||
: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
|
||||
// Has to wait for longer than SlideTransition duration
|
||||
setTimeout(() => {
|
||||
if (!window) {
|
||||
return; // for unit testing
|
||||
}
|
||||
|
||||
uiStore.appGridDimensions = {
|
||||
...uiStore.appGridDimensions,
|
||||
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
|
||||
*/
|
||||
|
||||
export function sanitizeHtml(dirtyHtml: string) {
|
||||
const sanitizedHtml = xss(dirtyHtml, {
|
||||
onTagAttr: (tag, name, value) => {
|
||||
@@ -48,21 +47,6 @@ export const sanitizeIfString = <T>(message: T): string | T => {
|
||||
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 => {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
};
|
||||
@@ -74,3 +58,18 @@ export const getBannerRowHeight = async (): Promise<number> => {
|
||||
}, 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