fix(editor): Fix keyboard shortcuts no longer working after editing sticky note (#13502)

This commit is contained in:
Alex Grozav 2025-02-26 12:42:49 +02:00 committed by GitHub
parent 819fc2da63
commit ab41fc3fb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 122 additions and 23 deletions

View file

@ -44,7 +44,8 @@ const emit = defineEmits<{
'update:modelValue': [elements: CanvasNode[]]; 'update:modelValue': [elements: CanvasNode[]];
'update:node:position': [id: string, position: XYPosition]; 'update:node:position': [id: string, position: XYPosition];
'update:nodes:position': [events: CanvasNodeMoveEvent[]]; 'update:nodes:position': [events: CanvasNodeMoveEvent[]];
'update:node:active': [id: string]; 'update:node:activated': [id: string];
'update:node:deactivated': [id: string];
'update:node:enabled': [id: string]; 'update:node:enabled': [id: string];
'update:node:selected': [id?: string]; 'update:node:selected': [id?: string];
'update:node:name': [id: string]; 'update:node:name': [id: string];
@ -246,7 +247,7 @@ function selectUpstreamNodes(id: string) {
const keyMap = computed(() => ({ const keyMap = computed(() => ({
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)), ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
enter: emitWithLastSelectedNode((id) => onSetNodeActive(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
'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(), 'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(),
@ -337,9 +338,13 @@ function onSelectionDragStop(event: NodeDragEvent) {
onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position }))); onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })));
} }
function onSetNodeActive(id: string) { function onSetNodeActivated(id: string) {
props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:active' }); props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:activated' });
emit('update:node:active', id); emit('update:node:activated', id);
}
function onSetNodeDeactivated(id: string) {
emit('update:node:deactivated', id);
} }
function clearSelectedNodes() { function clearSelectedNodes() {
@ -607,7 +612,7 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
case 'toggle_activation': case 'toggle_activation':
return emit('update:nodes:enabled', nodeIds); return emit('update:nodes:enabled', nodeIds);
case 'open': case 'open':
return onSetNodeActive(nodeIds[0]); return onSetNodeActivated(nodeIds[0]);
case 'rename': case 'rename':
return emit('update:node:name', nodeIds[0]); return emit('update:node:name', nodeIds[0]);
case 'change_color': case 'change_color':
@ -772,7 +777,8 @@ provide(CanvasKey, {
@run="onRunNode" @run="onRunNode"
@select="onSelectNode" @select="onSelectNode"
@toggle="onToggleNodeEnabled" @toggle="onToggleNodeEnabled"
@activate="onSetNodeActive" @activate="onSetNodeActivated"
@deactivate="onSetNodeDeactivated"
@open:contextmenu="onOpenNodeContextMenu" @open:contextmenu="onOpenNodeContextMenu"
@update="onUpdateNodeParameters" @update="onUpdateNodeParameters"
@update:inputs="onUpdateNodeInputs" @update:inputs="onUpdateNodeInputs"

View file

@ -59,6 +59,7 @@ const emit = defineEmits<{
select: [id: string, selected: boolean]; select: [id: string, selected: boolean];
toggle: [id: string]; toggle: [id: string];
activate: [id: string]; activate: [id: string];
deactivate: [id: string];
'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click']; 'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click'];
update: [id: string, parameters: Record<string, unknown>]; update: [id: string, parameters: Record<string, unknown>];
'update:inputs': [id: string]; 'update:inputs': [id: string];
@ -258,6 +259,10 @@ function onActivate() {
emit('activate', props.id); emit('activate', props.id);
} }
function onDeactivate() {
emit('deactivate', props.id);
}
function onOpenContextMenuFromToolbar(event: MouseEvent) { function onOpenContextMenuFromToolbar(event: MouseEvent) {
emit('open:contextmenu', props.id, event, 'node-button'); emit('open:contextmenu', props.id, event, 'node-button');
} }
@ -395,7 +400,8 @@ onBeforeUnmount(() => {
/> />
<CanvasNodeRenderer <CanvasNodeRenderer
@dblclick.stop="onActivate" @activate="onActivate"
@deactivate="onDeactivate"
@move="onMove" @move="onMove"
@update="onUpdate" @update="onUpdate"
@open:contextmenu="onOpenContextMenuFromNode" @open:contextmenu="onOpenContextMenuFromNode"

View file

@ -5,6 +5,7 @@ import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
import { fireEvent } from '@testing-library/vue';
const renderComponent = createComponentRenderer(CanvasNodeDefault, { const renderComponent = createComponentRenderer(CanvasNodeDefault, {
global: { global: {
@ -333,4 +334,18 @@ describe('CanvasNodeDefault', () => {
expect(getByTestId('canvas-trigger-node')).toMatchSnapshot(); expect(getByTestId('canvas-trigger-node')).toMatchSnapshot();
}); });
}); });
it('should emit "activate" on double click', async () => {
const { getByText, emitted } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
await fireEvent.dblClick(getByText('Test Node'));
expect(emitted()).toHaveProperty('activate');
});
}); });

