diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts index fa1f9490c8..3ac4bd83fc 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.test.ts @@ -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 { createTestNode, defaultNodeDescriptions, mockNodeTypeDescription, } from '@/__tests__/mocks'; -import { IF_NODE_TYPE, SET_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants'; -import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { mock } from 'vitest-mock-extended'; +import { createComponentRenderer } from '@/__tests__/render'; +import VirtualSchema from '@/components/VirtualSchema.vue'; +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 { 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 { createResultOk, NodeConnectionTypes, - type IDataObject, + type IBinaryData, type INodeExecutionData, } from 'n8n-workflow'; -import * as nodeHelpers from '@/composables/useNodeHelpers'; -import * as workflowHelpers from '@/composables/useWorkflowHelpers'; -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 { setActivePinia } from 'pinia'; +import { mock } from 'vitest-mock-extended'; 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({ name: 'Manual Trigger', @@ -65,6 +65,14 @@ const aiTool = createTestNode({ 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({ name: 'Unknown Node Type', type: 'unknown', @@ -76,15 +84,23 @@ const defaultNodes = [ ]; async function setupStore() { - const workflow = mock({ + const workflow = { id: '123', name: 'Test Workflow', connections: {}, 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); const workflowsStore = useWorkflowsStore(); @@ -102,20 +118,24 @@ async function setupStore() { name: IF_NODE_TYPE, outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main], }), + mockNodeTypeDescription({ + name: 'n8n-nodes-base.notion', + outputs: [NodeConnectionTypes.Main], + }), ]); - workflowsStore.workflow = workflow; + workflowsStore.workflow = workflow as IWorkflowDb; return pinia; } -function mockNodeOutputData(nodeName: string, data: IDataObject[], outputIndex = 0) { +function mockNodeOutputData(nodeName: string, data: INodeExecutionData[], outputIndex = 0) { const originalNodeHelpers = nodeHelpers.useNodeHelpers(); vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => { return { ...originalNodeHelpers, getNodeInputData: vi.fn((node, _, output) => { if (node.name === nodeName && output === outputIndex) { - return data.map((json) => ({ json })); + return data; } return []; }), @@ -146,7 +166,7 @@ describe('VirtualSchema.vue', () => { beforeEach(async () => { cleanup(); - vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue(123); + vi.resetAllMocks(); vi.setSystemTime('2025-01-01'); renderComponent = createComponentRenderer(VirtualSchema, { global: { @@ -183,6 +203,20 @@ describe('VirtualSchema.vue', () => { 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() } }]); + + 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 () => { useWorkflowsStore().pinData({ node: mockNode1, @@ -307,10 +341,7 @@ describe('VirtualSchema.vue', () => { it('renders schema for correct output branch', async () => { mockNodeOutputData( 'If', - [ - { id: 1, name: 'John' }, - { id: 2, name: 'Jane' }, - ], + [{ json: { id: 1, name: 'John' } }, { json: { id: 2, name: 'Jane' } }], 1, ); const { getAllByTestId } = renderComponent({ @@ -330,10 +361,7 @@ describe('VirtualSchema.vue', () => { it('renders previous nodes schema for AI tools', async () => { mockNodeOutputData( 'If', - [ - { id: 1, name: 'John' }, - { id: 2, name: 'Jane' }, - ], + [{ json: { id: 1, name: 'John' } }, { json: { id: 2, name: 'Jane' } }], 0, ); const { getAllByTestId } = renderComponent({ @@ -481,14 +509,6 @@ describe('VirtualSchema.vue', () => { }); 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 trackSpy = vi.spyOn(telemetry, 'track'); const posthogStore = usePostHog(); @@ -513,7 +533,7 @@ describe('VirtualSchema.vue', () => { const { getAllByTestId } = renderComponent({ 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(); }); + + it('should do something', () => { + expect(true).toBe(true); + }); }); diff --git a/packages/frontend/editor-ui/src/components/VirtualSchema.vue b/packages/frontend/editor-ui/src/components/VirtualSchema.vue index bc8017adfd..ea5c16584e 100644 --- a/packages/frontend/editor-ui/src/components/VirtualSchema.vue +++ b/packages/frontend/editor-ui/src/components/VirtualSchema.vue @@ -105,21 +105,11 @@ const toggleNodeAndScrollTop = (id: string) => { const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) => { const pinData = workflowsStore.pinDataByNodeName(connectedNode.name); const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0]; - const data = - pinData ?? - connectedOutputIndexes - .map((outputIndex) => - executionDataToJson( - getNodeInputData( - fullNode, - props.runIndex, - outputIndex, - props.paneType, - props.connectionType, - ), - ), - ) - .flat(); + const nodeData = connectedOutputIndexes.map((outputIndex) => + getNodeInputData(fullNode, props.runIndex, outputIndex, props.paneType, props.connectionType), + ); + const hasBinary = nodeData.flat().some((data) => !isEmpty(data.binary)); + const data = pinData ?? nodeData.map(executionDataToJson).flat(); let schema = getSchemaForExecutionData(data); let preview = false; @@ -137,6 +127,7 @@ const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) = connectedOutputIndexes, itemsCount: data.length, preview, + hasBinary, }; }; @@ -251,7 +242,7 @@ const nodesSchemas = asyncComputed(async () => { const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion); if (!nodeType) continue; - const { schema, connectedOutputIndexes, itemsCount, preview } = await getNodeSchema( + const { schema, connectedOutputIndexes, itemsCount, preview, hasBinary } = await getNodeSchema( fullNode, node, ); @@ -268,6 +259,7 @@ const nodesSchemas = asyncComputed(async () => { nodeType, schema: filteredSchema, preview, + hasBinary, }); } diff --git a/packages/frontend/editor-ui/src/composables/useDataSchema.ts b/packages/frontend/editor-ui/src/composables/useDataSchema.ts index 44197c18f2..32420280a3 100644 --- a/packages/frontend/editor-ui/src/composables/useDataSchema.ts +++ b/packages/frontend/editor-ui/src/composables/useDataSchema.ts @@ -233,6 +233,7 @@ export type SchemaNode = { itemsCount: number; schema: Schema; preview: boolean; + hasBinary: boolean; }; export type RenderItem = { @@ -295,10 +296,12 @@ const icons = { 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()}`, icon: '', - value: useI18n().baseText('dataMapping.schemaView.emptyData'), + value: message, type: 'item', }); @@ -457,7 +460,12 @@ export const useFlattenSchema = () => { } 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; } diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json index bf512ebf80..2a61973243 100644 --- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json @@ -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.link": "JSON view", "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.noMatches": "No results for '{search}'", "dataMapping.schemaView.preview": "Usually outputs the following fields. Execute the node to see the actual ones. {link}",