mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Make auto-focus work for Focus Panel (no-changelog) (#17294)
This commit is contained in:
@@ -71,7 +71,7 @@ const dragAndDropEnabled = computed(() => {
|
||||
return !props.isReadOnly;
|
||||
});
|
||||
|
||||
const { highlightLine, readEditorValue, editor } = useCodeEditor({
|
||||
const { highlightLine, readEditorValue, editor, focus } = useCodeEditor({
|
||||
id: props.id,
|
||||
editorRef: codeNodeEditorRef,
|
||||
language: () => props.language,
|
||||
@@ -208,6 +208,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
||||
|
||||
await dropInCodeEditor(toRaw(editor.value), event, valueToInsert);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -69,7 +69,11 @@ const extensions = computed(() => [
|
||||
mappingDropCursor(),
|
||||
]);
|
||||
|
||||
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
||||
const {
|
||||
editor: editorRef,
|
||||
readEditorValue,
|
||||
focus,
|
||||
} = useExpressionEditor({
|
||||
editorRef: cssEditor,
|
||||
editorValue,
|
||||
extensions,
|
||||
@@ -83,6 +87,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
||||
|
||||
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import {
|
||||
formatAsExpression,
|
||||
@@ -14,6 +14,7 @@ import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||
import { useResolvedExpression } from '@/composables/useResolvedExpression';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
import {
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
type CodeExecutionMode,
|
||||
@@ -37,6 +38,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
focus: [];
|
||||
saveKeyboardShortcut: [event: KeyboardEvent];
|
||||
}>();
|
||||
|
||||
// ESLint: false positive
|
||||
@@ -49,6 +51,7 @@ const focusPanelStore = useFocusPanelStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
const deviceSupport = useDeviceSupport();
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
||||
@@ -206,11 +209,13 @@ function optionSelected(command: string) {
|
||||
if (!resolvedParameter.value) return;
|
||||
|
||||
switch (command) {
|
||||
case 'resetValue':
|
||||
return (
|
||||
typeof resolvedParameter.value.parameter.default === 'string' &&
|
||||
valueChanged(resolvedParameter.value.parameter.default)
|
||||
);
|
||||
case 'resetValue': {
|
||||
if (typeof resolvedParameter.value.parameter.default === 'string') {
|
||||
valueChanged(resolvedParameter.value.parameter.default);
|
||||
}
|
||||
void setFocus();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'addExpression': {
|
||||
const newValue = formatAsExpression(
|
||||
@@ -241,12 +246,53 @@ function optionSelected(command: string) {
|
||||
|
||||
case 'formatHtml':
|
||||
htmlEditorEventBus.emit('format-html');
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 });
|
||||
|
||||
// Wait for editor to mount before focusing
|
||||
function focusWithDelay() {
|
||||
setTimeout(() => {
|
||||
void setFocus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 's' && deviceSupport.isCtrlKeyPressed(event)) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (isReadOnly.value) return;
|
||||
|
||||
emit('saveKeyboardShortcut', event);
|
||||
}
|
||||
}
|
||||
|
||||
const registerKeyboardListener = () => {
|
||||
document.addEventListener('keydown', handleKeydown, true);
|
||||
};
|
||||
|
||||
const unregisterKeyboardListener = () => {
|
||||
document.removeEventListener('keydown', handleKeydown, true);
|
||||
};
|
||||
|
||||
watch([() => focusPanelStore.lastFocusTimestamp, () => expressionModeEnabled.value], () =>
|
||||
focusWithDelay(),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => focusPanelStore.focusPanelActive,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
registerKeyboardListener();
|
||||
} else {
|
||||
unregisterKeyboardListener();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onResize(event: ResizeData) {
|
||||
focusPanelStore.updateWidth(event.width);
|
||||
}
|
||||
@@ -328,6 +374,8 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
<CodeNodeEditor
|
||||
v-if="editorType === 'codeNodeEditor'"
|
||||
:id="resolvedParameter.parameterPath"
|
||||
ref="inputField"
|
||||
:class="$style.heightFull"
|
||||
:mode="codeEditorMode"
|
||||
:model-value="resolvedParameter.value"
|
||||
:default-value="resolvedParameter.parameter.default"
|
||||
@@ -339,6 +387,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
@update:model-value="valueChangedDebounced" />
|
||||
<HtmlEditor
|
||||
v-else-if="editorType === 'htmlEditor'"
|
||||
ref="inputField"
|
||||
:model-value="resolvedParameter.value"
|
||||
:is-read-only="isReadOnly"
|
||||
:rows="editorRows"
|
||||
@@ -348,6 +397,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
@update:model-value="valueChangedDebounced" />
|
||||
<CssEditor
|
||||
v-else-if="editorType === 'cssEditor'"
|
||||
ref="inputField"
|
||||
:model-value="resolvedParameter.value"
|
||||
:is-read-only="isReadOnly"
|
||||
:rows="editorRows"
|
||||
@@ -355,6 +405,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
@update:model-value="valueChangedDebounced" />
|
||||
<SqlEditor
|
||||
v-else-if="editorType === 'sqlEditor'"
|
||||
ref="inputField"
|
||||
:model-value="resolvedParameter.value"
|
||||
:dialect="getTypeOption('sqlDialect')"
|
||||
:is-read-only="isReadOnly"
|
||||
@@ -363,6 +414,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
@update:model-value="valueChangedDebounced" />
|
||||
<JsEditor
|
||||
v-else-if="editorType === 'jsEditor'"
|
||||
ref="inputField"
|
||||
:model-value="resolvedParameter.value"
|
||||
:is-read-only="isReadOnly"
|
||||
:rows="editorRows"
|
||||
@@ -371,6 +423,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
@update:model-value="valueChangedDebounced" />
|
||||
<JsonEditor
|
||||
v-else-if="resolvedParameter.parameter.type === 'json'"
|
||||
ref="inputField"
|
||||
:model-value="resolvedParameter.value"
|
||||
:is-read-only="isReadOnly"
|
||||
:rows="editorRows"
|
||||
@@ -496,4 +549,8 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heightFull {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,11 @@ const extensions = computed(() => [
|
||||
highlightActiveLine(),
|
||||
mappingDropCursor(),
|
||||
]);
|
||||
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
||||
const {
|
||||
editor: editorRef,
|
||||
readEditorValue,
|
||||
focus,
|
||||
} = useExpressionEditor({
|
||||
editorRef: htmlEditor,
|
||||
editorValue: () => props.modelValue,
|
||||
extensions,
|
||||
@@ -235,6 +239,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
||||
|
||||
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -107,6 +107,17 @@ const extensions = computed(() => {
|
||||
}
|
||||
return extensionsToApply;
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
const view = editor.value;
|
||||
if (view && typeof view.focus === 'function') {
|
||||
view.focus();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -106,6 +106,17 @@ function createEditor() {
|
||||
function destroyEditor() {
|
||||
editor.value?.destroy();
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
const view = editor.value;
|
||||
if (view && typeof view.focus === 'function') {
|
||||
view.focus();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -117,6 +117,7 @@ const {
|
||||
segments: { all: segments },
|
||||
readEditorValue,
|
||||
hasFocus: editorHasFocus,
|
||||
focus,
|
||||
} = useExpressionEditor({
|
||||
editorRef: sqlEditor,
|
||||
editorValue: () => props.modelValue,
|
||||
@@ -128,8 +129,8 @@ const {
|
||||
},
|
||||
});
|
||||
|
||||
watch(editorHasFocus, (focus) => {
|
||||
if (focus) {
|
||||
watch(editorHasFocus, (hasFocus) => {
|
||||
if (hasFocus) {
|
||||
isFocused.value = true;
|
||||
}
|
||||
});
|
||||
@@ -191,6 +192,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
||||
|
||||
await dropInExpressionEditor(toRaw(editor.value), event, value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useWorkflowsStore } from './workflows.store';
|
||||
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { watchOnce } from '@vueuse/core';
|
||||
|
||||
const DEFAULT_PANEL_WIDTH = 528;
|
||||
|
||||
@@ -55,6 +56,8 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
||||
focusPanelData.value[workflowsStore.workflowId] ?? DEFAULT_FOCUS_PANEL_DATA,
|
||||
);
|
||||
|
||||
const lastFocusTimestamp = ref(0);
|
||||
|
||||
const focusPanelActive = computed(() => currentFocusPanelData.value.isActive);
|
||||
const focusPanelWidth = computed(() => currentFocusPanelData.value.width ?? DEFAULT_PANEL_WIDTH);
|
||||
const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters);
|
||||
@@ -101,6 +104,10 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
||||
width,
|
||||
},
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
lastFocusTimestamp.value = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// When a new workflow is saved, we should update the focus panel data with the new workflow ID
|
||||
@@ -144,9 +151,20 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
||||
return 'value' in p && 'node' in p;
|
||||
}
|
||||
|
||||
// Ensure lastFocusTimestamp is set on initial load if panel is already active (e.g. after reload)
|
||||
watchOnce(
|
||||
() => currentFocusPanelData.value,
|
||||
(value) => {
|
||||
if (value.isActive && value.parameters.length > 0) {
|
||||
lastFocusTimestamp.value = Date.now();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
focusPanelActive,
|
||||
focusedNodeParameters,
|
||||
lastFocusTimestamp,
|
||||
openWithFocusedNodeParameter,
|
||||
isRichParameter,
|
||||
closeFocusPanel,
|
||||
|
||||
@@ -2157,7 +2157,11 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</Suspense>
|
||||
</WorkflowCanvas>
|
||||
<FocusPanel v-if="isFocusPanelFeatureEnabled" :is-canvas-read-only="isCanvasReadOnly" />
|
||||
<FocusPanel
|
||||
v-if="isFocusPanelFeatureEnabled"
|
||||
:is-canvas-read-only="isCanvasReadOnly"
|
||||
@save-keyboard-shortcut="onSaveWorkflow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user