View file

@ -12,10 +12,12 @@ const i18n = useI18n();
const emit = defineEmits<{ const emit = defineEmits<{
'open:contextmenu': [event: MouseEvent]; 'open:contextmenu': [event: MouseEvent];
activate: [id: string];
}>(); }>();
const { initialized, viewport } = useCanvas(); const { initialized, viewport } = useCanvas();
const { const {
id,
label, label,
subtitle, subtitle,
inputs, inputs,
@ -122,10 +124,20 @@ watch(viewport, () => {
function openContextMenu(event: MouseEvent) { function openContextMenu(event: MouseEvent) {
emit('open:contextmenu', event); emit('open:contextmenu', event);
} }
function onActivate() {
emit('activate', id.value);
}
</script> </script>
<template> <template>
<div :class="classes" :style="styles" :data-test-id="dataTestId" @contextmenu="openContextMenu"> <div
:class="classes"
:style="styles"
:data-test-id="dataTestId"
@contextmenu="openContextMenu"
@dblclick.stop="onActivate"
>
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" /> <CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
<slot /> <slot />
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" /> <CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />

View file

@ -68,4 +68,48 @@ describe('CanvasNodeStickyNote', () => {
expect(getComputedStyle(stickyOptions).display).toBe('none'); expect(getComputedStyle(stickyOptions).display).toBe('none');
}); });
it('should emit "activate" on double click', async () => {
const { container, emitted } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
id: 'sticky',
}),
},
},
});
const sticky = container.querySelector('.sticky-textarea');
if (!sticky) throw new Error('Sticky not found');
await fireEvent.dblClick(sticky);
expect(emitted()).toHaveProperty('activate');
});
it('should emit "deactivate" on blur', async () => {
const { container, emitted } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
id: 'sticky',
}),
},
},
});
const sticky = container.querySelector('.sticky-textarea');
if (!sticky) throw new Error('Sticky not found');
await fireEvent.dblClick(sticky);
const stickyTextarea = container.querySelector('.sticky-textarea textarea');
if (!stickyTextarea) throw new Error('Textarea not found');
await fireEvent.blur(stickyTextarea);
expect(emitted()).toHaveProperty('deactivate');
});
}); });

View file

