fix(editor): Change label for unexecuted nodes (#14260)

This commit is contained in:
Dana
2025-04-01 16:15:42 +02:00
committed by GitHub
parent 837131fc96
commit 08450b20af
6 changed files with 95 additions and 48 deletions

View File

@@ -251,6 +251,8 @@ If that gets executed in one of the package folders it will only run the tests
of this package. If it gets executed in the n8n-root folder it will run all of this package. If it gets executed in the n8n-root folder it will run all
tests of all packages. tests of all packages.
If you made a change which requires an update on a `.test.ts.snap` file, pass `-u` to the command to run tests or press `u` in watch mode.
#### E2E tests #### E2E tests
⚠️ You have to run `pnpm cypress:install` to install cypress before running the tests for the first time and to update cypress. ⚠️ You have to run `pnpm cypress:install` to install cypress before running the tests for the first time and to update cypress.

View File

@@ -191,16 +191,14 @@ describe('VirtualSchema.vue', () => {
}); });
}); });
it('renders schema for empty data', async () => { it('renders schema for empty data for unexecuted nodes', async () => {
const { getAllByText, getAllByTestId } = renderComponent(); const { getAllByText, getAllByTestId } = renderComponent();
await waitFor(() => await waitFor(() => expect(getAllByText('Execute previous nodes').length).toBe(2));
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(2),
);
// Collapse second node // Collapse second node
await userEvent.click(getAllByTestId('run-data-schema-header')[1]); await userEvent.click(getAllByTestId('run-data-schema-header')[1]);
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1); expect(getAllByText('Execute previous nodes').length).toBe(1);
}); });
it('renders schema for empty data with binary', async () => { it('renders schema for empty data with binary', async () => {
@@ -295,7 +293,7 @@ describe('VirtualSchema.vue', () => {
await waitFor(() => { await waitFor(() => {
const items = getAllByTestId('run-data-schema-item'); const items = getAllByTestId('run-data-schema-item');
expect(items).toHaveLength(6); expect(items).toHaveLength(5);
}); });
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
@@ -309,7 +307,7 @@ describe('VirtualSchema.vue', () => {
const { getAllByText } = renderComponent(); const { getAllByText } = renderComponent();
await waitFor(() => await waitFor(() =>
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(2), expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1),
); );
}); });
@@ -482,7 +480,7 @@ describe('VirtualSchema.vue', () => {
const { getAllByTestId } = renderComponent(); const { getAllByTestId } = renderComponent();
await waitFor(() => { await waitFor(() => {
expect(getAllByTestId('run-data-schema-item')).toHaveLength(6); expect(getAllByTestId('run-data-schema-item')).toHaveLength(5);
}); });
const items = getAllByTestId('run-data-schema-item'); const items = getAllByTestId('run-data-schema-item');

View File

@@ -49,6 +49,7 @@ import { asyncComputed } from '@vueuse/core';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'; import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import NodeExecuteButton from './NodeExecuteButton.vue';
type Props = { type Props = {
nodes?: IConnectedNode[]; nodes?: IConnectedNode[];
@@ -89,7 +90,7 @@ const { getSchemaForExecutionData, getSchemaForJsonSchema, getSchema, filterSche
useDataSchema(); useDataSchema();
const { closedNodes, flattenSchema, flattenMultipleSchemas, toggleLeaf, toggleNode } = const { closedNodes, flattenSchema, flattenMultipleSchemas, toggleLeaf, toggleNode } =
useFlattenSchema(); useFlattenSchema();
const { getNodeInputData } = useNodeHelpers(); const { getNodeInputData, getNodeTaskData } = useNodeHelpers();
const emit = defineEmits<{ const emit = defineEmits<{
'clear:search': []; 'clear:search': [];
@@ -104,6 +105,8 @@ const toggleNodeAndScrollTop = (id: string) => {
const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) => { const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) => {
const pinData = workflowsStore.pinDataByNodeName(connectedNode.name); const pinData = workflowsStore.pinDataByNodeName(connectedNode.name);
const hasPinnedData = pinData ? pinData.length > 0 : false;
const isNodeExecuted = getNodeTaskData(fullNode, props.runIndex) !== null || hasPinnedData;
const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0]; const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
const nodeData = connectedOutputIndexes.map((outputIndex) => const nodeData = connectedOutputIndexes.map((outputIndex) =>
getNodeInputData(fullNode, props.runIndex, outputIndex, props.paneType, props.connectionType), getNodeInputData(fullNode, props.runIndex, outputIndex, props.paneType, props.connectionType),
@@ -128,6 +131,7 @@ const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) =
itemsCount: data.length, itemsCount: data.length,
preview, preview,
hasBinary, hasBinary,
isNodeExecuted,
}; };
}; };
@@ -242,10 +246,8 @@ const nodesSchemas = asyncComputed<SchemaNode[]>(async () => {
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion); const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
if (!nodeType) continue; if (!nodeType) continue;
const { schema, connectedOutputIndexes, itemsCount, preview, hasBinary } = await getNodeSchema( const { schema, connectedOutputIndexes, itemsCount, preview, hasBinary, isNodeExecuted } =
fullNode, await getNodeSchema(fullNode, node);
node,
);
const filteredSchema = filterSchema(schema, search); const filteredSchema = filterSchema(schema, search);
@@ -260,6 +262,7 @@ const nodesSchemas = asyncComputed<SchemaNode[]>(async () => {
schema: filteredSchema, schema: filteredSchema,
preview, preview,
hasBinary, hasBinary,
isNodeExecuted,
}); });
} }
@@ -431,6 +434,34 @@ const onDragEnd = (el: HTMLElement) => {
class="notice" class="notice"
:style="{ marginLeft: `calc(var(--spacing-l) + var(--spacing-l) * ${item.level})` }" :style="{ marginLeft: `calc(var(--spacing-l) + var(--spacing-l) * ${item.level})` }"
/> />
<div
v-else-if="item.type === 'empty'"
:style="{
paddingBottom: `var(--spacing-xs)`,
marginLeft: `var(--spacing-xl)`,
}"
>
<N8nText tag="div" size="small">
<i18n-t
v-if="item.key === 'executeSchema'"
tag="span"
keypath="dataMapping.schemaView.executeSchema"
>
<template #link>
<NodeExecuteButton
:node-name="item.nodeName"
text
telemetry-source="inputs"
hide-icon
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
size="small"
:style="{ padding: 0 }"
/>
</template>
</i18n-t>
<i18n-t v-else tag="span" :keypath="`dataMapping.schemaView.${item.key}`" />
</N8nText>
</div>
</DynamicScrollerItem> </DynamicScrollerItem>
</template> </template>
</DynamicScroller> </DynamicScroller>

View File

@@ -2005,32 +2005,40 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
<div <div
class="schema-item draggable"
data-test-id="run-data-schema-item"
data-v-0f5e7239=""
data-v-d00cba9a="" data-v-d00cba9a=""
type="item"
> >
<div <div
class="toggle-container" class="n8n-text size-small regular"
data-v-0f5e7239="" data-v-d00cba9a=""
>
<!--v-if-->
</div>
<!--v-if-->
<!--v-if-->
<span
class="content text"
data-test-id="run-data-schema-item-value"
data-v-0f5e7239=""
> >
<span> <span
<!--v-if--> data-v-d00cba9a=""
No fields - item(s) exist, but they're empty >
<button
aria-live="polite"
class="button button primary small text 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"
transparent-background="false"
>
<!--v-if-->
<span>
Execute previous nodes
</span>
</button>
<!--teleport start-->
<!--teleport end-->
to see schema
</span> </span>
</span> </div>
</div> </div>

View File

@@ -233,6 +233,7 @@ export type SchemaNode = {
itemsCount: number; itemsCount: number;
schema: Schema; schema: Schema;
preview: boolean; preview: boolean;
isNodeExecuted: boolean;
hasBinary: boolean; hasBinary: boolean;
}; };
@@ -279,7 +280,14 @@ export type RenderNotice = {
message: string; message: string;
}; };
export type Renders = RenderHeader | RenderItem | RenderIcon | RenderNotice; export type RenderEmpty = {
id: string;
type: 'empty';
nodeName: string;
key: 'emptyData' | 'emptyDataWithBinary' | 'executeSchema';
};
export type Renders = RenderHeader | RenderItem | RenderIcon | RenderNotice | RenderEmpty;
const icons = { const icons = {
object: 'cube', object: 'cube',
@@ -296,13 +304,11 @@ const icons = {
const getIconBySchemaType = (type: Schema['type']): string => icons[type]; const getIconBySchemaType = (type: Schema['type']): string => icons[type];
const emptyItem = ( const emptyItem = (key: RenderEmpty['key'], nodeName?: string): RenderEmpty => ({
message = useI18n().baseText('dataMapping.schemaView.emptyData'),
): RenderItem => ({
id: `empty-${window.crypto.randomUUID()}`, id: `empty-${window.crypto.randomUUID()}`,
icon: '', type: 'empty',
value: message, key,
type: 'item', nodeName: nodeName || '',
}); });
const moreFieldsItem = (): RenderIcon => ({ const moreFieldsItem = (): RenderIcon => ({
@@ -361,10 +367,10 @@ export const useFlattenSchema = () => {
prefix?: string; prefix?: string;
level?: number; level?: number;
preview?: boolean; preview?: boolean;
}): RenderItem[] => { }): 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) && depth <= 0) {
return [emptyItem()]; return [emptyItem('emptyData')];
} }
const expression = `{{ ${expressionPrefix ? expressionPrefix + schema.path : schema.path.slice(1)} }}`; const expression = `{{ ${expressionPrefix ? expressionPrefix + schema.path : schema.path.slice(1)} }}`;
@@ -372,7 +378,7 @@ export const useFlattenSchema = () => {
const id = expression; const id = expression;
if (Array.isArray(schema.value)) { if (Array.isArray(schema.value)) {
const items: RenderItem[] = []; const items: Renders[] = [];
if (schema.key) { if (schema.key) {
items.push({ items.push({
@@ -459,13 +465,14 @@ export const useFlattenSchema = () => {
return acc; return acc;
} }
if (isDataEmpty(item.schema) && !item.isNodeExecuted && !item.hasBinary) {
acc.push(emptyItem('executeSchema', item.node.name));
return acc;
}
if (isDataEmpty(item.schema)) { if (isDataEmpty(item.schema)) {
const message = useI18n().baseText( const key = item.hasBinary ? 'emptyDataWithBinary' : 'emptyData';
item.hasBinary acc.push(emptyItem(key));
? 'dataMapping.schemaView.emptyDataWithBinary'
: 'dataMapping.schemaView.emptyData',
);
acc.push(emptyItem(message));
return acc; return acc;
} }

View File

@@ -669,6 +669,7 @@
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view", "dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty", "dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
"dataMapping.schemaView.emptyDataWithBinary": "Only binary data exists. View it using the 'Binary' tab", "dataMapping.schemaView.emptyDataWithBinary": "Only binary data exists. View it using the 'Binary' tab",
"dataMapping.schemaView.executeSchema": "{link} to see schema",
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through", "dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
"dataMapping.schemaView.noMatches": "No results for '{search}'", "dataMapping.schemaView.noMatches": "No results for '{search}'",
"dataMapping.schemaView.preview": "Usually outputs the following fields. Execute the node to see the actual ones. {link}", "dataMapping.schemaView.preview": "Usually outputs the following fields. Execute the node to see the actual ones. {link}",