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));
+ });
});