fix(editor): NDV in focus panel review batch 2 (no-changelog) (#19463)

This commit is contained in:
Suguru Inoue
2025-09-15 16:00:21 +02:00
committed by GitHub
parent 7e63e56ccd
commit 05e337be83
9 changed files with 110 additions and 61 deletions

View File

@@ -104,8 +104,9 @@ defineExpose({
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
/* Override break-all set for el-popper */ /* Override styles set for el-popper */
word-break: normal; word-break: normal;
text-align: unset;
} }
.dropdown { .dropdown {

View File

@@ -477,12 +477,13 @@ function handleChangeCollapsingColumn(columnName: string | null) {
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length" v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
:class="$style.noOutputData" :class="$style.noOutputData"
> >
<N8nText v-if="nodeNotRunMessageVariant === 'simple'" color="text-base" size="small"> <NDVEmptyState v-if="nodeNotRunMessageVariant === 'simple'">
<template #description>
<I18nT scope="global" keypath="ndv.input.noOutputData.embeddedNdv.description"> <I18nT scope="global" keypath="ndv.input.noOutputData.embeddedNdv.description">
<template #link> <template #link>
<NodeExecuteButton <NodeExecuteButton
:class="$style.executeButton" :class="$style.executeButton"
size="medium" size="large"
:node-name="nodeNameToExecute" :node-name="nodeNameToExecute"
:label="i18n.baseText('ndv.input.noOutputData.embeddedNdv.link')" :label="i18n.baseText('ndv.input.noOutputData.embeddedNdv.link')"
text text
@@ -491,7 +492,8 @@ function handleChangeCollapsingColumn(columnName: string | null) {
/> />
</template> </template>
</I18nT> </I18nT>
</N8nText> </template>
</NDVEmptyState>
<template v-else-if="isNDVV2"> <template v-else-if="isNDVV2">
<NDVEmptyState <NDVEmptyState

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ title: string }>(); defineProps<{ title?: string }>();
defineSlots<{ defineSlots<{
icon(): unknown; icon(): unknown;
@@ -10,7 +10,7 @@ defineSlots<{
<template> <template>
<article :class="$style.empty"> <article :class="$style.empty">
<slot name="icon" /> <slot name="icon" />
<h1 :class="$style.title">{{ title }}</h1> <h1 v-if="title" :class="$style.title">{{ title }}</h1>
<p :class="$style.description"><slot name="description" /></p> <p :class="$style.description"><slot name="description" /></p>
</article> </article>
</template> </template>
@@ -36,7 +36,8 @@ defineSlots<{
.description { .description {
font-size: var(--font-size-s); font-size: var(--font-size-s);
max-width: 180px; max-width: 240px;
margin: 0; margin: 0;
text-align: center;
} }
</style> </style>

View File

@@ -675,7 +675,7 @@ describe('ParameterInput.vue', () => {
inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 }, inputNode: { name: 'n1', runIndex: 0, branchIndex: 0 },
}); });
it('should render mapper', async () => { it('should render mapper when the current value is empty', async () => {
const rendered = renderComponent({ const rendered = renderComponent({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } }, global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: { props: {
@@ -688,6 +688,37 @@ describe('ParameterInput.vue', () => {
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument(); expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
}); });
it('should render mapper when editor type is specified in the parameter', async () => {
const rendered = renderComponent({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: {
displayName: 'Name',
name: 'name',
type: 'string',
typeOptions: { editor: 'sqlEditor' },
},
modelValue: 'SELECT 1;',
},
});
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
});
it('should render mapper when the current value is an expression', async () => {
const rendered = renderComponent({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: { displayName: 'Name', name: 'name', type: 'string' },
modelValue: '={{$today}}',
},
});
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
});
it('should not render mapper if given node property is a node setting', async () => { it('should not render mapper if given node property is a node setting', async () => {
const rendered = renderComponent({ const rendered = renderComponent({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } }, global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },

View File

@@ -277,7 +277,7 @@ const modelValueExpressionEdit = computed<NodeParameterValueType>(() => {
const editorRows = computed(() => getTypeOption<number>('rows')); const editorRows = computed(() => getTypeOption<number>('rows'));
const editorType = computed<EditorType | 'json' | 'code' | 'cssEditor'>(() => { const editorType = computed<EditorType | 'json' | 'code' | 'cssEditor' | undefined>(() => {
return getTypeOption<EditorType>('editor'); return getTypeOption<EditorType>('editor');
}); });
const editorIsReadOnly = computed<boolean>(() => { const editorIsReadOnly = computed<boolean>(() => {
@@ -636,7 +636,8 @@ const isMapperAvailable = computed(
!props.parameter.isNodeSetting && !props.parameter.isNodeSetting &&
(isModelValueExpression.value || (isModelValueExpression.value ||
props.forceShowExpression || props.forceShowExpression ||
(isEmpty(props.modelValue) && props.parameter.type !== 'dateTime')), (isEmpty(props.modelValue) && props.parameter.type !== 'dateTime') ||
editorType.value !== undefined),
); );
function isRemoteParameterOption(option: INodePropertyOptions) { function isRemoteParameterOption(option: INodePropertyOptions) {
@@ -825,7 +826,6 @@ async function setFocus() {
} }
isFocused.value = true; isFocused.value = true;
isMapperShown.value = isMapperAvailable.value;
} }
emit('focus'); emit('focus');
@@ -966,14 +966,23 @@ function expressionUpdated(value: string) {
valueChanged(val); valueChanged(val);
} }
function onBlur(event?: FocusEvent | KeyboardEvent) { function onBlur() {
emit('blur'); emit('blur');
isFocused.value = false; isFocused.value = false;
}
function onFocusIn() {
if (isMapperAvailable.value) {
isMapperShown.value = true;
}
}
function onFocusOutOrOutsideClickMapper(event: FocusEvent | MouseEvent) {
if ( if (
!(event?.target instanceof Node && wrapper.value?.contains(event.target)) &&
!(event?.target instanceof Node && mapperElRef.value?.contains(event.target)) && !(event?.target instanceof Node && mapperElRef.value?.contains(event.target)) &&
!( !(
event instanceof FocusEvent && 'relatedTarget' in event &&
event.relatedTarget instanceof Node && event.relatedTarget instanceof Node &&
mapperElRef.value?.contains(event.relatedTarget) mapperElRef.value?.contains(event.relatedTarget)
) )
@@ -1027,12 +1036,6 @@ function onUpdateTextInput(value: string) {
const onUpdateTextInputDebounced = debounce(onUpdateTextInput, { debounceTime: 200 }); const onUpdateTextInputDebounced = debounce(onUpdateTextInput, { debounceTime: 200 });
function onClickOutsideMapper() {
if (!isFocused.value) {
isMapperShown.value = false;
}
}
async function optionSelected(command: string) { async function optionSelected(command: string) {
const prevValue = props.modelValue; const prevValue = props.modelValue;
@@ -1247,7 +1250,7 @@ onUpdated(async () => {
} }
}); });
onClickOutside(mapperElRef, onClickOutsideMapper); onClickOutside(mapperElRef, onFocusOutOrOutsideClickMapper);
</script> </script>
<template> <template>
@@ -1290,6 +1293,8 @@ onClickOutside(mapperElRef, onClickOutsideMapper);
]" ]"
:style="parameterInputWrapperStyle" :style="parameterInputWrapperStyle"
:data-parameter-path="path" :data-parameter-path="path"
@focusin="onFocusIn"
@focusout="onFocusOutOrOutsideClickMapper"
> >
<ResourceLocator <ResourceLocator
v-if="parameter.type === 'resourceLocator'" v-if="parameter.type === 'resourceLocator'"

View File

@@ -89,6 +89,7 @@ import { usePostHog } from '@/stores/posthog.store';
import { I18nT } from 'vue-i18n'; import { I18nT } from 'vue-i18n';
import RunDataBinary from '@/components/RunDataBinary.vue'; import RunDataBinary from '@/components/RunDataBinary.vue';
import { hasTrimmedRunData } from '@/utils/executionUtils'; import { hasTrimmedRunData } from '@/utils/executionUtils';
import NDVEmptyState from '@/components/NDVEmptyState.vue';
const LazyRunDataTable = defineAsyncComponent( const LazyRunDataTable = defineAsyncComponent(
async () => await import('@/components/RunDataTable.vue'), async () => await import('@/components/RunDataTable.vue'),
@@ -1774,9 +1775,8 @@ defineExpose({ enterEditMode });
" "
:class="$style.center" :class="$style.center"
> >
<div v-if="search"> <NDVEmptyState v-if="search" :title="i18n.baseText('ndv.search.noMatch.title')">
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noMatch.title') }}</N8nText> <template #description>
<N8nText>
<I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global"> <I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global">
<template #link> <template #link>
<a href="#" @click="onSearchClear"> <a href="#" @click="onSearchClear">
@@ -1784,8 +1784,8 @@ defineExpose({ enterEditMode });
</a> </a>
</template> </template>
</I18nT> </I18nT>
</N8nText> </template>
</div> </NDVEmptyState>
<N8nText v-else> <N8nText v-else>
{{ noDataInBranchMessage }} {{ noDataInBranchMessage }}
</N8nText> </N8nText>
@@ -1848,9 +1848,12 @@ defineExpose({ enterEditMode });
</N8nText> </N8nText>
</div> </div>
<div v-else-if="showIoSearchNoMatchContent" :class="$style.center"> <NDVEmptyState
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noMatch.title') }}</N8nText> v-else-if="showIoSearchNoMatchContent"
<N8nText> :class="$style.center"
:title="i18n.baseText('ndv.search.noMatch.title')"
>
<template #description>
<I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global"> <I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global">
<template #link> <template #link>
<a href="#" @click="onSearchClear"> <a href="#" @click="onSearchClear">
@@ -1858,8 +1861,8 @@ defineExpose({ enterEditMode });
</a> </a>
</template> </template>
</I18nT> </I18nT>
</N8nText> </template>
</div> </NDVEmptyState>
<Suspense v-else-if="hasNodeRun && displayMode === 'table' && node"> <Suspense v-else-if="hasNodeRun && displayMode === 'table' && node">
<LazyRunDataTable <LazyRunDataTable

View File

@@ -47,6 +47,7 @@ import { DateTime } from 'luxon';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import { I18nT } from 'vue-i18n'; import { I18nT } from 'vue-i18n';
import { useTelemetryContext } from '@/composables/useTelemetryContext'; import { useTelemetryContext } from '@/composables/useTelemetryContext';
import NDVEmptyState from '@/components/NDVEmptyState.vue';
type Props = { type Props = {
nodes?: IConnectedNode[]; nodes?: IConnectedNode[];
@@ -430,10 +431,15 @@ const onDragEnd = (el: HTMLElement) => {
</script> </script>
<template> <template>
<div :class="['run-data-schema', 'full-height', props.compact ? 'compact' : '']"> <div
<div v-if="noSearchResults" class="no-results"> :class="[
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noNodeMatch.title') }}</N8nText> 'run-data-schema',
<N8nText> 'full-height',
{ compact: props.compact, 'no-search-results': noSearchResults },
]"
>
<NDVEmptyState v-if="noSearchResults" :title="i18n.baseText('ndv.search.noNodeMatch.title')">
<template #description>
<I18nT keypath="ndv.search.noMatchSchema.description" tag="span" scope="global"> <I18nT keypath="ndv.search.noMatchSchema.description" tag="span" scope="global">
<template #link> <template #link>
<a href="#" @click="emit('clear:search')"> <a href="#" @click="emit('clear:search')">
@@ -441,8 +447,8 @@ const onDragEnd = (el: HTMLElement) => {
</a> </a>
</template> </template>
</I18nT> </I18nT>
</N8nText> </template>
</div> </NDVEmptyState>
<Draggable <Draggable
v-if="items.length" v-if="items.length"
@@ -552,6 +558,12 @@ const onDragEnd = (el: HTMLElement) => {
.run-data-schema { .run-data-schema {
padding: 0; padding: 0;
&.no-search-results {
display: flex;
justify-content: center;
padding: var(--spacing-l) 0;
}
} }
.scroller { .scroller {
@@ -563,17 +575,6 @@ const onDragEnd = (el: HTMLElement) => {
} }
} }
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
gap: var(--spacing-2xs);
padding: var(--ndv-spacing) var(--ndv-spacing) var(--spacing-xl) var(--ndv-spacing);
}
.icon { .icon {
display: inline-flex; display: inline-flex;
margin-left: var(--spacing-xl); margin-left: var(--spacing-xl);

View File

@@ -106,8 +106,9 @@ defineExpose({
box-shadow: none !important; box-shadow: none !important;
margin-top: -2px; margin-top: -2px;
/* Override break-all set for el-popper */ /* Override styles set for el-popper */
word-break: normal; word-break: normal;
text-align: unset;
} }
.inputPanel { .inputPanel {

View File

@@ -146,6 +146,7 @@ import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesS
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store'; import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
import { useKeybindings } from '@/composables/useKeybindings'; import { useKeybindings } from '@/composables/useKeybindings';
import { type ContextMenuAction } from '@/composables/useContextMenuItems'; import { type ContextMenuAction } from '@/composables/useContextMenuItems';
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
defineOptions({ defineOptions({
name: 'NodeView', name: 'NodeView',
@@ -208,6 +209,7 @@ const agentRequestStore = useAgentRequestStore();
const logsStore = useLogsStore(); const logsStore = useLogsStore();
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore(); const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
const experimentalNdvStore = useExperimentalNdvStore();
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({ const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
route, route,
@@ -2217,7 +2219,9 @@ onBeforeUnmount(() => {
</Suspense> </Suspense>
</WorkflowCanvas> </WorkflowCanvas>
<FocusPanel <FocusPanel
v-if="!isLoading" v-if="
!isLoading && (experimentalNdvStore.isNdvInFocusPanelEnabled ? !isCanvasReadOnly : true)
"
:is-canvas-read-only="isCanvasReadOnly" :is-canvas-read-only="isCanvasReadOnly"
@save-keyboard-shortcut="onSaveWorkflow" @save-keyboard-shortcut="onSaveWorkflow"
@context-menu-action="onContextMenuAction" @context-menu-action="onContextMenuAction"