fix(editor): Fix empty objects in schema view in output panel (#14355)

This commit is contained in:
Elias Meire
2025-04-04 19:38:39 +02:00
committed by GitHub
parent 54fcf58331
commit 2f0b5e488e
4 changed files with 837 additions and 16 deletions

View File

@@ -658,7 +658,22 @@ describe('VirtualSchema.vue', () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('should do something', () => { it('renders schema for empty objects and arrays', async () => {
expect(true).toBe(true); useWorkflowsStore().pinData({
node: mockNode1,
data: [{ json: { empty: {}, emptyArray: [], nested: [{ empty: {}, emptyArray: [] }] } }],
});
const { container, getAllByTestId } = renderComponent({
props: {
nodes: [{ name: mockNode1.name, indicies: [], depth: 1 }],
},
});
await waitFor(() => {
const headers = getAllByTestId('run-data-schema-header');
expect(headers.length).toBe(2);
});
expect(container).toMatchSnapshot();
}); });
}); });

View File

@@ -432,14 +432,12 @@ const onDragEnd = (el: HTMLElement) => {
v-else-if="item.type === 'notice'" v-else-if="item.type === 'notice'"
v-n8n-html="item.message" v-n8n-html="item.message"
class="notice" class="notice"
:style="{ marginLeft: `calc(var(--spacing-l) + var(--spacing-l) * ${item.level})` }" :style="{ '--schema-level': item.level }"
/> />
<div <div
v-else-if="item.type === 'empty'" v-else-if="item.type === 'empty'"
:style="{ class="empty-schema"
paddingBottom: `var(--spacing-xs)`, :style="{ '--schema-level': item.level }"
marginLeft: `var(--spacing-xl)`,
}"
> >
<N8nText tag="div" size="small"> <N8nText tag="div" size="small">
<i18n-t <i18n-t
@@ -450,12 +448,12 @@ const onDragEnd = (el: HTMLElement) => {
<template #link> <template #link>
<NodeExecuteButton <NodeExecuteButton
:node-name="item.nodeName" :node-name="item.nodeName"
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
text text
telemetry-source="inputs" telemetry-source="inputs"
hide-icon hide-icon
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
size="small" size="small"
:style="{ padding: 0 }" class="execute-button"
/> />
</template> </template>
</i18n-t> </i18n-t>
@@ -507,4 +505,17 @@ const onDragEnd = (el: HTMLElement) => {
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
line-height: var(--font-line-height-loose); line-height: var(--font-line-height-loose);
} }
.notice {
margin-left: calc(var(--spacing-l) * var(--schema-level));
}
.empty-schema {
padding-bottom: var(--spacing-xs);
margin-left: calc((var(--spacing-xl) * var(--schema-level)));
}
.execute-button {
padding: 0;
}
</style> </style>

View File

