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;
border: none !important;
/* Override break-all set for el-popper */
/* Override styles set for el-popper */
word-break: normal;
text-align: unset;
}
.dropdown {

View File

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

View File

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

View File

@@ -675,7 +675,7 @@ describe('ParameterInput.vue', () => {
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({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
@@ -688,6 +688,37 @@ describe('ParameterInput.vue', () => {
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 () => {
const rendered = renderComponent({
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },

View File

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

View File

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

View File

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

View File

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

View File

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