diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/schema.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/schema.svg index 41e2702675..f9f657519e 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/schema.svg +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/schema.svg @@ -1 +1,6 @@ - + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts index 18eff4e3c9..ea0ae92de5 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts @@ -3,7 +3,6 @@ import BoltFilled from './custom/bolt-filled.svg'; import Continue from './custom/Continue.svg'; import EmptyOutput from './custom/EmptyOutput.svg'; import GripLinesVertical from './custom/grip-lines-vertical.svg'; -import Json from './custom/json.svg'; import PopOut from './custom/pop-out.svg'; import Retry from './custom/Retry.svg'; import RunOnce from './custom/RunOnce.svg'; @@ -36,6 +35,7 @@ import IconLucideBell from '~icons/lucide/bell'; import IconLucideBook from '~icons/lucide/book'; import IconLucideBot from '~icons/lucide/bot'; import IconLucideBox from '~icons/lucide/box'; +import IconLucideBraces from '~icons/lucide/braces'; import IconLucideBrain from '~icons/lucide/brain'; import IconLucideBug from '~icons/lucide/bug'; import IconLucideCalculator from '~icons/lucide/calculator'; @@ -208,7 +208,7 @@ export const deprecatedIconSet = { 'status-warning': StatusWarning, 'vector-square': VectorSquare, schema: Schema, - json: Json, + json: IconLucideBraces, binary: Binary, text: Text, toolbox: Toolbox, @@ -415,7 +415,7 @@ export const updatedIconSet = { 'retry-on-fail': Retry, 'execute-once': RunOnce, schema: Schema, - json: Json, + json: IconLucideBraces, binary: Binary, text: Text, toolbox: Toolbox, diff --git a/packages/frontend/editor-ui/src/__tests__/mocks.ts b/packages/frontend/editor-ui/src/__tests__/mocks.ts index abff8d36aa..85f8719fa4 100644 --- a/packages/frontend/editor-ui/src/__tests__/mocks.ts +++ b/packages/frontend/editor-ui/src/__tests__/mocks.ts @@ -130,14 +130,18 @@ export const defaultNodeDescriptions = Object.values(defaultNodeTypes).map( ({ type }) => type.description, ) as INodeTypeDescription[]; -const nodeTypes = mock({ - getByName(nodeType) { - return defaultNodeTypes[nodeType].type; - }, - getByNameAndVersion(nodeType: string, version?: number): INodeType { - return NodeHelpers.getVersionedNodeType(defaultNodeTypes[nodeType].type, version); - }, -}); +export function createMockNodeTypes(data: INodeTypeData) { + return mock({ + getByName(nodeType) { + return data[nodeType].type; + }, + getByNameAndVersion(nodeType: string, version?: number): INodeType { + return NodeHelpers.getVersionedNodeType(data[nodeType].type, version); + }, + }); +} + +const nodeTypes = createMockNodeTypes(defaultNodeTypes); export function createTestWorkflowObject({ id = uuid(), @@ -148,6 +152,7 @@ export function createTestWorkflowObject({ staticData = {}, settings = {}, pinData = {}, + ...rest }: { id?: string; name?: string; @@ -157,6 +162,7 @@ export function createTestWorkflowObject({ staticData?: IDataObject; settings?: IWorkflowSettings; pinData?: IPinData; + nodeTypes?: INodeTypes; } = {}) { return new Workflow({ id, @@ -167,7 +173,7 @@ export function createTestWorkflowObject({ staticData, settings, pinData, - nodeTypes, + nodeTypes: rest.nodeTypes ?? nodeTypes, }); } diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index 5aae98112d..b168727df1 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -112,3 +112,8 @@ Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { writable: true, value: vi.fn(), }); + +Object.defineProperty(HTMLElement.prototype, 'scrollTo', { + writable: true, + value: vi.fn(), +}); diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index 83e3b701d3..23586185a4 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -721,7 +721,6 @@ onMounted(() => { }); if (props.paneType === 'output') { - setDisplayMode(); activatePane(); } @@ -1231,6 +1230,10 @@ function init() { if (isNDVV2.value) { pageSize.value = RUN_DATA_DEFAULT_PAGE_SIZE; } + + if (props.paneType === 'output') { + setDisplayMode(); + } } function closeBinaryDataDisplay() { @@ -1337,14 +1340,14 @@ function enableNode() { } } +const shouldDisplayHtml = computed( + () => + node.value?.type === HTML_NODE_TYPE && + node.value.parameters.operation === 'generateHtmlTemplate', +); + function setDisplayMode() { - if (!activeNode.value) return; - - const shouldDisplayHtml = - activeNode.value.type === HTML_NODE_TYPE && - activeNode.value.parameters.operation === 'generateHtmlTemplate'; - - if (shouldDisplayHtml) { + if (shouldDisplayHtml.value) { emit('displayModeChange', 'html'); } } @@ -1461,10 +1464,7 @@ defineExpose({ enterEditMode }); :value="displayMode" :has-binary-data="binaryData.length > 0" :pane-type="paneType" - :node-generates-html=" - activeNode?.type === HTML_NODE_TYPE && - activeNode.parameters.operation === 'generateHtmlTemplate' - " + :node-generates-html="shouldDisplayHtml" :has-renderable-data="hasParsedAiContent" @change="onDisplayModeChange" /> diff --git a/packages/frontend/editor-ui/src/features/logs/components/LogDetailsPanel.test.ts b/packages/frontend/editor-ui/src/features/logs/components/LogDetailsPanel.test.ts index 2dd5ebc254..e566ee5d5e 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/LogDetailsPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/components/LogDetailsPanel.test.ts @@ -1,19 +1,23 @@ -import { fireEvent, within } from '@testing-library/vue'; +import { fireEvent, waitFor, within } from '@testing-library/vue'; import { renderComponent } from '@/__tests__/render'; import LogDetailsPanel from './LogDetailsPanel.vue'; import { createRouter, createWebHistory } from 'vue-router'; import { createTestingPinia, type TestingPinia } from '@pinia/testing'; import { h } from 'vue'; import { + createMockNodeTypes, createTestNode, createTestTaskData, createTestWorkflow, createTestWorkflowObject, + defaultNodeTypes, + mockLoadedNodeType, } from '@/__tests__/mocks'; import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants'; import type { LogEntry } from '../logs.types'; import { createTestLogEntry } from '../__test__/mocks'; import { NodeConnectionTypes } from 'n8n-workflow'; +import { HTML_NODE_TYPE } from '@/constants'; describe('LogDetailsPanel', () => { let pinia: TestingPinia; @@ -182,4 +186,47 @@ describe('LogDetailsPanel', () => { ), ).toBeInTheDocument(); }); + + it('should render output data in HTML mode for HTML node', async () => { + const nodeA = createTestNode({ name: 'A' }); + const nodeB = createTestNode({ + name: 'B', + type: HTML_NODE_TYPE, + }); + const runDataA = createTestTaskData({ data: { [NodeConnectionTypes.Main]: [[{ json: {} }]] } }); + const runDataB = createTestTaskData({ + data: { [NodeConnectionTypes.Main]: [[{ json: { html: '

Hi!

' } }]] }, + source: [{ previousNode: 'A' }], + }); + const workflow = createTestWorkflowObject({ + nodes: [nodeA, nodeB], + nodeTypes: createMockNodeTypes({ + ...defaultNodeTypes, + [HTML_NODE_TYPE]: mockLoadedNodeType(HTML_NODE_TYPE), + }), + }); + const execution = { resultData: { runData: { A: [runDataA], B: [runDataB] } } }; + const logA = createLogEntry({ node: nodeA, runData: runDataA, workflow, execution }); + const logB = createLogEntry({ node: nodeB, runData: runDataB, workflow, execution }); + + // HACK: Setting parameters after creating workflow because validation removes parameters that are not define in node types. + nodeB.parameters = { operation: 'generateHtmlTemplate' }; + + const props = { + isOpen: true, + panels: LOG_DETAILS_PANEL_STATE.BOTH, + collapsingInputTableColumnName: null, + collapsingOutputTableColumnName: null, + }; + + const rendered = render({ ...props, logEntry: logB }); + + await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1)); + await rendered.rerender({ ...props, logEntry: logA }); + await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(0)); + + // Re-selecting node B should render HTML again + await rendered.rerender({ ...props, logEntry: logB }); + await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1)); + }); });