Skip to content

Commit 49ee76d

Browse files
DylanMerigaudclaude
andcommitted
Animate the live path edges as an invoice routes the graph
The live workflow graph lit up nodes by status but the edges stayed inert — the "loop made visible" didn't actually show the bill moving. Now an edge animates (marching ants) and goes accent when the invoice flowed along it: the source step passed (approved/done) and the target was reached (a live status, not a dead skip/block). The skipped branches stay neutral gray, so the eye follows the realized path down to the gate it's waiting on or the ERP post. Kept the structural `edges` status-free and patch the flow styling in a separate cheap effect keyed on a status signature — so a status tick never resets/re-measures/re-lays-out the graph (that path only fires on a real structural change). Inert everywhere there's no run (onboarding, diff preview): no statuses → no live edges, exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 332063a commit 49ee76d

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

components/workflow-graph.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import type { WorkflowIssue } from "@/lib/workflow-validate";
4545

4646
export type StepStatuses = Record<string, string>;
4747

48+
/** Target statuses that mean "the invoice reached this node" — so the edge into it is
49+
on the realized path (skipped/blocked are dead branches, not part of the flow). */
50+
const REACHED = new Set(["pending", "approved", "done", "rejected"]);
51+
4852
/* ── node visuals ──────────────────────────────────────────────────────────── */
4953

5054
const statusTone = (
@@ -507,6 +511,8 @@ const Inner = ({
507511
return [...real, ...gone];
508512
}, [workflow, statuses, changeOf, removed, issueOf, vertical]);
509513

514+
// Structural edges only (no status) so the layout/reset path never re-fires on a
515+
// status change — the live "flow" styling is patched separately below.
510516
const edges = useMemo<Edge[]>(() => {
511517
const out: Edge[] = [];
512518
for (const s of workflow.steps)
@@ -663,6 +669,44 @@ const Inner = ({
663669
void fitView({ nodes: ids, duration: 400, padding: 0.3 });
664670
}, [focusKey, initialized, graphKey, fitView]);
665671

672+
// Patch edge "flow" styling from the live statuses — cheap, no relayout (kept out of
673+
// the structural `edges` so a status tick never resets/re-measures the graph). An edge
674+
// animates + goes accent when the invoice flowed along it: source passed
675+
// (approved/done) and target was reached. Keyed on a status signature so it only runs
676+
// when statuses change.
677+
const statusKey = statuses
678+
? Object.entries(statuses)
679+
.map(([k, v]) => `${k}:${v}`)
680+
.sort()
681+
.join("|")
682+
: "";
683+
useEffect(() => {
684+
const st = statuses ?? {};
685+
setEdges((cur) =>
686+
cur.map((e) => {
687+
const src = st[e.source];
688+
const live =
689+
(src === "approved" || src === "done") &&
690+
REACHED.has(st[e.target] ?? "");
691+
const stroke = live ? "#5B53D6" : "#CBCDD4";
692+
const strokeWidth = live ? 2 : 1.5;
693+
const prev = e.style ?? {};
694+
if (
695+
e.animated === live &&
696+
prev.stroke === stroke &&
697+
prev.strokeWidth === strokeWidth
698+
) {
699+
return e;
700+
}
701+
return {
702+
...e,
703+
animated: live,
704+
style: { ...prev, stroke, strokeWidth },
705+
};
706+
}),
707+
);
708+
}, [statusKey, statuses, setEdges]);
709+
666710
return (
667711
<ReactFlow
668712
nodes={nodes}

0 commit comments

Comments
 (0)