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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 08:30:38 -07:00
parent c848ce11de
commit 52e4a260c4
6 changed files with 143 additions and 15 deletions

View File

@@ -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.

View File

@@ -215,14 +215,26 @@ The global mobile touch-action reset applies to descendants, and browsers inters
/* /*
FNXC:WorkflowBoard 2026-06-29-00:00: 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. 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 { .card-workflow-badge {
max-width: 18ch; max-width: min(100%, 18ch);
background: var(--surface-2); background: var(--surface-2);
border-color: var(--border); border-color: var(--border);
color: var(--text-muted); color: var(--text-muted);
text-transform: none; text-transform: none;
letter-spacing: 0; letter-spacing: 0;
flex: 0 1 auto;
} }
.card-no-commits-expected-badge { .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); row-gap: var(--space-xs);
} }
.card-workflow-badge-row {
margin-top: var(--space-xs);
}
.card-workflow-badge {
max-width: 100%;
}
.card-time-indicator, .card-time-indicator,
.card-github-tracking-chip, .card-github-tracking-chip,
.card-retry-badge, .card-retry-badge,

View File

@@ -2285,8 +2285,7 @@ function TaskCardComponent({
&& workflowBadge.workflowName.trim().length > 0; && workflowBadge.workflowName.trim().length > 0;
const hasCardMetaBadges = showPriorityBadge const hasCardMetaBadges = showPriorityBadge
|| task.executionMode === "fast" || task.executionMode === "fast"
|| isAgentCreated || isAgentCreated;
|| hasWorkflowBadge;
if (isEditing) { if (isEditing) {
return ( return (
@@ -2505,17 +2504,6 @@ function TaskCardComponent({
)} )}
{hasCardMetaBadges && ( {hasCardMetaBadges && (
<div className="card-meta-badges" data-testid="card-meta-badges"> <div className="card-meta-badges" data-testid="card-meta-badges">
{hasWorkflowBadge && (
<span
className="card-workflow-badge"
title={t("tasks.workflowBadgeTitle", "Workflow: {{name}}", { name: workflowBadge.workflowName })}
aria-label={t("tasks.workflowBadgeAriaLabel", "Workflow {{name}}", { name: workflowBadge.workflowName })}
data-testid="card-workflow-badge"
data-workflow-id={workflowBadge.workflowId}
>
{workflowBadge.workflowName}
</span>
)}
{showPriorityBadge && ( {showPriorityBadge && (
<span className={`card-priority-badge card-priority-badge--${normalizedPriority}`}> <span className={`card-priority-badge card-priority-badge--${normalizedPriority}`}>
{normalizedPriority} {normalizedPriority}
@@ -3045,6 +3033,23 @@ function TaskCardComponent({
{showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()} {showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()}
</div> </div>
)} )}
{hasWorkflowBadge && (
<div className="card-workflow-badge-row" data-testid="card-workflow-badge-row">
{/*
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.
*/}
<span
className="card-workflow-badge"
title={t("tasks.workflowBadgeTitle", "Workflow: {{name}}", { name: workflowBadge.workflowName })}
aria-label={t("tasks.workflowBadgeAriaLabel", "Workflow {{name}}", { name: workflowBadge.workflowName })}
data-testid="card-workflow-badge"
data-workflow-id={workflowBadge.workflowId}
>
{workflowBadge.workflowName}
</span>
</div>
)}
<PluginSlot slotId="task-card-badge" projectId={projectId} /> <PluginSlot slotId="task-card-badge" projectId={projectId} />
{(showCreatePrQuickAction || isPrCreateOpen) && ( {(showCreatePrQuickAction || isPrCreateOpen) && (
<PrCreateModal <PrCreateModal

View File

@@ -114,4 +114,20 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expect(styles.maxWidth).not.toBe("none"); expect(styles.maxWidth).not.toBe("none");
expect(styles.whiteSpace).toBe("nowrap"); 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");
});
}); });

View File

@@ -22,6 +22,7 @@ vi.mock("lucide-react", () => ({
RotateCw: () => <svg />, RotateCw: () => <svg />,
Zap: () => <svg />, Zap: () => <svg />,
AlertTriangle: () => <svg />, AlertTriangle: () => <svg />,
ArrowUpRight: () => <svg />,
})); }));
vi.mock("../ProviderIcon", () => ({ vi.mock("../ProviderIcon", () => ({
@@ -152,4 +153,36 @@ describe("TaskCard footer wrapping (FN-5210)", () => {
expect(styles.whiteSpace).toBe("nowrap"); expect(styles.whiteSpace).toBe("nowrap");
expect(styles.flexShrink).toBe("0"); expect(styles.flexShrink).toBe("0");
}); });
it("places workflow badges after footer and action rows in DOM order", () => {
const { container } = render(
<TaskCard
task={makeTask()}
onOpenDetail={noop}
addToast={noop}
onOpenDetailWithTab={noop}
onPromote={vi.fn(async () => 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");
});
}); });

View File

@@ -4993,7 +4993,7 @@ describe("TaskCard memo comparator provenance behavior", () => {
describe("TaskCard workflow badges", () => { describe("TaskCard workflow badges", () => {
it("renders a compact accessible workflow badge only when metadata is present", () => { it("renders a compact accessible workflow badge only when metadata is present", () => {
const { rerender } = render( const { container, rerender } = render(
<TaskCard <TaskCard
task={makeTask()} task={makeTask()}
onOpenDetail={noop} onOpenDetail={noop}
@@ -5003,9 +5003,13 @@ describe("TaskCard workflow badges", () => {
); );
const badge = screen.getByTestId("card-workflow-badge"); const badge = screen.getByTestId("card-workflow-badge");
const row = screen.getByTestId("card-workflow-badge-row");
expect(badge).toHaveTextContent("Custom Flow"); expect(badge).toHaveTextContent("Custom Flow");
expect(badge).toHaveAttribute("data-workflow-id", "wf-custom"); expect(badge).toHaveAttribute("data-workflow-id", "wf-custom");
expect(badge).toHaveAccessibleName("Workflow Custom Flow"); 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( rerender(
<TaskCard <TaskCard
@@ -5016,6 +5020,49 @@ describe("TaskCard workflow badges", () => {
/>, />,
); );
expect(screen.queryByTestId("card-workflow-badge")).toBeNull(); 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(
<TaskCard
task={makeTask({
column: "in-progress",
priority: "high",
executionMode: "fast",
sourceType: "automation",
sourceMetadata: { agentName: "Task Robot" },
assignedAgentId: "agent-1",
columnMovedAt: "2026-06-30T12:00:00.000Z",
updatedAt: "2026-06-30T12:00:00.000Z",
dependencies: ["FN-1"],
})}
onOpenDetail={noop}
addToast={noop}
workflowBadge={{ workflowId: "wf-long", workflowName: "Very long custom workflow name for aggregate cards" }}
/>,
);
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();
}); });
}); });