feat(editor): NDV UI overhaul experiment (#14209)

Co-authored-by: jakeranallo <jake.ranallo@gmail.com>
This commit is contained in:
Elias Meire
2025-07-04 17:14:17 +02:00
committed by GitHub
parent 5db122be0d
commit 6ef38411d8
33 changed files with 2528 additions and 195 deletions

View File

@@ -22,6 +22,8 @@ import { N8nRadioButtons, N8nText } from '@n8n/design-system';
import { useSettingsStore } from '@/stores/settings.store';
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
import { CanvasNodeDirtiness } from '@/types';
import { NDV_UI_OVERHAUL_EXPERIMENT } from '@/constants';
import { usePostHog } from '@/stores/posthog.store';
import { type IRunDataDisplayMode } from '@/Interface';
// Types
@@ -67,6 +69,7 @@ const emit = defineEmits<{
itemHover: [item: { itemIndex: number; outputIndex: number } | null];
search: [string];
openSettings: [];
execute: [];
displayModeChange: [IRunDataDisplayMode];
}>();
@@ -75,6 +78,7 @@ const emit = defineEmits<{
const ndvStore = useNDVStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const posthogStore = usePostHog();
const telemetry = useTelemetry();
const i18n = useI18n();
const { activeNode } = storeToRefs(ndvStore);
@@ -250,6 +254,13 @@ const allToolsWereUnusedNotice = computed(() => {
}
});
const isNDVV2 = computed(() =>
posthogStore.isVariantEnabled(
NDV_UI_OVERHAUL_EXPERIMENT.name,
NDV_UI_OVERHAUL_EXPERIMENT.variant,
),
);
// Methods
const insertTestData = () => {
@@ -335,6 +346,7 @@ const activatePane = () => {
:callout-message="allToolsWereUnusedNotice"
:display-mode="displayMode"
:disable-ai-content="true"
data-test-id="ndv-output-panel"
@activate-pane="activatePane"
@run-change="onRunIndexChange"
@link-run="onLinkRun"
@@ -345,7 +357,7 @@ const activatePane = () => {
@display-mode-change="emit('displayModeChange', $event)"
>
<template #header>
<div :class="$style.titleSection">
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
<template v-if="hasAiMetadata">
<N8nRadioButtons
v-model="outputMode"
@@ -353,7 +365,7 @@ const activatePane = () => {
:options="outputTypes"
/>
</template>
<span v-else :class="$style.title">
<span v-else :class="[$style.title, { [$style.titleV2]: isNDVV2 }]">
{{ i18n.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
</span>
<RunInfo
@@ -371,24 +383,79 @@ const activatePane = () => {
</template>
<template #node-not-run>
<N8nText v-if="workflowRunning && !isTriggerNode" data-test-id="ndv-output-waiting">{{
i18n.baseText('ndv.output.waitingToRun')
}}</N8nText>
<N8nText v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
<template v-if="isSubNodeType">
{{ i18n.baseText('ndv.output.runNodeHintSubNode') }}
</template>
<template v-else>
{{ i18n.baseText('ndv.output.runNodeHint') }}
<span v-if="canPinData" @click="insertTestData">
<br />
{{ i18n.baseText('generic.or') }}
<N8nText tag="a" size="medium" color="primary">
{{ i18n.baseText('ndv.output.insertTestData') }}
</N8nText>
</span>
</template>
</N8nText>
<template v-if="isNDVV2">
<NDVEmptyState
:title="
i18n.baseText(
isTriggerNode
? 'ndv.output.noOutputData.trigger.title'
: 'ndv.output.noOutputData.v2.title',
)
"
>
<template v-if="isTriggerNode" #icon>
<svg width="16" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.9062 2.40625L8.5 8.03125H12C12.4062 8.03125 12.7812 8.28125 12.9375 8.65625C13.0625 9.0625 12.9688 9.5 12.6562 9.78125L4.65625 16.7812C4.28125 17.0625 3.78125 17.0938 3.40625 16.8125C3.03125 16.5625 2.875 16.0625 3.0625 15.625L5.46875 10H2C1.5625 10 1.1875 9.75 1.0625 9.375C0.90625 8.96875 1 8.53125 1.3125 8.25L9.3125 1.25C9.6875 0.96875 10.1875 0.9375 10.5625 1.21875C10.9375 1.46875 11.0938 1.96875 10.9062 2.40625Z"
fill="currentColor"
/>
</svg>
</template>
<template v-else #icon>
<N8nIcon icon="arrow-right-from-line" size="xlarge" />
</template>
<template #description>
<i18n-t
tag="span"
:keypath="
isSubNodeType
? 'ndv.output.runNodeHintSubNode'
: 'ndv.output.noOutputData.v2.description'
"
>
<template #link>
<NodeExecuteButton
hide-icon
transparent
type="secondary"
:node-name="activeNode?.name ?? ''"
:label="
i18n.baseText(
isTriggerNode
? 'ndv.output.noOutputData.trigger.action'
: 'ndv.output.noOutputData.v2.action',
)
"
telemetry-source="inputs"
@execute="emit('execute')"
/>
<br />
</template>
</i18n-t>
</template>
</NDVEmptyState>
</template>
<template v-else>
<N8nText v-if="workflowRunning && !isTriggerNode" data-test-id="ndv-output-waiting">{{
i18n.baseText('ndv.output.waitingToRun')
}}</N8nText>
<N8nText v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
<template v-if="isSubNodeType">
{{ i18n.baseText('ndv.output.runNodeHintSubNode') }}
</template>
<template v-else>
{{ i18n.baseText('ndv.output.runNodeHint') }}
<span v-if="canPinData" @click="insertTestData">
<br />
{{ i18n.baseText('generic.or') }}
<N8nText tag="a" size="medium" color="primary">
{{ i18n.baseText('ndv.output.insertTestData') }}
</N8nText>
</span>
</template>
</N8nText>
</template>
</template>
<template #node-waiting>
@@ -453,14 +520,23 @@ const activatePane = () => {
}
}
.titleSectionV2 {
padding-left: var(--spacing-4xs);
}
.title {
text-transform: uppercase;
color: var(--color-text-light);
letter-spacing: 3px;
letter-spacing: 2px;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-s);
}
.titleV2 {
letter-spacing: 2px;
font-size: var(--font-size-xs);
}
.noOutputData {
max-width: 180px;
@@ -482,4 +558,11 @@ const activatePane = () => {
margin-bottom: var(--spacing-m);
}
}
.link {
display: inline;
padding: 0;
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
}
</style>