@tangle-network/sandbox-ui 0.51.0 → 0.52.0

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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GTJ5PSK5.js";
4
4
  import {
5
5
  buildWorkflowGraph
6
- } from "./chunk-46VFHJPQ.js";
6
+ } from "./chunk-DEN4GUA7.js";
7
7
  import {
8
8
  providerLabel
9
9
  } from "./chunk-7JLB7UZ3.js";
@@ -15,7 +15,9 @@ import {
15
15
  Controls,
16
16
  Handle,
17
17
  Position,
18
- ReactFlow
18
+ ReactFlow,
19
+ useEdgesState,
20
+ useNodesState
19
21
  } from "@xyflow/react";
20
22
  import "@xyflow/react/dist/style.css";
21
23
  import {
@@ -27,7 +29,9 @@ import {
27
29
 
28
30
  // src/workflows/flow-graph.ts
29
31
  function buildFlowGraph(yaml, nodeState) {
30
- const graph = buildWorkflowGraph(yaml);
32
+ const graph = buildWorkflowGraph(yaml, {
33
+ reserveRunState: nodeState !== void 0
34
+ });
31
35
  return {
32
36
  error: graph.error,
33
37
  nodes: graph.nodes.map(
@@ -47,6 +51,22 @@ function buildFlowGraph(yaml, nodeState) {
47
51
  }))
48
52
  };
49
53
  }
54
+ function sameRunState(a, b) {
55
+ if (a === b) return true;
56
+ if (!a || !b) return false;
57
+ const aKeys = Object.keys(a);
58
+ const bKeys = Object.keys(b);
59
+ if (aKeys.length !== bKeys.length) return false;
60
+ return aKeys.every((k) => a[k] === b[k]);
61
+ }
62
+ function mergeRunState(prev, baseDataById, nodeState) {
63
+ return prev.map((n) => {
64
+ const base = baseDataById.get(n.id) ?? n.data;
65
+ const s = nodeState?.[n.id];
66
+ const nextData = s ? { ...base, state: s } : base;
67
+ return sameRunState(n.data.state, nextData.state) ? n : { ...n, data: nextData };
68
+ });
69
+ }
50
70
 
51
71
  // src/workflows/format.ts
52
72
  function fmtDuration(ms) {
@@ -240,22 +260,33 @@ function WorkflowGraph({
240
260
  onNodeClick
241
261
  }) {
242
262
  const colorMode = useColorMode();
243
- const { nodes, edges, error } = useMemo(
244
- () => buildFlowGraph(yaml, nodeState),
245
- [yaml, nodeState]
263
+ const hasRunOverlay = nodeState !== void 0;
264
+ const structural = useMemo(
265
+ () => buildFlowGraph(yaml, hasRunOverlay ? {} : void 0),
266
+ [yaml, hasRunOverlay]
246
267
  );
268
+ const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
269
+ const [edges, setEdges, onEdgesChange] = useEdgesState(structural.edges);
270
+ useEffect(() => {
271
+ setNodes(structural.nodes);
272
+ setEdges(structural.edges);
273
+ }, [structural, setNodes, setEdges]);
274
+ useEffect(() => {
275
+ const baseById = new Map(structural.nodes.map((n) => [n.id, n.data]));
276
+ setNodes((prev) => mergeRunState(prev, baseById, nodeState));
277
+ }, [nodeState, structural, setNodes]);
247
278
  const handleNodeClick = useCallback(
248
279
  (_event, node) => {
249
280
  onNodeClick?.(node.id, node.data);
250
281
  },
251
282
  [onNodeClick]
252
283
  );
253
- if (error || nodes.length === 0) {
284
+ if (structural.error || structural.nodes.length === 0) {
254
285
  return /* @__PURE__ */ jsx(
255
286
  "div",
256
287
  {
257
288
  className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-text-muted text-xs ${className ?? ""}`,
258
- children: error ?? "Nothing to show"
289
+ children: structural.error ?? "Nothing to show"
259
290
  }
260
291
  );
261
292
  }
@@ -265,6 +296,8 @@ function WorkflowGraph({
265
296
  {
266
297
  nodes,
267
298
  edges,
299
+ onNodesChange,
300
+ onEdgesChange,
268
301
  nodeTypes: NODE_TYPES,
269
302
  colorMode,
270
303
  onNodeClick: onNodeClick ? handleNodeClick : void 0,
@@ -273,6 +306,8 @@ function WorkflowGraph({
273
306
  proOptions: { hideAttribution: true },
274
307
  nodesDraggable: !isPreview,
275
308
  nodesConnectable: false,
309
+ deleteKeyCode: null,
310
+ edgesReconnectable: false,
276
311
  elementsSelectable: !isPreview,
277
312
  edgesFocusable: false,
278
313
  zoomOnScroll: false,
@@ -4,10 +4,22 @@ import {
4
4
 
5
5
  // src/workflows/model.ts
6
6
  import { parse as parseYaml } from "yaml";
7
- var ROW_GAP = 112;
8
7
  var BRANCH_X = 300;
9
- var BRANCH_ROW_GAP = 84;
10
- var BRANCH_STACK_PADDING = 40;
8
+ var NODE_GAP = 44;
9
+ var BRANCH_GAP = 24;
10
+ var CARD_CHROME = 20;
11
+ var TITLE_ROW = 22;
12
+ var TEXT_ROW = 20;
13
+ var META_ROW = 44;
14
+ var PREVIEW_ROW = 34;
15
+ function estimateNodeHeight(data, withRunState) {
16
+ let h = CARD_CHROME + TITLE_ROW;
17
+ if (data.subtitle) h += TEXT_ROW;
18
+ if (withRunState || data.model) h += META_ROW;
19
+ if (withRunState) h += PREVIEW_ROW;
20
+ if (data.provider) h += TEXT_ROW;
21
+ return h;
22
+ }
11
23
  function asRecord(v) {
12
24
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
13
25
  }
@@ -165,7 +177,8 @@ function describeTrigger(on) {
165
177
  tone: "trigger"
166
178
  };
167
179
  }
168
- function buildWorkflowGraph(yaml) {
180
+ function buildWorkflowGraph(yaml, options) {
181
+ const reserveRunState = options?.reserveRunState ?? false;
169
182
  if (!yaml || yaml.trim() === "") {
170
183
  return { nodes: [], edges: [], error: "No definition" };
171
184
  }
@@ -193,13 +206,10 @@ function buildWorkflowGraph(yaml) {
193
206
  });
194
207
  };
195
208
  if (def.on) {
196
- nodes.push({
197
- id: "trigger",
198
- position: { x: 0, y },
199
- data: describeTrigger(def.on)
200
- });
209
+ const data = describeTrigger(def.on);
210
+ nodes.push({ id: "trigger", position: { x: 0, y }, data });
201
211
  prevId = "trigger";
202
- y += ROW_GAP;
212
+ y += estimateNodeHeight(data, false) + NODE_GAP;
203
213
  }
204
214
  actions.forEach((action, i) => {
205
215
  const id = `a${i}`;
@@ -207,6 +217,7 @@ function buildWorkflowGraph(yaml) {
207
217
  nodes.push({ id, position: { x: 0, y }, data });
208
218
  if (prevId) addEdge(prevId, id, false);
209
219
  prevId = id;
220
+ const spineHeight = estimateNodeHeight(data, reserveRunState);
210
221
  const rec = asRecord(action);
211
222
  const children = "parallel" in rec ? Array.isArray(asRecord(rec.parallel).branches) ? asRecord(rec.parallel).branches : [] : "foreach" in rec ? (
212
223
  // A foreach with no `do` template has no child to render.
@@ -214,22 +225,23 @@ function buildWorkflowGraph(yaml) {
214
225
  ) : [];
215
226
  if (children.length > 0) {
216
227
  let by = y;
228
+ let branchStackHeight = 0;
217
229
  children.forEach((child, j) => {
218
230
  const cid = `${id}-b${j}`;
231
+ const childData = describeAction(child);
219
232
  nodes.push({
220
233
  id: cid,
221
234
  position: { x: BRANCH_X, y: by },
222
- data: describeAction(child)
235
+ data: childData
223
236
  });
224
237
  addEdge(id, cid, true);
225
- by += BRANCH_ROW_GAP;
238
+ const childHeight = estimateNodeHeight(childData, reserveRunState);
239
+ by += childHeight + BRANCH_GAP;
240
+ branchStackHeight += childHeight + BRANCH_GAP;
226
241
  });
227
- y += Math.max(
228
- ROW_GAP,
229
- children.length * BRANCH_ROW_GAP + BRANCH_STACK_PADDING
230
- );
242
+ y += Math.max(spineHeight, branchStackHeight) + NODE_GAP;
231
243
  } else {
232
- y += ROW_GAP;
244
+ y += spineHeight + NODE_GAP;
233
245
  }
234
246
  });
235
247
  return { nodes, edges, error: null };
@@ -18,6 +18,12 @@ type WfNodeStatus = "queued" | "running" | "succeeded" | "failed";
18
18
  * merged onto {@link WfNodeData.state} at render time. Absent ⇒ the graph shows
19
19
  * the static definition only (the assistant-proposal preview path). Driven from
20
20
  * a workflow run's per-action results so the graph animates as the run executes.
21
+ *
22
+ * Keep every field PRIMITIVE: `sameRunState` (flow-graph.ts) shallow-compares
23
+ * with `===` to decide whether a render tick changed a node. A nested object/
24
+ * array field would compare by reference (a structurally-equal value reads as
25
+ * changed → a harmless extra render, never a stale node) — if one is ever needed,
26
+ * teach `sameRunState` to compare it.
21
27
  */
22
28
  interface WfNodeState {
23
29
  status: WfNodeStatus;
@@ -85,10 +91,22 @@ interface WfGraph {
85
91
  /** Set when the YAML couldn't be parsed into a renderable graph. */
86
92
  error: string | null;
87
93
  }
94
+ /** Options for {@link buildWorkflowGraph}. */
95
+ interface BuildWorkflowGraphOptions {
96
+ /**
97
+ * Reserve vertical space for the rows live run state adds to a spine node (the
98
+ * meta-chip row + the output/error preview), so the RUN view's spine never
99
+ * overlaps once a node starts or terminates. The static/preview layout (no run
100
+ * overlay) leaves this off to stay compact. Defaults to `false`.
101
+ */
102
+ reserveRunState?: boolean;
103
+ }
88
104
  /** Build a positioned graph from a workflow YAML string. Never throws —
89
105
  * malformed YAML or an empty definition returns an `error` the UI can fall
90
- * back on (e.g. show the raw YAML while authoring). */
91
- declare function buildWorkflowGraph(yaml: string): WfGraph;
106
+ * back on (e.g. show the raw YAML while authoring). Pass
107
+ * `{ reserveRunState: true }` for the run view so the spine leaves room for the
108
+ * rows live run state adds (see {@link estimateNodeHeight}). */
109
+ declare function buildWorkflowGraph(yaml: string, options?: BuildWorkflowGraphOptions): WfGraph;
92
110
 
93
111
  interface WorkflowGraphProps {
94
112
  /** Workflow YAML to render. */
@@ -116,4 +134,4 @@ interface WorkflowGraphProps {
116
134
 
117
135
  declare function WorkflowGraph(props: WorkflowGraphProps): react_jsx_runtime.JSX.Element;
118
136
 
119
- export { type WfEdge, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, buildWorkflowGraph };
137
+ export { type BuildWorkflowGraphOptions, type WfEdge, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, buildWorkflowGraph };
package/dist/workflows.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildWorkflowGraph
3
- } from "./chunk-46VFHJPQ.js";
3
+ } from "./chunk-DEN4GUA7.js";
4
4
  import "./chunk-7JLB7UZ3.js";
5
5
  import "./chunk-U67V476Y.js";
6
6
 
@@ -27,7 +27,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
27
27
  function lazyWorkflowGraph() {
28
28
  return lazy(
29
29
  () => retryImport(
30
- () => import("./WorkflowGraph-4JREVTSK.js").then((m) => ({ default: m.WorkflowGraph }))
30
+ () => import("./WorkflowGraph-FSS7GW5O.js").then((m) => ({ default: m.WorkflowGraph }))
31
31
  )
32
32
  );
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",