@@ -910,6 +910,795 @@ exports[`VirtualSchema.vue > renders schema for correct output branch 1`] = `
</div> </div>
`; `;
exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
<div>
<div
class="run-data-schema full-height"
data-v-d00cba9a=""
>
<!--v-if-->
<div
class="full-height"
data-v-d00cba9a=""
>
<div
class="full-height scroller"
data-v-d00cba9a=""
min-item-size="38"
>
<div
class="schema-header-wrapper"
data-v-882a318e=""
data-v-d00cba9a=""
id="Manual Trigger"
type="header"
>
<div
class="schema-header"
data-test-id="run-data-schema-header"
data-v-882a318e=""
>
<div
class="toggle"
data-v-882a318e=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-882a318e=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
<div
class="n8n-node-icon icon icon-trigger icon icon-trigger"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
<div
class="title"
data-v-882a318e=""
>
Manual Trigger
<!--v-if-->
</div>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="trigger-icon"
data-v-882a318e=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="bolt"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="xs"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<div
class="extra-info"
data-test-id="run-data-schema-node-item-count"
data-v-882a318e=""
>
1 item
</div>
</div>
<!--v-if-->
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="empty"
data-nest-level="1"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".empty"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.empty }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="cube"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
empty
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="emptyArray"
data-nest-level="1"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".emptyArray"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.emptyArray }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="list"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
emptyArray
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="nested"
data-nest-level="1"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".nested"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.nested }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="list"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
nested
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="nested[0]"
data-nest-level="2"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".nested[0]"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.nested[0] }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="cube"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
nested[0]
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="empty"
data-nest-level="3"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".nested[0].empty"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.nested[0].empty }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="cube"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
empty
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a=""
type="item"
>
<div
class="toggle-container"
data-v-0f5e7239=""
>
<div
class="toggle"
data-v-0f5e7239=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
</div>
<div
class="pill"
data-depth="1"
data-name="emptyArray"
data-nest-level="3"
data-node-name="Manual Trigger"
data-node-type="n8n-nodes-base.manualTrigger"
data-path=".nested[0].emptyArray"
data-target="mappable"
data-test-id="run-data-schema-node-name"
data-v-0f5e7239=""
data-value="{{ $json.nested[0].emptyArray }}"
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="type-icon"
data-v-0f5e7239=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="list"
inverse="false"
listitem="false"
pulse="false"
shake="false"
size="sm"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
<span
class="content title"
data-v-0f5e7239=""
>
<span>
<!--v-if-->
emptyArray
</span>
</span>
</div>
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
>
<span />
</span>
</div>
<div
class="schema-header-wrapper"
data-v-882a318e=""
data-v-d00cba9a=""
id="variables"
type="header"
>
<div
class="schema-header"
data-test-id="run-data-schema-header"
data-v-882a318e=""
>
<div
class="toggle"
data-v-882a318e=""
>
<font-awesome-icon-stub
beat="false"
beatfade="false"
border="false"
bounce="false"
class="collapse-icon collapsed"
data-v-882a318e=""
fade="false"
fixedwidth="false"
flash="false"
flip="false"
icon="angle-down"
inverse="false"
listitem="false"
pulse="false"
shake="false"
spin="false"
spinpulse="false"
spinreverse="false"
swapopacity="false"
symbol="false"
/>
</div>
<!--v-if-->
<div
class="title"
data-v-882a318e=""
>
Variables and context
<!--v-if-->
</div>
<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
<!--teleport start-->
<!--teleport end-->
</div>
</div>
</div>
`;
exports[`VirtualSchema.vue > renders schema in output pane 1`] = ` exports[`VirtualSchema.vue > renders schema in output pane 1`] = `
<div> <div>
<div <div
@@ -2005,7 +2794,9 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
<div <div
class="empty-schema"
data-v-d00cba9a="" data-v-d00cba9a=""
style="--schema-level: 1;"
> >
<div <div
class="n8n-text size-small regular" class="n8n-text size-small regular"
@@ -2019,8 +2810,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
<button <button
aria-live="polite" aria-live="polite"
class="button button primary small text el-tooltip__trigger el-tooltip__trigger" class="button button primary small text execute-button el-tooltip__trigger el-tooltip__trigger execute-button el-tooltip__trigger el-tooltip__trigger"
style="padding: 0px;"
title="Runs the current node. Will also run previous nodes if they have not been run yet" title="Runs the current node. Will also run previous nodes if they have not been run yet"
transparent-background="false" transparent-background="false"
> >

View File

@@ -283,6 +283,7 @@ export type RenderNotice = {
export type RenderEmpty = { export type RenderEmpty = {
id: string; id: string;
type: 'empty'; type: 'empty';
level: number;
nodeName: string; nodeName: string;
key: 'emptyData' | 'emptyDataWithBinary' | 'executeSchema'; key: 'emptyData' | 'emptyDataWithBinary' | 'executeSchema';
}; };
@@ -304,11 +305,15 @@ const icons = {
const getIconBySchemaType = (type: Schema['type']): string => icons[type]; const getIconBySchemaType = (type: Schema['type']): string => icons[type];
const emptyItem = (key: RenderEmpty['key'], nodeName?: string): RenderEmpty => ({ const emptyItem = (
key: RenderEmpty['key'],
{ nodeName, level }: { nodeName?: string; level?: number } = {},
): RenderEmpty => ({
id: `empty-${window.crypto.randomUUID()}`, id: `empty-${window.crypto.randomUUID()}`,
type: 'empty', type: 'empty',
key, key,
nodeName: nodeName || '', level: level ?? 0,
nodeName: nodeName ?? '',
}); });
const moreFieldsItem = (): RenderIcon => ({ const moreFieldsItem = (): RenderIcon => ({
@@ -369,7 +374,7 @@ export const useFlattenSchema = () => {
preview?: boolean; preview?: boolean;
}): Renders[] => { }): Renders[] => {
// only show empty item for the first level // only show empty item for the first level
if (isDataEmpty(schema) && depth <= 0) { if (isDataEmpty(schema) && level < 0) {
return [emptyItem('emptyData')]; return [emptyItem('emptyData')];
} }
@@ -466,13 +471,13 @@ export const useFlattenSchema = () => {
} }
if (isDataEmpty(item.schema) && !item.isNodeExecuted && !item.hasBinary) { if (isDataEmpty(item.schema) && !item.isNodeExecuted && !item.hasBinary) {
acc.push(emptyItem('executeSchema', item.node.name)); acc.push(emptyItem('executeSchema', { nodeName: item.node.name, level: 1 }));
return acc; return acc;
} }
if (isDataEmpty(item.schema)) { if (isDataEmpty(item.schema)) {
const key = item.hasBinary ? 'emptyDataWithBinary' : 'emptyData'; const key = item.hasBinary ? 'emptyDataWithBinary' : 'emptyData';
acc.push(emptyItem(key)); acc.push(emptyItem(key, { level: 1 }));
return acc; return acc;
} }