mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +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;
|
return !props.isReadOnly;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { highlightLine, readEditorValue, editor } = useCodeEditor({
|
const { highlightLine, readEditorValue, editor, focus } = useCodeEditor({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
editorRef: codeNodeEditorRef,
|
editorRef: codeNodeEditorRef,
|
||||||
language: () => props.language,
|
language: () => props.language,
|
||||||
@@ -208,6 +208,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
|||||||
|
|
||||||
await dropInCodeEditor(toRaw(editor.value), event, valueToInsert);
|
await dropInCodeEditor(toRaw(editor.value), event, valueToInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ const extensions = computed(() => [
|
|||||||
mappingDropCursor(),
|
mappingDropCursor(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
const {
|
||||||
|
editor: editorRef,
|
||||||
|
readEditorValue,
|
||||||
|
focus,
|
||||||
|
} = useExpressionEditor({
|
||||||
editorRef: cssEditor,
|
editorRef: cssEditor,
|
||||||
editorValue,
|
editorValue,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -83,6 +87,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
|||||||
|
|
||||||
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system';
|
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 { useI18n } from '@n8n/i18n';
|
||||||
import {
|
import {
|
||||||
formatAsExpression,
|
formatAsExpression,
|
||||||
@@ -14,6 +14,7 @@ import { isValueExpression } from '@/utils/nodeTypesUtils';
|
|||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { useResolvedExpression } from '@/composables/useResolvedExpression';
|
import { useResolvedExpression } from '@/composables/useResolvedExpression';
|
||||||
|
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||||
import {
|
import {
|
||||||
AI_TRANSFORM_NODE_TYPE,
|
AI_TRANSFORM_NODE_TYPE,
|
||||||
type CodeExecutionMode,
|
type CodeExecutionMode,
|
||||||
@@ -37,6 +38,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
focus: [];
|
focus: [];
|
||||||
|
saveKeyboardShortcut: [event: KeyboardEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// ESLint: false positive
|
// ESLint: false positive
|
||||||
@@ -49,6 +51,7 @@ const focusPanelStore = useFocusPanelStore();
|
|||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
const environmentsStore = useEnvironmentsStore();
|
const environmentsStore = useEnvironmentsStore();
|
||||||
|
const deviceSupport = useDeviceSupport();
|
||||||
const { debounce } = useDebounce();
|
const { debounce } = useDebounce();
|
||||||
|
|
||||||
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
||||||
@@ -206,11 +209,13 @@ function optionSelected(command: string) {
|
|||||||
if (!resolvedParameter.value) return;
|
if (!resolvedParameter.value) return;
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'resetValue':
|
case 'resetValue': {
|
||||||
return (
|
if (typeof resolvedParameter.value.parameter.default === 'string') {
|
||||||
typeof resolvedParameter.value.parameter.default === 'string' &&
|
valueChanged(resolvedParameter.value.parameter.default);
|
||||||
valueChanged(resolvedParameter.value.parameter.default)
|
}
|
||||||
);
|
void setFocus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'addExpression': {
|
case 'addExpression': {
|
||||||
const newValue = formatAsExpression(
|
const newValue = formatAsExpression(
|
||||||
@@ -241,12 +246,53 @@ function optionSelected(command: string) {
|
|||||||
|
|
||||||
case 'formatHtml':
|
case 'formatHtml':
|
||||||
htmlEditorEventBus.emit('format-html');
|
htmlEditorEventBus.emit('format-html');
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 });
|
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) {
|
function onResize(event: ResizeData) {
|
||||||
focusPanelStore.updateWidth(event.width);
|
focusPanelStore.updateWidth(event.width);
|
||||||
}
|
}
|
||||||
@@ -328,6 +374,8 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
<CodeNodeEditor
|
<CodeNodeEditor
|
||||||
v-if="editorType === 'codeNodeEditor'"
|
v-if="editorType === 'codeNodeEditor'"
|
||||||
:id="resolvedParameter.parameterPath"
|
:id="resolvedParameter.parameterPath"
|
||||||
|
ref="inputField"
|
||||||
|
:class="$style.heightFull"
|
||||||
:mode="codeEditorMode"
|
:mode="codeEditorMode"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:default-value="resolvedParameter.parameter.default"
|
:default-value="resolvedParameter.parameter.default"
|
||||||
@@ -339,6 +387,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
@update:model-value="valueChangedDebounced" />
|
@update:model-value="valueChangedDebounced" />
|
||||||
<HtmlEditor
|
<HtmlEditor
|
||||||
v-else-if="editorType === 'htmlEditor'"
|
v-else-if="editorType === 'htmlEditor'"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
@@ -348,6 +397,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
@update:model-value="valueChangedDebounced" />
|
@update:model-value="valueChangedDebounced" />
|
||||||
<CssEditor
|
<CssEditor
|
||||||
v-else-if="editorType === 'cssEditor'"
|
v-else-if="editorType === 'cssEditor'"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
@@ -355,6 +405,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
@update:model-value="valueChangedDebounced" />
|
@update:model-value="valueChangedDebounced" />
|
||||||
<SqlEditor
|
<SqlEditor
|
||||||
v-else-if="editorType === 'sqlEditor'"
|
v-else-if="editorType === 'sqlEditor'"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:dialect="getTypeOption('sqlDialect')"
|
:dialect="getTypeOption('sqlDialect')"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
@@ -363,6 +414,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
@update:model-value="valueChangedDebounced" />
|
@update:model-value="valueChangedDebounced" />
|
||||||
<JsEditor
|
<JsEditor
|
||||||
v-else-if="editorType === 'jsEditor'"
|
v-else-if="editorType === 'jsEditor'"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
@@ -371,6 +423,7 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
@update:model-value="valueChangedDebounced" />
|
@update:model-value="valueChangedDebounced" />
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
v-else-if="resolvedParameter.parameter.type === 'json'"
|
v-else-if="resolvedParameter.parameter.type === 'json'"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
@@ -496,4 +549,8 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heightFull {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ const extensions = computed(() => [
|
|||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
mappingDropCursor(),
|
mappingDropCursor(),
|
||||||
]);
|
]);
|
||||||
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
const {
|
||||||
|
editor: editorRef,
|
||||||
|
readEditorValue,
|
||||||
|
focus,
|
||||||
|
} = useExpressionEditor({
|
||||||
editorRef: htmlEditor,
|
editorRef: htmlEditor,
|
||||||
editorValue: () => props.modelValue,
|
editorValue: () => props.modelValue,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -235,6 +239,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
|||||||
|
|
||||||
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
await dropInExpressionEditor(toRaw(editorRef.value), event, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -107,6 +107,17 @@ const extensions = computed(() => {
|
|||||||
}
|
}
|
||||||
return extensionsToApply;
|
return extensionsToApply;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
const view = editor.value;
|
||||||
|
if (view && typeof view.focus === 'function') {
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -106,6 +106,17 @@ function createEditor() {
|
|||||||
function destroyEditor() {
|
function destroyEditor() {
|
||||||
editor.value?.destroy();
|
editor.value?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
const view = editor.value;
|
||||||
|
if (view && typeof view.focus === 'function') {
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const {
|
|||||||
segments: { all: segments },
|
segments: { all: segments },
|
||||||
readEditorValue,
|
readEditorValue,
|
||||||
hasFocus: editorHasFocus,
|
hasFocus: editorHasFocus,
|
||||||
|
focus,
|
||||||
} = useExpressionEditor({
|
} = useExpressionEditor({
|
||||||
editorRef: sqlEditor,
|
editorRef: sqlEditor,
|
||||||
editorValue: () => props.modelValue,
|
editorValue: () => props.modelValue,
|
||||||
@@ -128,8 +129,8 @@ const {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(editorHasFocus, (focus) => {
|
watch(editorHasFocus, (hasFocus) => {
|
||||||
if (focus) {
|
if (hasFocus) {
|
||||||
isFocused.value = true;
|
isFocused.value = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,6 +192,10 @@ async function onDrop(value: string, event: MouseEvent) {
|
|||||||
|
|
||||||
await dropInExpressionEditor(toRaw(editor.value), event, value);
|
await dropInExpressionEditor(toRaw(editor.value), event, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { useWorkflowsStore } from './workflows.store';
|
import { useWorkflowsStore } from './workflows.store';
|
||||||
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||||
import { useStorage } from '@/composables/useStorage';
|
import { useStorage } from '@/composables/useStorage';
|
||||||
|
import { watchOnce } from '@vueuse/core';
|
||||||
|
|
||||||
const DEFAULT_PANEL_WIDTH = 528;
|
const DEFAULT_PANEL_WIDTH = 528;
|
||||||
|
|
||||||
@@ -55,6 +56,8 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
|||||||
focusPanelData.value[workflowsStore.workflowId] ?? DEFAULT_FOCUS_PANEL_DATA,
|
focusPanelData.value[workflowsStore.workflowId] ?? DEFAULT_FOCUS_PANEL_DATA,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastFocusTimestamp = ref(0);
|
||||||
|
|
||||||
const focusPanelActive = computed(() => currentFocusPanelData.value.isActive);
|
const focusPanelActive = computed(() => currentFocusPanelData.value.isActive);
|
||||||
const focusPanelWidth = computed(() => currentFocusPanelData.value.width ?? DEFAULT_PANEL_WIDTH);
|
const focusPanelWidth = computed(() => currentFocusPanelData.value.width ?? DEFAULT_PANEL_WIDTH);
|
||||||
const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters);
|
const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters);
|
||||||
@@ -101,6 +104,10 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
|||||||
width,
|
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
|
// 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;
|
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 {
|
return {
|
||||||
focusPanelActive,
|
focusPanelActive,
|
||||||
focusedNodeParameters,
|
focusedNodeParameters,
|
||||||
|
lastFocusTimestamp,
|
||||||
openWithFocusedNodeParameter,
|
openWithFocusedNodeParameter,
|
||||||
isRichParameter,
|
isRichParameter,
|
||||||
closeFocusPanel,
|
closeFocusPanel,
|
||||||
|
|||||||
@@ -2157,7 +2157,11 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</WorkflowCanvas>
|
</WorkflowCanvas>
|
||||||
<FocusPanel v-if="isFocusPanelFeatureEnabled" :is-canvas-read-only="isCanvasReadOnly" />
|
<FocusPanel
|
||||||
|
v-if="isFocusPanelFeatureEnabled"
|
||||||
|
:is-canvas-read-only="isCanvasReadOnly"
|
||||||
|
@save-keyboard-shortcut="onSaveWorkflow"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user