@startsimpli/ui 0.4.14 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +457 -398
- package/package.json +18 -13
- package/src/components/__tests__/calendar-view-popup.test.tsx +42 -0
- package/src/components/__tests__/chat.test.tsx +129 -0
- package/src/components/__tests__/meetings-list.test.tsx +114 -0
- package/src/components/__tests__/slide-deck-viewer.test.tsx +82 -0
- package/src/components/__tests__/workspace.test.tsx +106 -0
- package/src/components/account/__tests__/account.test.tsx +5 -32
- package/src/components/account/change-password-form.tsx +1 -28
- package/src/components/calendar/calendar-view.tsx +31 -0
- package/src/components/calendar/index.ts +7 -0
- package/src/components/calendar/meetings-list.tsx +202 -0
- package/src/components/calendar/upcoming-meetings.tsx +5 -5
- package/src/components/chat/ChatComposer.tsx +113 -0
- package/src/components/chat/ChatMessage.tsx +81 -0
- package/src/components/chat/ChatThread.tsx +57 -0
- package/src/components/chat/index.ts +12 -0
- package/src/components/chat/types.ts +20 -0
- package/src/components/index.ts +13 -0
- package/src/components/slide-deck/SlideCanvas.tsx +68 -0
- package/src/components/slide-deck/SlideDeckViewer.tsx +144 -0
- package/src/components/slide-deck/SlideFilmstrip.tsx +73 -0
- package/src/components/slide-deck/index.ts +7 -0
- package/src/components/slide-deck/types.ts +18 -0
- package/src/components/team/DomainClaimCard.tsx +170 -0
- package/src/components/team/InviteMemberDialog.tsx +182 -0
- package/src/components/team/LeaveTeamDialog.tsx +130 -0
- package/src/components/team/MembersTable.tsx +138 -0
- package/src/components/team/OrgSwitcher.tsx +68 -0
- package/src/components/team/PendingInvitationCallout.tsx +106 -0
- package/src/components/team/RoleSelector.tsx +68 -0
- package/src/components/team/__tests__/team-components.test.tsx +352 -0
- package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
- package/src/components/team/index.ts +57 -0
- package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
- package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
- package/src/components/team/members-table-default-class-names.ts +39 -0
- package/src/components/team/org-switcher-default-class-names.ts +13 -0
- package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
- package/src/components/team/role-selector-default-class-names.ts +11 -0
- package/src/components/team/types.ts +97 -0
- package/src/components/workflows/ExecNodeDetails.tsx +83 -0
- package/src/components/workflows/ExecutionTimeline.tsx +146 -0
- package/src/components/workflows/NodeInspector.tsx +257 -0
- package/src/components/workflows/NodePalette.tsx +119 -0
- package/src/components/workflows/WorkflowCanvas.tsx +113 -0
- package/src/components/workflows/WorkflowEdge.tsx +65 -0
- package/src/components/workflows/WorkflowEditor.tsx +130 -0
- package/src/components/workflows/WorkflowNode.tsx +198 -0
- package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
- package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
- package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
- package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
- package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
- package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
- package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
- package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
- package/src/components/workflows/__tests__/serialization.test.ts +278 -0
- package/src/components/workflows/exec-status.ts +90 -0
- package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
- package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
- package/src/components/workflows/index.ts +78 -0
- package/src/components/workflows/layout/auto-layout.ts +142 -0
- package/src/components/workflows/node-icons.ts +31 -0
- package/src/components/workflows/serialization.ts +171 -0
- package/src/components/workflows/theme/categories.ts +96 -0
- package/src/components/workflows/types.ts +231 -0
- package/src/components/workflows/workflows.css +29 -0
- package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
- package/src/components/workspace/SplitPane.tsx +174 -0
- package/src/components/workspace/index.ts +4 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dagre-based auto-layout for workflow flow graphs.
|
|
3
|
+
*
|
|
4
|
+
* Wraps dagre with LR (left-to-right) and TB (top-to-bottom) presets,
|
|
5
|
+
* assigning non-overlapping positions to xyflow nodes. Nodes with explicit
|
|
6
|
+
* positions can be preserved (`preservePositions`), and edgeless / orphan
|
|
7
|
+
* nodes are laid out by dagre alongside connected ones (dagre places
|
|
8
|
+
* disconnected components without overlap).
|
|
9
|
+
*
|
|
10
|
+
* Pure-TS — no React. Operates on the structural FlowNode/FlowEdge shapes
|
|
11
|
+
* from serialization.ts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import dagre from 'dagre';
|
|
15
|
+
|
|
16
|
+
import type { FlowNode, FlowEdge } from '../serialization';
|
|
17
|
+
|
|
18
|
+
export type LayoutDirection = 'LR' | 'TB';
|
|
19
|
+
|
|
20
|
+
export interface AutoLayoutOptions {
|
|
21
|
+
/** Layout direction. Defaults to 'TB'. */
|
|
22
|
+
direction?: LayoutDirection;
|
|
23
|
+
/** Default node width (px) for layout sizing. */
|
|
24
|
+
nodeWidth?: number;
|
|
25
|
+
/** Default node height (px) for layout sizing. */
|
|
26
|
+
nodeHeight?: number;
|
|
27
|
+
/** Horizontal spacing between ranks/nodes (px). */
|
|
28
|
+
rankSep?: number;
|
|
29
|
+
/** Spacing between nodes in the same rank (px). */
|
|
30
|
+
nodeSep?: number;
|
|
31
|
+
/**
|
|
32
|
+
* When true, nodes whose source data already has an explicit position
|
|
33
|
+
* keep it; only positionless nodes are laid out by dagre.
|
|
34
|
+
*/
|
|
35
|
+
preservePositions?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_NODE_WIDTH = 180;
|
|
39
|
+
const DEFAULT_NODE_HEIGHT = 60;
|
|
40
|
+
const DEFAULT_RANK_SEP = 80;
|
|
41
|
+
const DEFAULT_NODE_SEP = 50;
|
|
42
|
+
|
|
43
|
+
/** Whether this flow node carries an explicit (user-set) position. */
|
|
44
|
+
function hasExplicitPosition(node: FlowNode): boolean {
|
|
45
|
+
return Boolean(node.data?.node?.position);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute non-overlapping positions for the given nodes/edges.
|
|
50
|
+
*
|
|
51
|
+
* Returns new node objects with updated `position`; edges are returned
|
|
52
|
+
* unchanged. The original arrays are not mutated.
|
|
53
|
+
*/
|
|
54
|
+
export function autoLayout(
|
|
55
|
+
nodes: FlowNode[],
|
|
56
|
+
edges: FlowEdge[],
|
|
57
|
+
options: AutoLayoutOptions = {}
|
|
58
|
+
): { nodes: FlowNode[]; edges: FlowEdge[] } {
|
|
59
|
+
const {
|
|
60
|
+
direction = 'TB',
|
|
61
|
+
nodeWidth = DEFAULT_NODE_WIDTH,
|
|
62
|
+
nodeHeight = DEFAULT_NODE_HEIGHT,
|
|
63
|
+
rankSep = DEFAULT_RANK_SEP,
|
|
64
|
+
nodeSep = DEFAULT_NODE_SEP,
|
|
65
|
+
preservePositions = false,
|
|
66
|
+
} = options;
|
|
67
|
+
|
|
68
|
+
if (nodes.length === 0) {
|
|
69
|
+
return { nodes: [], edges };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const g = new dagre.graphlib.Graph();
|
|
73
|
+
g.setGraph({
|
|
74
|
+
rankdir: direction,
|
|
75
|
+
ranksep: rankSep,
|
|
76
|
+
nodesep: nodeSep,
|
|
77
|
+
marginx: 20,
|
|
78
|
+
marginy: 20,
|
|
79
|
+
});
|
|
80
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
81
|
+
|
|
82
|
+
const pinned = new Set<string>();
|
|
83
|
+
|
|
84
|
+
for (const node of nodes) {
|
|
85
|
+
const w = node.width ?? nodeWidth;
|
|
86
|
+
const h = node.height ?? nodeHeight;
|
|
87
|
+
g.setNode(node.id, { width: w, height: h });
|
|
88
|
+
if (preservePositions && hasExplicitPosition(node)) {
|
|
89
|
+
pinned.add(node.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const edge of edges) {
|
|
94
|
+
// Only add edges whose endpoints are present (defensive).
|
|
95
|
+
if (g.hasNode(edge.source) && g.hasNode(edge.target)) {
|
|
96
|
+
g.setEdge(edge.source, edge.target);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
dagre.layout(g);
|
|
101
|
+
|
|
102
|
+
const laidNodes: FlowNode[] = nodes.map((node) => {
|
|
103
|
+
if (pinned.has(node.id)) {
|
|
104
|
+
// Keep the explicit position verbatim.
|
|
105
|
+
return {
|
|
106
|
+
...node,
|
|
107
|
+
position: { ...(node.data.node.position as { x: number; y: number }) },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const pos = g.node(node.id);
|
|
111
|
+
const w = node.width ?? nodeWidth;
|
|
112
|
+
const h = node.height ?? nodeHeight;
|
|
113
|
+
// dagre returns center coords; convert to top-left for xyflow.
|
|
114
|
+
return {
|
|
115
|
+
...node,
|
|
116
|
+
position: {
|
|
117
|
+
x: (pos?.x ?? 0) - w / 2,
|
|
118
|
+
y: (pos?.y ?? 0) - h / 2,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { nodes: laidNodes, edges };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Convenience: left-to-right layout. */
|
|
127
|
+
export function layoutLR(
|
|
128
|
+
nodes: FlowNode[],
|
|
129
|
+
edges: FlowEdge[],
|
|
130
|
+
options: Omit<AutoLayoutOptions, 'direction'> = {}
|
|
131
|
+
) {
|
|
132
|
+
return autoLayout(nodes, edges, { ...options, direction: 'LR' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Convenience: top-to-bottom layout. */
|
|
136
|
+
export function layoutTB(
|
|
137
|
+
nodes: FlowNode[],
|
|
138
|
+
edges: FlowEdge[],
|
|
139
|
+
options: Omit<AutoLayoutOptions, 'direction'> = {}
|
|
140
|
+
) {
|
|
141
|
+
return autoLayout(nodes, edges, { ...options, direction: 'TB' });
|
|
142
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a category token's icon hint (a lucide-react icon name) to the actual
|
|
3
|
+
* lucide component. Kept separate from theme/categories.ts so that module
|
|
4
|
+
* stays pure-TS (no React/lucide import) while the editor can still resolve a
|
|
5
|
+
* real icon component. Unknown names fall back to `Box`.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Box,
|
|
9
|
+
GitBranch,
|
|
10
|
+
Play,
|
|
11
|
+
Settings2,
|
|
12
|
+
Shuffle,
|
|
13
|
+
Workflow,
|
|
14
|
+
Zap,
|
|
15
|
+
type LucideIcon,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
19
|
+
Box,
|
|
20
|
+
GitBranch,
|
|
21
|
+
Play,
|
|
22
|
+
Settings2,
|
|
23
|
+
Shuffle,
|
|
24
|
+
Workflow,
|
|
25
|
+
Zap,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function getCategoryIcon(iconName: string | undefined): LucideIcon {
|
|
29
|
+
if (!iconName) return Box;
|
|
30
|
+
return ICONS[iconName] ?? Box;
|
|
31
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow graph <-> xyflow conversion.
|
|
3
|
+
*
|
|
4
|
+
* `graphToFlow` turns a {@link WorkflowGraph} (flat nodes + edges) into the
|
|
5
|
+
* xyflow `{ nodes, edges }` shape an editor renders.
|
|
6
|
+
*
|
|
7
|
+
* `flowToGraph` turns xyflow nodes/edges back into `{ nodes, connections }`
|
|
8
|
+
* where `connections` is the LOCKED backend n8n shape:
|
|
9
|
+
*
|
|
10
|
+
* ```json
|
|
11
|
+
* { "sourceId": { "main": [[{ "node": "targetId", "type": "main", "index": 0 }]] } }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Round-trip guarantee: `flowToGraph(graphToFlow(g)).connections` equals the
|
|
15
|
+
* direct serialization of `g`'s edges into that shape. The xyflow
|
|
16
|
+
* `sourceHandle` encodes the source output index and `targetHandle` encodes
|
|
17
|
+
* the target input index, so multi-output (if/else), parallel fan-out, and
|
|
18
|
+
* non-zero target inputs all survive the round-trip.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
WorkflowGraph,
|
|
23
|
+
WfNode,
|
|
24
|
+
WfEdge,
|
|
25
|
+
BackendConnection,
|
|
26
|
+
BackendConnections,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
// ── xyflow shapes (structurally typed; we only depend on the fields used) ──
|
|
30
|
+
|
|
31
|
+
/** Minimal xyflow Node shape produced/consumed here. */
|
|
32
|
+
export interface FlowNode {
|
|
33
|
+
id: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
position: { x: number; y: number };
|
|
36
|
+
data: { node: WfNode } & Record<string, unknown>;
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Minimal xyflow Edge shape produced/consumed here. */
|
|
42
|
+
export interface FlowEdge {
|
|
43
|
+
id: string;
|
|
44
|
+
source: string;
|
|
45
|
+
target: string;
|
|
46
|
+
sourceHandle?: string | null;
|
|
47
|
+
targetHandle?: string | null;
|
|
48
|
+
type?: string;
|
|
49
|
+
data?: { connectionType: string } & Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_CONNECTION_TYPE = 'main';
|
|
53
|
+
|
|
54
|
+
/** Stable edge id encoding source/output -> target/input. */
|
|
55
|
+
export function edgeId(
|
|
56
|
+
source: string,
|
|
57
|
+
sourceOutput: number,
|
|
58
|
+
target: string,
|
|
59
|
+
targetInput: number
|
|
60
|
+
): string {
|
|
61
|
+
return `${source}:${sourceOutput}->${target}:${targetInput}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert a {@link WorkflowGraph} into xyflow `{ nodes, edges }`.
|
|
66
|
+
*
|
|
67
|
+
* Node positions: explicit `node.position` is preserved; otherwise a
|
|
68
|
+
* placeholder `{ x: 0, y: 0 }` is assigned (run auto-layout for real
|
|
69
|
+
* coordinates).
|
|
70
|
+
*/
|
|
71
|
+
export function graphToFlow(graph: WorkflowGraph): {
|
|
72
|
+
nodes: FlowNode[];
|
|
73
|
+
edges: FlowEdge[];
|
|
74
|
+
} {
|
|
75
|
+
const nodes: FlowNode[] = graph.nodes.map((node) => ({
|
|
76
|
+
id: node.id,
|
|
77
|
+
type: node.type,
|
|
78
|
+
position: node.position ?? { x: 0, y: 0 },
|
|
79
|
+
data: { node },
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const edges: FlowEdge[] = graph.edges.map((edge) => {
|
|
83
|
+
const sourceOutput = edge.sourceOutput ?? 0;
|
|
84
|
+
const targetInput = edge.targetInput ?? 0;
|
|
85
|
+
const connectionType = edge.type ?? DEFAULT_CONNECTION_TYPE;
|
|
86
|
+
return {
|
|
87
|
+
id: edgeId(edge.source, sourceOutput, edge.target, targetInput),
|
|
88
|
+
source: edge.source,
|
|
89
|
+
target: edge.target,
|
|
90
|
+
sourceHandle: String(sourceOutput),
|
|
91
|
+
targetHandle: String(targetInput),
|
|
92
|
+
type: 'default',
|
|
93
|
+
data: { connectionType },
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { nodes, edges };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parse a handle id back to its numeric index (null/undefined -> 0). */
|
|
101
|
+
function handleIndex(handle: string | null | undefined): number {
|
|
102
|
+
if (handle === null || handle === undefined || handle === '') return 0;
|
|
103
|
+
const n = Number.parseInt(handle, 10);
|
|
104
|
+
return Number.isNaN(n) ? 0 : n;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Convert xyflow nodes/edges back into `{ nodes, connections }`.
|
|
109
|
+
*
|
|
110
|
+
* `connections` is the LOCKED backend n8n shape. The `main` (or other
|
|
111
|
+
* connection type) array is positional by source output index — earlier
|
|
112
|
+
* unconnected outputs are back-filled with empty arrays so a connection on
|
|
113
|
+
* output index 1 yields `[[], [...]]`.
|
|
114
|
+
*/
|
|
115
|
+
export function flowToGraph(
|
|
116
|
+
nodes: FlowNode[],
|
|
117
|
+
edges: FlowEdge[]
|
|
118
|
+
): { nodes: WfNode[]; connections: BackendConnections } {
|
|
119
|
+
const outNodes: WfNode[] = nodes.map((n) => n.data.node);
|
|
120
|
+
|
|
121
|
+
const connections: BackendConnections = {};
|
|
122
|
+
|
|
123
|
+
for (const edge of edges) {
|
|
124
|
+
const connectionType =
|
|
125
|
+
edge.data?.connectionType ?? edge.type ?? DEFAULT_CONNECTION_TYPE;
|
|
126
|
+
const sourceOutput = handleIndex(edge.sourceHandle);
|
|
127
|
+
const targetInput = handleIndex(edge.targetHandle);
|
|
128
|
+
|
|
129
|
+
const sourceMap = (connections[edge.source] ??= {});
|
|
130
|
+
const outputLists = (sourceMap[connectionType] ??= []);
|
|
131
|
+
|
|
132
|
+
// Back-fill positional output slots up to sourceOutput.
|
|
133
|
+
while (outputLists.length <= sourceOutput) {
|
|
134
|
+
outputLists.push([]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const conn: BackendConnection = {
|
|
138
|
+
node: edge.target,
|
|
139
|
+
type: connectionType,
|
|
140
|
+
index: targetInput,
|
|
141
|
+
};
|
|
142
|
+
outputLists[sourceOutput].push(conn);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { nodes: outNodes, connections };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Hydrate flat {@link WfEdge}s from the LOCKED backend connections shape.
|
|
150
|
+
* Inverse of the connections half of {@link flowToGraph}; useful when
|
|
151
|
+
* loading a workflow from the API into editor state.
|
|
152
|
+
*/
|
|
153
|
+
export function connectionsToEdges(connections: BackendConnections): WfEdge[] {
|
|
154
|
+
const edges: WfEdge[] = [];
|
|
155
|
+
for (const [source, outputs] of Object.entries(connections)) {
|
|
156
|
+
for (const [type, outputLists] of Object.entries(outputs)) {
|
|
157
|
+
outputLists.forEach((conns, sourceOutput) => {
|
|
158
|
+
for (const conn of conns) {
|
|
159
|
+
edges.push({
|
|
160
|
+
source,
|
|
161
|
+
target: conn.node,
|
|
162
|
+
sourceOutput,
|
|
163
|
+
targetInput: conn.index,
|
|
164
|
+
type,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return edges;
|
|
171
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic category → visual token map.
|
|
3
|
+
*
|
|
4
|
+
* Maps a node-type category (free string, e.g. the backend
|
|
5
|
+
* NodeType.Category choices) to a small token bundle the renderer can use.
|
|
6
|
+
* Tokens are framework-agnostic (CSS custom-property friendly hex + a
|
|
7
|
+
* Tailwind-ish class hint) so this stays in pure-TS land — no React here.
|
|
8
|
+
*
|
|
9
|
+
* Categories mirror the backend NodeType.Category text choices but the
|
|
10
|
+
* map is open: unknown categories resolve to DEFAULT_CATEGORY_TOKEN.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface CategoryToken {
|
|
14
|
+
/** Stable category key. */
|
|
15
|
+
category: string;
|
|
16
|
+
/** Human label. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Accent color (hex) for borders/handles. */
|
|
19
|
+
color: string;
|
|
20
|
+
/** Background tint (hex). */
|
|
21
|
+
background: string;
|
|
22
|
+
/** Foreground/text color (hex). */
|
|
23
|
+
foreground: string;
|
|
24
|
+
/** Icon hint (lucide-react icon name) — consumed by a later React epic. */
|
|
25
|
+
icon: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fallback token for unknown categories. */
|
|
29
|
+
export const DEFAULT_CATEGORY_TOKEN: CategoryToken = {
|
|
30
|
+
category: 'default',
|
|
31
|
+
label: 'Node',
|
|
32
|
+
color: '#64748b', // slate-500
|
|
33
|
+
background: '#f1f5f9', // slate-100
|
|
34
|
+
foreground: '#0f172a', // slate-900
|
|
35
|
+
icon: 'Box',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Built-in category tokens. Keys match backend NodeType.Category values.
|
|
40
|
+
*/
|
|
41
|
+
export const CATEGORY_TOKENS: Record<string, CategoryToken> = {
|
|
42
|
+
trigger: {
|
|
43
|
+
category: 'trigger',
|
|
44
|
+
label: 'Trigger',
|
|
45
|
+
color: '#16a34a', // green-600
|
|
46
|
+
background: '#dcfce7', // green-100
|
|
47
|
+
foreground: '#14532d', // green-900
|
|
48
|
+
icon: 'Zap',
|
|
49
|
+
},
|
|
50
|
+
action: {
|
|
51
|
+
category: 'action',
|
|
52
|
+
label: 'Action',
|
|
53
|
+
color: '#2563eb', // blue-600
|
|
54
|
+
background: '#dbeafe', // blue-100
|
|
55
|
+
foreground: '#1e3a8a', // blue-900
|
|
56
|
+
icon: 'Play',
|
|
57
|
+
},
|
|
58
|
+
condition: {
|
|
59
|
+
category: 'condition',
|
|
60
|
+
label: 'Condition',
|
|
61
|
+
color: '#d97706', // amber-600
|
|
62
|
+
background: '#fef3c7', // amber-100
|
|
63
|
+
foreground: '#78350f', // amber-900
|
|
64
|
+
icon: 'GitBranch',
|
|
65
|
+
},
|
|
66
|
+
transform: {
|
|
67
|
+
category: 'transform',
|
|
68
|
+
label: 'Transform',
|
|
69
|
+
color: '#7c3aed', // violet-600
|
|
70
|
+
background: '#ede9fe', // violet-100
|
|
71
|
+
foreground: '#4c1d95', // violet-900
|
|
72
|
+
icon: 'Shuffle',
|
|
73
|
+
},
|
|
74
|
+
subworkflow: {
|
|
75
|
+
category: 'subworkflow',
|
|
76
|
+
label: 'Sub-workflow',
|
|
77
|
+
color: '#0891b2', // cyan-600
|
|
78
|
+
background: '#cffafe', // cyan-100
|
|
79
|
+
foreground: '#164e63', // cyan-900
|
|
80
|
+
icon: 'Workflow',
|
|
81
|
+
},
|
|
82
|
+
control: {
|
|
83
|
+
category: 'control',
|
|
84
|
+
label: 'Control',
|
|
85
|
+
color: '#db2777', // pink-600
|
|
86
|
+
background: '#fce7f3', // pink-100
|
|
87
|
+
foreground: '#831843', // pink-900
|
|
88
|
+
icon: 'Settings2',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Resolve a category token, falling back to the default. */
|
|
93
|
+
export function getCategoryToken(category: string | undefined): CategoryToken {
|
|
94
|
+
if (!category) return DEFAULT_CATEGORY_TOKEN;
|
|
95
|
+
return CATEGORY_TOKENS[category] ?? DEFAULT_CATEGORY_TOKEN;
|
|
96
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic workflow-graph types — mirrors the StartSimpli backend
|
|
3
|
+
* (apps.workflows) n8n-style JSON graph definition.
|
|
4
|
+
*
|
|
5
|
+
* The graph is "behavior as data": a directed graph of typed nodes
|
|
6
|
+
* connected by edges. The shapes here are intentionally generic — node
|
|
7
|
+
* `type` is just a slug and `category` is a free string token resolved
|
|
8
|
+
* against the category theme map (theme/categories.ts).
|
|
9
|
+
*
|
|
10
|
+
* NOTE: `connections` is the LOCKED backend persistence format. See
|
|
11
|
+
* serialization.ts for the canonical shape and round-trip guarantees.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Status enums (match backend TextChoices verbatim) ──
|
|
15
|
+
|
|
16
|
+
/** NodeExecution.Status — per-node runtime status (+ "idle" for editor). */
|
|
17
|
+
export type NodeStatus =
|
|
18
|
+
| 'idle'
|
|
19
|
+
| 'pending'
|
|
20
|
+
| 'running'
|
|
21
|
+
| 'success'
|
|
22
|
+
| 'error'
|
|
23
|
+
| 'skipped';
|
|
24
|
+
|
|
25
|
+
/** WorkflowExecution.Status — overall run status. */
|
|
26
|
+
export type RunStatus =
|
|
27
|
+
| 'pending'
|
|
28
|
+
| 'running'
|
|
29
|
+
| 'completed'
|
|
30
|
+
| 'failed'
|
|
31
|
+
| 'cancelled'
|
|
32
|
+
| 'waiting';
|
|
33
|
+
|
|
34
|
+
// ── Graph definition ──
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A single node in a workflow graph.
|
|
38
|
+
*
|
|
39
|
+
* Matches a backend `Workflow.nodes[]` entry:
|
|
40
|
+
* `{ id, name, type, parameters, ... }`. Extra backend fields
|
|
41
|
+
* (`disabled`, `continue_on_fail`) are preserved opaquely.
|
|
42
|
+
*/
|
|
43
|
+
export interface WfNode {
|
|
44
|
+
/** Stable node id — referenced by connections. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Human-readable name. */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Node type slug, e.g. "condition.if", "trigger.event". */
|
|
49
|
+
type: string;
|
|
50
|
+
/** Node parameters (executor config). */
|
|
51
|
+
parameters?: Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Optional explicit canvas position. When present, auto-layout
|
|
54
|
+
* preserves it instead of computing one.
|
|
55
|
+
*/
|
|
56
|
+
position?: WfPosition;
|
|
57
|
+
/** Whether the node is disabled (skipped at runtime). */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** Whether downstream traversal continues if this node fails. */
|
|
60
|
+
continueOnFail?: boolean;
|
|
61
|
+
/** Any additional opaque node fields. */
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WfPosition {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A logical edge in the graph.
|
|
72
|
+
*
|
|
73
|
+
* `sourceOutput` is the source node's output index (n8n `main[i]`):
|
|
74
|
+
* 0 for a single output, 0/1 for if/else, 0..N-1 for switch.
|
|
75
|
+
* `targetInput` is the target node's input index (the `index` field
|
|
76
|
+
* stored on each backend connection — almost always 0).
|
|
77
|
+
*/
|
|
78
|
+
export interface WfEdge {
|
|
79
|
+
/** Source node id. */
|
|
80
|
+
source: string;
|
|
81
|
+
/** Target node id. */
|
|
82
|
+
target: string;
|
|
83
|
+
/** Source output index (default 0). */
|
|
84
|
+
sourceOutput?: number;
|
|
85
|
+
/** Target input index (default 0). */
|
|
86
|
+
targetInput?: number;
|
|
87
|
+
/** Connection type — n8n uses "main"; default "main". */
|
|
88
|
+
type?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Definition of a node type (catalog entry). Mirrors backend NodeType.
|
|
93
|
+
*/
|
|
94
|
+
export interface NodeTypeDef {
|
|
95
|
+
/** Unique slug, e.g. "condition.if". */
|
|
96
|
+
slug: string;
|
|
97
|
+
/** Human-readable name. */
|
|
98
|
+
name: string;
|
|
99
|
+
/** Category token (resolved via theme/categories.ts). */
|
|
100
|
+
category: string;
|
|
101
|
+
/** Number of outputs (1 for most, 2 for if/else, N for switch). */
|
|
102
|
+
outputCount?: number;
|
|
103
|
+
/** Labels for each output, e.g. ["true", "false"]. */
|
|
104
|
+
outputLabels?: string[];
|
|
105
|
+
/** Optional description. */
|
|
106
|
+
description?: string;
|
|
107
|
+
/**
|
|
108
|
+
* JSON-Schema (object schema) describing the node's `parameters`. Drives the
|
|
109
|
+
* editor's NodeInspector form. Intentionally typed loosely — only the subset
|
|
110
|
+
* the inspector understands (`type`, `properties`, `title`, `enum`,
|
|
111
|
+
* `description`) is consumed; unknown keywords are ignored.
|
|
112
|
+
*/
|
|
113
|
+
parameterSchema?: JsonSchema;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Minimal JSON-Schema subset consumed by the NodeInspector form. */
|
|
117
|
+
export interface JsonSchema {
|
|
118
|
+
type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array';
|
|
119
|
+
title?: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
/** For object schemas: per-property sub-schemas. */
|
|
122
|
+
properties?: Record<string, JsonSchema>;
|
|
123
|
+
/** Required property names (object schemas). */
|
|
124
|
+
required?: string[];
|
|
125
|
+
/** Enumerated allowed values (renders a select). */
|
|
126
|
+
enum?: (string | number)[];
|
|
127
|
+
/** Default value. */
|
|
128
|
+
default?: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A complete workflow graph in the editor's working shape.
|
|
133
|
+
*
|
|
134
|
+
* `connections` (the backend LOCKED format) is intentionally NOT stored
|
|
135
|
+
* here — instead edges are kept as a flat list and converted to/from the
|
|
136
|
+
* nested backend shape by serialization.ts.
|
|
137
|
+
*/
|
|
138
|
+
export interface WorkflowGraph {
|
|
139
|
+
/** Optional workflow id. */
|
|
140
|
+
id?: string;
|
|
141
|
+
/** Optional workflow name. */
|
|
142
|
+
name?: string;
|
|
143
|
+
/** Graph nodes. */
|
|
144
|
+
nodes: WfNode[];
|
|
145
|
+
/** Graph edges (flat). */
|
|
146
|
+
edges: WfEdge[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Backend persistence (LOCKED n8n connections shape) ──
|
|
150
|
+
|
|
151
|
+
/** A single connection target: `{ node, type, index }`. */
|
|
152
|
+
export interface BackendConnection {
|
|
153
|
+
/** Target node id. */
|
|
154
|
+
node: string;
|
|
155
|
+
/** Connection type — "main". */
|
|
156
|
+
type: string;
|
|
157
|
+
/** Target input index. */
|
|
158
|
+
index: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The LOCKED backend connections map:
|
|
163
|
+
*
|
|
164
|
+
* ```json
|
|
165
|
+
* {
|
|
166
|
+
* "sourceNodeId": {
|
|
167
|
+
* "main": [
|
|
168
|
+
* [ { "node": "targetId", "type": "main", "index": 0 } ]
|
|
169
|
+
* ]
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* Outer array index = source output index. Each inner array is the list
|
|
175
|
+
* of parallel targets fanning out from that output.
|
|
176
|
+
*/
|
|
177
|
+
export type BackendConnections = Record<
|
|
178
|
+
string,
|
|
179
|
+
Record<string, BackendConnection[][]>
|
|
180
|
+
>;
|
|
181
|
+
|
|
182
|
+
/** Backend node shape (snake_case fields as persisted). */
|
|
183
|
+
export interface BackendNode {
|
|
184
|
+
id: string;
|
|
185
|
+
name: string;
|
|
186
|
+
type: string;
|
|
187
|
+
parameters?: Record<string, unknown>;
|
|
188
|
+
position?: WfPosition;
|
|
189
|
+
disabled?: boolean;
|
|
190
|
+
continue_on_fail?: boolean;
|
|
191
|
+
[key: string]: unknown;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Execution views (read-only overlays for run visualization) ──
|
|
195
|
+
|
|
196
|
+
/** Per-node execution status overlay. Mirrors backend NodeExecution. */
|
|
197
|
+
export interface NodeExecView {
|
|
198
|
+
nodeId: string;
|
|
199
|
+
status: NodeStatus;
|
|
200
|
+
/**
|
|
201
|
+
* Display name of the node. Optional because the bare status overlay
|
|
202
|
+
* (keyed by node id) may be merged with the graph's node names at the
|
|
203
|
+
* call site; the execution monitor renders it when present.
|
|
204
|
+
*/
|
|
205
|
+
name?: string;
|
|
206
|
+
/** Node type slug (e.g. "condition.if"), shown in the detail panel. */
|
|
207
|
+
type?: string;
|
|
208
|
+
/**
|
|
209
|
+
* Position of this node in the run's execution sequence. The timeline
|
|
210
|
+
* renders rows sorted by this value (ascending).
|
|
211
|
+
*/
|
|
212
|
+
executionOrder?: number;
|
|
213
|
+
startedAt?: string | null;
|
|
214
|
+
completedAt?: string | null;
|
|
215
|
+
executionTimeMs?: number | null;
|
|
216
|
+
error?: string;
|
|
217
|
+
/** Input payload fed to the node (pretty-printed in the detail panel). */
|
|
218
|
+
inputData?: Record<string, unknown>;
|
|
219
|
+
outputData?: Record<string, unknown>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Whole-run execution overlay. Mirrors backend WorkflowExecution. */
|
|
223
|
+
export interface WfExecutionView {
|
|
224
|
+
executionId?: string;
|
|
225
|
+
status: RunStatus;
|
|
226
|
+
startedAt?: string | null;
|
|
227
|
+
completedAt?: string | null;
|
|
228
|
+
errorMessage?: string;
|
|
229
|
+
/** Per-node status keyed by node id. */
|
|
230
|
+
nodes: Record<string, NodeExecView>;
|
|
231
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/ui/workflows/styles
|
|
3
|
+
*
|
|
4
|
+
* Scoped stylesheet for the workflow editor/viewer. Re-exports the
|
|
5
|
+
* @xyflow/react base stylesheet (required for the canvas to render its
|
|
6
|
+
* pane, controls, handles, and edges) and adds the animated-edge keyframes
|
|
7
|
+
* used by WorkflowEdge for live runs. Consumers import this once, e.g.:
|
|
8
|
+
*
|
|
9
|
+
* import '@startsimpli/ui/workflows/styles';
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@import '@xyflow/react/dist/style.css';
|
|
13
|
+
|
|
14
|
+
/* Animated flow for edges whose source node is running/succeeded. */
|
|
15
|
+
.workflow-edge--animated {
|
|
16
|
+
stroke-dasharray: 6 4;
|
|
17
|
+
animation: workflow-edge-dash 0.6s linear infinite;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@keyframes workflow-edge-dash {
|
|
21
|
+
to {
|
|
22
|
+
stroke-dashoffset: -10;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Keep the canvas filling its container. */
|
|
27
|
+
.workflow-canvas {
|
|
28
|
+
min-height: 0;
|
|
29
|
+
}
|