mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Improve schema view empty state when node has binary data (#14044)
This commit is contained in:
@@ -1,33 +1,33 @@
|
|||||||
import { createComponentRenderer } from '@/__tests__/render';
|
|
||||||
import VirtualSchema from '@/components/VirtualSchema.vue';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { userEvent } from '@testing-library/user-event';
|
|
||||||
import { cleanup, waitFor } from '@testing-library/vue';
|
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
|
||||||
import {
|
import {
|
||||||
createTestNode,
|
createTestNode,
|
||||||
defaultNodeDescriptions,
|
defaultNodeDescriptions,
|
||||||
mockNodeTypeDescription,
|
mockNodeTypeDescription,
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { IF_NODE_TYPE, SET_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import VirtualSchema from '@/components/VirtualSchema.vue';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import * as nodeHelpers from '@/composables/useNodeHelpers';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { IF_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { fireEvent } from '@testing-library/dom';
|
||||||
|
import { userEvent } from '@testing-library/user-event';
|
||||||
|
import { cleanup, waitFor } from '@testing-library/vue';
|
||||||
import {
|
import {
|
||||||
createResultOk,
|
createResultOk,
|
||||||
NodeConnectionTypes,
|
NodeConnectionTypes,
|
||||||
type IDataObject,
|
type IBinaryData,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import * as nodeHelpers from '@/composables/useNodeHelpers';
|
import { setActivePinia } from 'pinia';
|
||||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
|
||||||
import { fireEvent } from '@testing-library/dom';
|
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { useSchemaPreviewStore } from '../stores/schemaPreview.store';
|
|
||||||
import { usePostHog } from '../stores/posthog.store';
|
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
|
||||||
import { defaultSettings } from '../__tests__/defaults';
|
import { defaultSettings } from '../__tests__/defaults';
|
||||||
|
import { usePostHog } from '../stores/posthog.store';
|
||||||
|
import { useSchemaPreviewStore } from '../stores/schemaPreview.store';
|
||||||
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
|
|
||||||
const mockNode1 = createTestNode({
|
const mockNode1 = createTestNode({
|
||||||
name: 'Manual Trigger',
|
name: 'Manual Trigger',
|
||||||
@@ -65,6 +65,14 @@ const aiTool = createTestNode({
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeWithCredential = createTestNode({
|
||||||
|
name: 'Notion',
|
||||||
|
type: 'n8n-nodes-base.notion',
|
||||||
|
typeVersion: 1,
|
||||||
|
credentials: { notionApi: { id: 'testId', name: 'testName' } },
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
const unknownNodeType = createTestNode({
|
const unknownNodeType = createTestNode({
|
||||||
name: 'Unknown Node Type',
|
name: 'Unknown Node Type',
|
||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
@@ -76,15 +84,23 @@ const defaultNodes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function setupStore() {
|
async function setupStore() {
|
||||||
const workflow = mock<IWorkflowDb>({
|
const workflow = {
|
||||||
id: '123',
|
id: '123',
|
||||||
name: 'Test Workflow',
|
name: 'Test Workflow',
|
||||||
connections: {},
|
connections: {},
|
||||||
active: true,
|
active: true,
|
||||||
nodes: [mockNode1, mockNode2, disabledNode, ifNode, aiTool, unknownNodeType],
|
nodes: [
|
||||||
});
|
mockNode1,
|
||||||
|
mockNode2,
|
||||||
|
disabledNode,
|
||||||
|
ifNode,
|
||||||
|
aiTool,
|
||||||
|
unknownNodeType,
|
||||||
|
nodeWithCredential,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createTestingPinia({ stubActions: false });
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -102,20 +118,24 @@ async function setupStore() {
|
|||||||
name: IF_NODE_TYPE,
|
name: IF_NODE_TYPE,
|
||||||
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
|
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
|
||||||
}),
|
}),
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: 'n8n-nodes-base.notion',
|
||||||
|
outputs: [NodeConnectionTypes.Main],
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
workflowsStore.workflow = workflow;
|
workflowsStore.workflow = workflow as IWorkflowDb;
|
||||||
|
|
||||||
return pinia;
|
return pinia;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockNodeOutputData(nodeName: string, data: IDataObject[], outputIndex = 0) {
|
function mockNodeOutputData(nodeName: string, data: INodeExecutionData[], outputIndex = 0) {
|
||||||
const originalNodeHelpers = nodeHelpers.useNodeHelpers();
|
const originalNodeHelpers = nodeHelpers.useNodeHelpers();
|
||||||
vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
|
vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
...originalNodeHelpers,
|
...originalNodeHelpers,
|
||||||
getNodeInputData: vi.fn((node, _, output) => {
|
getNodeInputData: vi.fn((node, _, output) => {
|
||||||
if (node.name === nodeName && output === outputIndex) {
|
if (node.name === nodeName && output === outputIndex) {
|
||||||
return data.map((json) => ({ json }));
|
return data;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}),
|
}),
|
||||||
@@ -146,7 +166,7 @@ describe('VirtualSchema.vue', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue(123);
|
vi.resetAllMocks();
|
||||||
vi.setSystemTime('2025-01-01');
|
vi.setSystemTime('2025-01-01');
|
||||||
renderComponent = createComponentRenderer(VirtualSchema, {
|
renderComponent = createComponentRenderer(VirtualSchema, {
|
||||||
global: {
|
global: {
|
||||||
@@ -183,6 +203,20 @@ describe('VirtualSchema.vue', () => {
|
|||||||
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1);
|
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders schema for empty data with binary', async () => {
|
||||||
|
mockNodeOutputData(mockNode1.name, [{ json: {}, binary: { data: mock<IBinaryData>() } }]);
|
||||||
|
|
||||||
|
const { getByText } = renderComponent({
|
||||||
|
props: { nodes: [{ name: mockNode1.name, indicies: [], depth: 1 }] },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getByText("Only binary data exists. View it using the 'Binary' tab"),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders schema for data', async () => {
|
it('renders schema for data', async () => {
|
||||||
useWorkflowsStore().pinData({
|
useWorkflowsStore().pinData({
|
||||||
node: mockNode1,
|
node: mockNode1,
|
||||||
@@ -307,10 +341,7 @@ describe('VirtualSchema.vue', () => {
|
|||||||
it('renders schema for correct output branch', async () => {
|
it('renders schema for correct output branch', async () => {
|
||||||
mockNodeOutputData(
|
mockNodeOutputData(
|
||||||
'If',
|
'If',
|
||||||
[
|
[{ json: { id: 1, name: 'John' } }, { json: { id: 2, name: 'Jane' } }],
|
||||||
{ id: 1, name: 'John' },
|
|
||||||
{ id: 2, name: 'Jane' },
|
|
||||||
],
|
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
const { getAllByTestId } = renderComponent({
|
const { getAllByTestId } = renderComponent({
|
||||||
@@ -330,10 +361,7 @@ describe('VirtualSchema.vue', () => {
|
|||||||
it('renders previous nodes schema for AI tools', async () => {
|
it('renders previous nodes schema for AI tools', async () => {
|
||||||
mockNodeOutputData(
|
mockNodeOutputData(
|
||||||
'If',
|
'If',
|
||||||
[
|
[{ json: { id: 1, name: 'John' } }, { json: { id: 2, name: 'Jane' } }],
|
||||||
{ id: 1, name: 'John' },
|
|
||||||
{ id: 2, name: 'Jane' },
|
|
||||||
],
|
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const { getAllByTestId } = renderComponent({
|
const { getAllByTestId } = renderComponent({
|
||||||
@@ -481,14 +509,6 @@ describe('VirtualSchema.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should track data pill drag and drop for schema preview', async () => {
|
it('should track data pill drag and drop for schema preview', async () => {
|
||||||
useWorkflowsStore().pinData({
|
|
||||||
node: {
|
|
||||||
...mockNode2,
|
|
||||||
credentials: { myCredential: { id: 'myCredential', name: 'myCredential' } },
|
|
||||||
},
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const trackSpy = vi.spyOn(telemetry, 'track');
|
const trackSpy = vi.spyOn(telemetry, 'track');
|
||||||
const posthogStore = usePostHog();
|
const posthogStore = usePostHog();
|
||||||
@@ -513,7 +533,7 @@ describe('VirtualSchema.vue', () => {
|
|||||||
|
|
||||||
const { getAllByTestId } = renderComponent({
|
const { getAllByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
nodes: [{ name: mockNode2.name, indicies: [], depth: 1 }],
|
nodes: [{ name: nodeWithCredential.name, indicies: [], depth: 1 }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -639,4 +659,8 @@ describe('VirtualSchema.vue', () => {
|
|||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should do something', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,21 +105,11 @@ 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 connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
|
const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
|
||||||
const data =
|
const nodeData = connectedOutputIndexes.map((outputIndex) =>
|
||||||
pinData ??
|
getNodeInputData(fullNode, props.runIndex, outputIndex, props.paneType, props.connectionType),
|
||||||
connectedOutputIndexes
|
);
|
||||||
.map((outputIndex) =>
|
const hasBinary = nodeData.flat().some((data) => !isEmpty(data.binary));
|
||||||
executionDataToJson(
|
const data = pinData ?? nodeData.map(executionDataToJson).flat();
|
||||||
getNodeInputData(
|
|
||||||
fullNode,
|
|
||||||
props.runIndex,
|
|
||||||
outputIndex,
|
|
||||||
props.paneType,
|
|
||||||
props.connectionType,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
let schema = getSchemaForExecutionData(data);
|
let schema = getSchemaForExecutionData(data);
|
||||||
let preview = false;
|
let preview = false;
|
||||||
@@ -137,6 +127,7 @@ const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) =
|
|||||||
connectedOutputIndexes,
|
connectedOutputIndexes,
|
||||||
itemsCount: data.length,
|
itemsCount: data.length,
|
||||||
preview,
|
preview,
|
||||||
|
hasBinary,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,7 +242,7 @@ 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 } = await getNodeSchema(
|
const { schema, connectedOutputIndexes, itemsCount, preview, hasBinary } = await getNodeSchema(
|
||||||
fullNode,
|
fullNode,
|
||||||
node,
|
node,
|
||||||
);
|
);
|
||||||
@@ -268,6 +259,7 @@ const nodesSchemas = asyncComputed<SchemaNode[]>(async () => {
|
|||||||
nodeType,
|
nodeType,
|
||||||
schema: filteredSchema,
|
schema: filteredSchema,
|
||||||
preview,
|
preview,
|
||||||
|
hasBinary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ export type SchemaNode = {
|
|||||||
itemsCount: number;
|
itemsCount: number;
|
||||||
schema: Schema;
|
schema: Schema;
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
|
hasBinary: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenderItem = {
|
export type RenderItem = {
|
||||||
@@ -295,10 +296,12 @@ const icons = {
|
|||||||
|
|
||||||
const getIconBySchemaType = (type: Schema['type']): string => icons[type];
|
const getIconBySchemaType = (type: Schema['type']): string => icons[type];
|
||||||
|
|
||||||
const emptyItem = (): RenderItem => ({
|
const emptyItem = (
|
||||||
|
message = useI18n().baseText('dataMapping.schemaView.emptyData'),
|
||||||
|
): RenderItem => ({
|
||||||
id: `empty-${window.crypto.randomUUID()}`,
|
id: `empty-${window.crypto.randomUUID()}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
value: useI18n().baseText('dataMapping.schemaView.emptyData'),
|
value: message,
|
||||||
type: 'item',
|
type: 'item',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -457,7 +460,12 @@ export const useFlattenSchema = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDataEmpty(item.schema)) {
|
if (isDataEmpty(item.schema)) {
|
||||||
acc.push(emptyItem());
|
const message = useI18n().baseText(
|
||||||
|
item.hasBinary
|
||||||
|
? 'dataMapping.schemaView.emptyDataWithBinary'
|
||||||
|
: 'dataMapping.schemaView.emptyData',
|
||||||
|
);
|
||||||
|
acc.push(emptyItem(message));
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -667,6 +667,7 @@
|
|||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
||||||
"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.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}",
|
||||||
|
|||||||
Reference in New Issue
Block a user