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:
7
.changeset/fn-7277-workflow-badge-bottom-left.md
Normal file
7
.changeset/fn-7277-workflow-badge-bottom-left.md
Normal 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.
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user