Skip to content

Commit 35d0bd9

Browse files
authored
chore: extract node preparation into dedicated lib files (#3917)
## Summary - Extract `prepareTriggerNode`, `prepareCompositeNode`, `prepareComponentNode` from `index.tsx` into `lib/canvas-node-preparation.ts` - Extract annotation node preparation into `lib/canvas-annotation-node.ts` - Extract custom field rendering into `lib/render-workflow-node-custom-field.ts` - Add fallback node builders in `lib/canvas-node-fallback.ts` for graceful degradation - Rename `workflow-groups` to `canvas-groups` and add `prepareGroupNode` + `wireGroupParentChildRelationships` - Extract `CANVAS_BUNDLE_ICON_SLUG`/`CANVAS_BUNDLE_COLOR` constants into `lib/canvas-bundle.ts` - Net reduction of ~420 lines from `index.tsx` ## Context Split from #3899. This is PR 2/3, depends on #3916. Restructures the data preparation layer without changing behavior. --------- Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
1 parent 7eb916e commit 35d0bd9

File tree

9 files changed

+901
-443
lines changed

9 files changed

+901
-443
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { QueryClient } from "@tanstack/react-query";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import type { ComponentsComponent, ComponentsNode } from "@/api-client";
4+
import type { CustomFieldRenderer } from "./mappers/types";
5+
import * as mappers from "./mappers";
6+
import { createSafeCustomFieldRenderer } from "./mappers/safeMappers";
7+
import { prepareComponentBaseNode, prepareTriggerNode } from "./lib/canvas-node-preparation";
8+
import { renderCanvasNodeCustomField } from "./lib/render-canvas-node-custom-field";
9+
10+
type FallbackComponentData = {
11+
renderFallback?: {
12+
source: string;
13+
message: string;
14+
};
15+
component: {
16+
error?: string;
17+
emptyStateProps?: {
18+
title?: string;
19+
};
20+
};
21+
};
22+
23+
function makeNode(overrides: Partial<ComponentsNode> = {}): ComponentsNode {
24+
return {
25+
id: "node-1",
26+
name: "Broken Component",
27+
type: "TYPE_COMPONENT",
28+
position: { x: 10, y: 20 },
29+
component: {
30+
name: "approval",
31+
},
32+
configuration: {},
33+
...overrides,
34+
} as ComponentsNode;
35+
}
36+
37+
function makeComponent(overrides: Partial<ComponentsComponent> = {}): ComponentsComponent {
38+
return {
39+
name: "approval",
40+
label: "Approval",
41+
icon: "hand",
42+
color: "orange",
43+
outputChannels: [{ name: "default" }],
44+
...overrides,
45+
} as ComponentsComponent;
46+
}
47+
48+
function makeTriggerNode(overrides: Partial<ComponentsNode> = {}): ComponentsNode {
49+
return {
50+
id: "trigger-1",
51+
name: "Incoming Event",
52+
type: "TYPE_TRIGGER",
53+
position: { x: 0, y: 0 },
54+
trigger: {
55+
name: "webhook",
56+
},
57+
configuration: {},
58+
...overrides,
59+
} as ComponentsNode;
60+
}
61+
62+
describe("canvas node preparation resilience", () => {
63+
beforeEach(() => {
64+
vi.restoreAllMocks();
65+
});
66+
67+
it("returns a fallback canvas node when component preparation fails", () => {
68+
vi.spyOn(mappers, "getComponentAdditionalDataBuilder").mockReturnValue({
69+
buildAdditionalData: () => {
70+
throw new Error("builder failed");
71+
},
72+
});
73+
vi.spyOn(mappers, "getComponentBaseMapper").mockReturnValue({
74+
props: () => {
75+
throw new Error("mapper failed");
76+
},
77+
subtitle: () => "",
78+
getExecutionDetails: () => ({}),
79+
});
80+
81+
const result = prepareComponentBaseNode({
82+
nodes: [makeNode()],
83+
node: makeNode(),
84+
components: [makeComponent()],
85+
nodeExecutionsMap: {},
86+
nodeQueueItemsMap: {},
87+
canvasId: "canvas-1",
88+
queryClient: new QueryClient(),
89+
organizationId: "org-1",
90+
});
91+
92+
const fallbackData = result.data as unknown as FallbackComponentData;
93+
94+
expect(fallbackData.renderFallback).toEqual({
95+
source: "mapper",
96+
message: "Can't display",
97+
});
98+
expect(fallbackData.component.error).toBeUndefined();
99+
expect(fallbackData.component.emptyStateProps?.title).toBe("Can't display");
100+
});
101+
102+
it("returns null when a custom field renderer throws so sidebar rendering stays alive", () => {
103+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
104+
const renderer = createSafeCustomFieldRenderer(
105+
{
106+
render: () => {
107+
throw new Error("custom field failed");
108+
},
109+
} satisfies CustomFieldRenderer,
110+
"approval",
111+
);
112+
113+
const result = renderCanvasNodeCustomField({
114+
renderer,
115+
node: makeNode(),
116+
});
117+
118+
expect(result).toBeNull();
119+
expect(consoleSpy).toHaveBeenCalledWith(
120+
expect.stringContaining('Custom field renderer "approval" threw in render()'),
121+
expect.any(Error),
122+
);
123+
consoleSpy.mockRestore();
124+
});
125+
126+
it("keeps trigger error and warning precedence on node state only", () => {
127+
vi.spyOn(mappers, "getTriggerRenderer").mockReturnValue({
128+
getTriggerProps: () => ({
129+
title: "Webhook",
130+
iconSlug: "bolt",
131+
metadata: [],
132+
error: "renderer error",
133+
warning: "renderer warning",
134+
}),
135+
getRootEventValues: () => ({}),
136+
getTitleAndSubtitle: () => ({ title: "Event", subtitle: "" }),
137+
});
138+
139+
const result = prepareTriggerNode(
140+
makeTriggerNode(),
141+
[{ name: "webhook", label: "Webhook", icon: "bolt" }] as never,
142+
{},
143+
);
144+
145+
const triggerData = result.data as { trigger: { error?: string; warning?: string } };
146+
147+
expect(triggerData.trigger.error).toBeUndefined();
148+
expect(triggerData.trigger.warning).toBeUndefined();
149+
});
150+
});

0 commit comments

Comments
 (0)