From 8cae114c7eb470d5df7af2ac629971ffb94b49e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Thu, 14 May 2026 11:39:51 +0800 Subject: [PATCH 1/2] fix(dashboard): unified solid edges, hide FALLBACK labels, conditional cursor (#247 Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What: Restore graph visual preferences — all edges solid, FALLBACK labels hidden, inactive nodes not clickable. Why: Visual consistency and cleaner graph appearance per design preferences. Changes: - condition-edge.tsx: remove strokeDasharray, unify stroke color, hide FALLBACK labels - role-node.tsx: cursor-pointer only on non-default state nodes Ref: #247, closes #248 --- .../src/components/workflow-graph/condition-edge.tsx | 9 ++++----- .../src/components/workflow-graph/role-node.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx index e16d2f2..c7ddecd 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx +++ b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx @@ -78,9 +78,8 @@ export function ConditionEdge(props: EdgeProps) { defaultLabelY = result[2]; } - const stroke = isFallback ? "var(--color-text-muted)" : "var(--color-accent)"; - const strokeDasharray = isFallback ? "5 4" : undefined; - const label = edgeData?.condition ?? ""; + const stroke = "var(--color-accent)"; + const label = isFallback ? "" : (edgeData?.condition ?? ""); // Use pre-computed label position if available, otherwise fall back to default const labelX = edgeData?.labelX ?? defaultLabelX; @@ -92,7 +91,7 @@ export function ConditionEdge(props: EdgeProps) { id={id} path={path} markerEnd={markerEnd} - style={{ stroke, strokeWidth: 1.5, strokeDasharray }} + style={{ stroke, strokeWidth: 1.5 }} /> {label !== "" && ( @@ -102,7 +101,7 @@ export function ConditionEdge(props: EdgeProps) { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, background: "var(--color-surface)", border: "1px solid var(--color-border)", - color: isFallback ? "var(--color-text-muted)" : "var(--color-text)", + color: "var(--color-text)", whiteSpace: "nowrap", zIndex: 10, }} diff --git a/packages/workflow-dashboard/src/components/workflow-graph/role-node.tsx b/packages/workflow-dashboard/src/components/workflow-graph/role-node.tsx index 51933ce..99925b8 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/role-node.tsx +++ b/packages/workflow-dashboard/src/components/workflow-graph/role-node.tsx @@ -31,7 +31,7 @@ export function RoleNode(props: NodeProps) { return (
Date: Thu, 14 May 2026 11:42:06 +0800 Subject: [PATCH 2/2] fix(dashboard): alternate feedback edges left/right (#247 Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What: Feedback (back) edges now alternate between left and right sides instead of all routing to the right. Why: Multiple feedback edges targeting the same node (e.g. reviewer→coder and tester→coder) were overlapping on the right side. Changes: - types.ts: add feedbackSide field to ConditionEdgeData - use-layout.ts: track feedback count per target, alternate sides - condition-edge.tsx: feedbackPath() accepts side param, mirrors path for left Ref: #247, closes #249 --- .../workflow-graph/condition-edge.tsx | 37 ++++++++++--------- .../src/components/workflow-graph/types.ts | 1 + .../components/workflow-graph/use-layout.ts | 16 ++++++-- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx index c7ddecd..18d9ec1 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx +++ b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx @@ -7,25 +7,23 @@ const FEEDBACK_OFFSET_X = 100; const FEEDBACK_RADIUS = 16; /** - * Build an SVG path for a feedback (back) edge that routes to the right of the nodes. - * The path goes: source right → arc → vertical up → arc → target right + * Build an SVG path for a feedback (back) edge that routes to the given side of the nodes. + * The path goes: source → arc → vertical up → arc → target */ -function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number): string { - const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X; +function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number, side: "right" | "left"): string { + const d = side === "right" ? 1 : -1; + const offsetX = + side === "right" + ? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X + : Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X; const r = FEEDBACK_RADIUS; - // Start from source right side, go right, then up, then left to target right side const segments = [ `M ${sourceX} ${sourceY}`, - // Horizontal to the right - `L ${rightX - r} ${sourceY}`, - // Arc turning upward - `Q ${rightX} ${sourceY} ${rightX} ${sourceY - r}`, - // Vertical upward - `L ${rightX} ${targetY + r}`, - // Arc turning left - `Q ${rightX} ${targetY} ${rightX - r} ${targetY}`, - // Horizontal left to target + `L ${offsetX - d * r} ${sourceY}`, + `Q ${offsetX} ${sourceY} ${offsetX} ${sourceY - r}`, + `L ${offsetX} ${targetY + r}`, + `Q ${offsetX} ${targetY} ${offsetX - d * r} ${targetY}`, `L ${targetX} ${targetY}`, ]; @@ -57,10 +55,13 @@ export function ConditionEdge(props: EdgeProps) { let defaultLabelY: number; if (isFeedback) { - // Custom feedback path routed to the right - path = feedbackPath(sourceX, sourceY, targetX, targetY); - const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X; - defaultLabelX = rightX; + const side = edgeData?.feedbackSide ?? "right"; + path = feedbackPath(sourceX, sourceY, targetX, targetY, side); + const offsetX = + side === "right" + ? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X + : Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X; + defaultLabelX = offsetX; defaultLabelY = (sourceY + targetY) / 2; } else { const result = getSmoothStepPath({ diff --git a/packages/workflow-dashboard/src/components/workflow-graph/types.ts b/packages/workflow-dashboard/src/components/workflow-graph/types.ts index f0a2a63..1c58832 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/types.ts +++ b/packages/workflow-dashboard/src/components/workflow-graph/types.ts @@ -23,6 +23,7 @@ export type ConditionEdgeData = { isFallback: boolean; isFeedback: boolean; isSelfLoop: boolean; + feedbackSide: "right" | "left" | null; labelX: number | null; labelY: number | null; [key: string]: unknown; diff --git a/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts b/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts index fef0d7d..d8b575d 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts +++ b/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts @@ -173,6 +173,8 @@ function computeLayout(input: LayoutInput): LayoutResult { // Build edges with label positions // For feedback edges (target rank < source rank), we'll compute label at midpoint // of the right-side arc. The actual SVG path is drawn by ConditionEdge component. + // Track feedback edge count per target node for alternating sides + const feedbackCountByTarget = new Map(); const edges: Edge[] = input.edges.map((e) => { const isFallback = e.condition === "FALLBACK"; const isSelfLoop = e.from === e.to; @@ -185,13 +187,20 @@ function computeLayout(input: LayoutInput): LayoutResult { let labelX: number | null = null; let labelY: number | null = null; + let feedbackSide: "right" | "left" | null = null; if (sourcePos !== undefined && targetPos !== undefined) { if (isFeedback) { - // Label on the right side of the feedback arc - const rightX = centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X; + // Alternate feedback edges left/right per target node + const count = feedbackCountByTarget.get(e.to) ?? 0; + feedbackCountByTarget.set(e.to, count + 1); + feedbackSide = count % 2 === 0 ? "right" : "left"; + const offsetX = + feedbackSide === "right" + ? centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X + : centerX - ROLE_NODE_WIDTH / 2 - FEEDBACK_OFFSET_X; const midY = (sourcePos.y + sourcePos.h / 2 + targetPos.y + targetPos.h / 2) / 2; - labelX = rightX; + labelX = offsetX; labelY = midY; } else if (!isSelfLoop) { // Forward edge: label between source bottom and target top @@ -214,6 +223,7 @@ function computeLayout(input: LayoutInput): LayoutResult { isFallback, isFeedback, isSelfLoop, + feedbackSide, labelX, labelY, },