@ -14,7 +14,8 @@ defineOptions({
const emit = defineEmits<{ const emit = defineEmits<{
update: [parameters: Record<string, unknown>]; update: [parameters: Record<string, unknown>];
move: [position: XYPosition]; move: [position: XYPosition];
dblclick: [event: MouseEvent]; activate: [id: string];
deactivate: [id: string];
'open:contextmenu': [event: MouseEvent]; 'open:contextmenu': [event: MouseEvent];
}>(); }>();
@ -58,16 +59,20 @@ function onInputChange(value: string) {
}); });
} }
function onEdit(edit: boolean) { function onSetActive(value: boolean) {
isActive.value = edit; if (isActive.value === value) return;
}
function onDoubleClick(event: MouseEvent) { isActive.value = value;
emit('dblclick', event);
if (value) {
emit('activate', id.value);
} else {
emit('deactivate', id.value);
}
} }
function onActivate() { function onActivate() {
onEdit(true); onSetActive(true);
} }
/** /**
@ -83,11 +88,11 @@ function openContextMenu(event: MouseEvent) {
*/ */
onMounted(() => { onMounted(() => {
eventBus.value?.on('update:node:active', onActivate); eventBus.value?.on('update:node:activated', onActivate);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
eventBus.value?.off('update:node:active', onActivate); eventBus.value?.off('update:node:activated', onActivate);
}); });
</script> </script>
<template> <template>
@ -110,8 +115,8 @@ onBeforeUnmount(() => {
:background-color="renderOptions.color" :background-color="renderOptions.color"
:edit-mode="isActive" :edit-mode="isActive"
:read-only="isReadOnly" :read-only="isReadOnly"
@edit="onEdit" @edit="onSetActive"
@dblclick="onDoubleClick" @dblclick.stop="onActivate"
@update:model-value="onInputChange" @update:model-value="onInputChange"
@contextmenu="openContextMenu" @contextmenu="openContextMenu"
/> />

View file

@ -395,6 +395,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
ndvStore.activeNodeName = name; ndvStore.activeNodeName = name;
} }
function clearNodeActive() {
ndvStore.activeNodeName = null;
}
function setNodeParameters(id: string, parameters: Record<string, unknown>) { function setNodeParameters(id: string, parameters: Record<string, unknown>) {
const node = workflowsStore.getNodeById(id); const node = workflowsStore.getNodeById(id);
if (!node) { if (!node) {
@ -1988,6 +1992,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
revertUpdateNodePosition, revertUpdateNodePosition,
setNodeActive, setNodeActive,
setNodeActiveByName, setNodeActiveByName,
clearNodeActive,
setNodeSelected, setNodeSelected,
toggleNodesDisabled, toggleNodesDisabled,
revertToggleNodeDisabled, revertToggleNodeDisabled,

View file

@ -143,7 +143,7 @@ export interface CanvasInjectionData {
export type CanvasNodeEventBusEvents = { export type CanvasNodeEventBusEvents = {
'update:sticky:color': never; 'update:sticky:color': never;
'update:node:active': never; 'update:node:activated': never;
'update:node:class': { className: string; add?: boolean }; 'update:node:class': { className: string; add?: boolean };
}; };

View file

@ -199,6 +199,7 @@ const {
revalidateNodeInputConnections, revalidateNodeInputConnections,
revalidateNodeOutputConnections, revalidateNodeOutputConnections,
setNodeActiveByName, setNodeActiveByName,
clearNodeActive,
addConnections, addConnections,
importWorkflowData, importWorkflowData,
fetchWorkflowDataFromUrl, fetchWorkflowDataFromUrl,
@ -621,10 +622,14 @@ function onClickNode() {
closeNodeCreator(); closeNodeCreator();
} }
function onSetNodeActive(id: string) { function onSetNodeActivated(id: string) {
setNodeActive(id); setNodeActive(id);
} }
function onSetNodeDeactivated() {
clearNodeActive();
}
function onSetNodeSelected(id?: string) { function onSetNodeSelected(id?: string) {
closeNodeCreator(); closeNodeCreator();
setNodeSelected(id); setNodeSelected(id);
@ -1728,7 +1733,8 @@ onBeforeUnmount(() => {
:key-bindings="keyBindingsEnabled" :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:activated="onSetNodeActivated"
@update:node:deactivated="onSetNodeDeactivated"
@update:node:selected="onSetNodeSelected" @update:node:selected="onSetNodeSelected"
@update:node:enabled="onToggleNodeDisabled" @update:node:enabled="onToggleNodeDisabled"
@update:node:name="onOpenRenameNodeModal" @update:node:name="onOpenRenameNodeModal"