From 52e4a260c4c3530cfd0dce657c7e42bef2afa03e Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 30 Jun 2026 08:30:38 -0700 Subject: [PATCH] FN-7277: move workflow badges to card footers Move aggregate workflow identity below task-card footer content so it stays anchored at the bottom-left. - Reposition the optional workflow badge out of the meta-badge row and into its own bottom-left row. - Add responsive styling so long workflow names wrap within the card footer area without creating extra actions. - Expand TaskCard layout tests for badge ordering across metadata, footer, and action surfaces. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-7277-workflow-badge-bottom-left.md | 7 ++++ packages/dashboard/app/components/TaskCard.css | 22 +++++++++- packages/dashboard/app/components/TaskCard.tsx | 31 ++++++++------ .../__tests__/TaskCard.badge-wrap.test.tsx | 16 +++++++ .../__tests__/TaskCard.footer-wrap.test.tsx | 33 +++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 49 +++++++++++++++++++++- 6 files changed, 143 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-7277 Fusion-Task-Lineage: 5dd05d2a-1728-49d3-8cbf-b99d6293730b Co-authored-by: Fusion (runfusion.ai) --- .../fn-7277-workflow-badge-bottom-left.md | 7 +++ .../dashboard/app/components/TaskCard.css | 22 ++++++++- .../dashboard/app/components/TaskCard.tsx | 31 +++++++----- .../__tests__/TaskCard.badge-wrap.test.tsx | 16 ++++++ .../__tests__/TaskCard.footer-wrap.test.tsx | 33 +++++++++++++ .../components/__tests__/TaskCard.test.tsx | 49 ++++++++++++++++++- 6 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 .changeset/fn-7277-workflow-badge-bottom-left.md diff --git a/.changeset/fn-7277-workflow-badge-bottom-left.md b/.changeset/fn-7277-workflow-badge-bottom-left.md new file mode 100644 index 0000000000..d709994919 --- /dev/null +++ b/.changeset/fn-7277-workflow-badge-bottom-left.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Move All workflows Board task-card workflow badges to the bottom-left. +category: fix +dev: Updates TaskCard workflowBadge placement and dashboard layout coverage. diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index cc53e9f62f..d040c45fb7 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -215,14 +215,26 @@ The global mobile touch-action reset applies to descendants, and browsers inters /* FNXC:WorkflowBoard 2026-06-29-00:00: Aggregate Board cards need a compact workflow-name badge so operators can identify each card's source workflow while shared columns combine multiple workflows. The badge is opt-in metadata from Board and uses neutral card tokens so non-board TaskCard callers do not render empty workflow shells. + +FNXC:WorkflowBoard 2026-06-30-00:00: +All workflows cards need workflow identity at the bottom-left below every other card icon/action/meta row, so the wrapper is rendered after those rows and left-aligns the neutral badge without adding click targets. */ +.card-workflow-badge-row { + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; + margin-top: var(--space-xs); +} + .card-workflow-badge { - max-width: 18ch; + max-width: min(100%, 18ch); background: var(--surface-2); border-color: var(--border); color: var(--text-muted); text-transform: none; letter-spacing: 0; + flex: 0 1 auto; } .card-no-commits-expected-badge { @@ -1583,6 +1595,14 @@ executing. These map 1:1 to the unified progress status so the dot color encodes row-gap: var(--space-xs); } + .card-workflow-badge-row { + margin-top: var(--space-xs); + } + + .card-workflow-badge { + max-width: 100%; + } + .card-time-indicator, .card-github-tracking-chip, .card-retry-badge, diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 1bcfa1b73c..f2e56dfd47 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -2285,8 +2285,7 @@ function TaskCardComponent({ && workflowBadge.workflowName.trim().length > 0; const hasCardMetaBadges = showPriorityBadge || task.executionMode === "fast" - || isAgentCreated - || hasWorkflowBadge; + || isAgentCreated; if (isEditing) { return ( @@ -2505,17 +2504,6 @@ function TaskCardComponent({ )} {hasCardMetaBadges && (
- {hasWorkflowBadge && ( - - {workflowBadge.workflowName} - - )} {showPriorityBadge && ( {normalizedPriority} @@ -3045,6 +3033,23 @@ function TaskCardComponent({ {showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()}
)} + {hasWorkflowBadge && ( +
+ {/* + FNXC:WorkflowBoard 2026-06-30-00:00: + All workflows Board cards need workflow identity anchored at the card's bottom-left, below footer chips, dependency/meta rows, provider icons, and action controls, while per-workflow cards keep omitting this opt-in metadata. + */} + + {workflowBadge.workflowName} + +
+ )} {(showCreatePrQuickAction || isPrCreateOpen) && ( { expect(styles.maxWidth).not.toBe("none"); expect(styles.whiteSpace).toBe("nowrap"); }); + + it("places the workflow badge in a left-aligned bottom row outside the header badge cluster", () => { + const workflowRow = container.querySelector(".card-workflow-badge-row") as HTMLElement; + const workflowBadge = container.querySelector(".card-workflow-badge") as HTMLElement; + const metaBadges = container.querySelector(".card-meta-badges") as HTMLElement; + + expect(workflowRow).toBeTruthy(); + expect(workflowBadge).toBeTruthy(); + expect(workflowRow.contains(workflowBadge)).toBe(true); + expect(metaBadges.contains(workflowBadge)).toBe(false); + + const styles = getComputedStyle(workflowRow); + expect(styles.display).toBe("flex"); + expect(styles.justifyContent).toBe("flex-start"); + expect(styles.minWidth).toBe("0px"); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx index 62840c5456..2d778fcb8b 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx @@ -22,6 +22,7 @@ vi.mock("lucide-react", () => ({ RotateCw: () => , Zap: () => , AlertTriangle: () => , + ArrowUpRight: () => , })); vi.mock("../ProviderIcon", () => ({ @@ -152,4 +153,36 @@ describe("TaskCard footer wrapping (FN-5210)", () => { expect(styles.whiteSpace).toBe("nowrap"); expect(styles.flexShrink).toBe("0"); }); + + it("places workflow badges after footer and action rows in DOM order", () => { + const { container } = render( + undefined)} + workflowBadge={{ workflowId: "wf-footer", workflowName: "Workflow with a very long display name for wrapping" }} + />, + ); + + const footerRow = container.querySelector(".card-footer-row") as HTMLElement; + const actionRow = container.querySelector(".card-action-row") as HTMLElement; + const workflowRow = container.querySelector(".card-workflow-badge-row") as HTMLElement; + const workflowBadge = container.querySelector(".card-workflow-badge") as HTMLElement; + + expect(footerRow).toBeTruthy(); + expect(actionRow).toBeTruthy(); + expect(workflowRow).toBeTruthy(); + expect(workflowRow.contains(workflowBadge)).toBe(true); + expect(footerRow.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(actionRow.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + + const rowStyles = getComputedStyle(workflowRow); + expect(rowStyles.justifyContent).toBe("flex-start"); + const badgeStyles = getComputedStyle(workflowBadge); + expect(badgeStyles.whiteSpace).toBe("nowrap"); + expect(badgeStyles.overflow).toBe("hidden"); + expect(badgeStyles.textOverflow).toBe("ellipsis"); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 01b0a351a8..0fd2cb264a 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -4993,7 +4993,7 @@ describe("TaskCard memo comparator provenance behavior", () => { describe("TaskCard workflow badges", () => { it("renders a compact accessible workflow badge only when metadata is present", () => { - const { rerender } = render( + const { container, rerender } = render( { ); const badge = screen.getByTestId("card-workflow-badge"); + const row = screen.getByTestId("card-workflow-badge-row"); expect(badge).toHaveTextContent("Custom Flow"); expect(badge).toHaveAttribute("data-workflow-id", "wf-custom"); expect(badge).toHaveAccessibleName("Workflow Custom Flow"); + expect(row).toContainElement(badge); + expect(badge.closest(".card-meta-badges")).toBeNull(); + expect(container.querySelector(".card-meta-badges")).toBeNull(); rerender( { />, ); expect(screen.queryByTestId("card-workflow-badge")).toBeNull(); + expect(screen.queryByTestId("card-workflow-badge-row")).toBeNull(); + }); + + it("keeps top badges in the meta cluster while rendering workflow identity below card rows", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-30T12:10:00.000Z")); + + const { container } = render( + , + ); + + const metaBadges = screen.getByTestId("card-meta-badges"); + const badge = screen.getByTestId("card-workflow-badge"); + const workflowRow = screen.getByTestId("card-workflow-badge-row"); + expect(metaBadges.querySelector(".card-priority-badge")).not.toBeNull(); + expect(metaBadges.querySelector(".card-execution-mode-badge")).not.toBeNull(); + expect(metaBadges.querySelector(".card-agent-created-badge")).not.toBeNull(); + expect(metaBadges.querySelector(".card-workflow-badge")).toBeNull(); + expect(workflowRow).toContainElement(badge); + + [".card-footer-row", ".card-meta", ".card-agent-row"].forEach((selector) => { + const row = container.querySelector(selector); + expect(row, `${selector} should render for the placement fixture`).not.toBeNull(); + expect(row!.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + expect(badge.closest(".card-action-row")).toBeNull(); + vi.useRealTimers(); }); });