feat(FN-4566): unify task card tracking retry timing chips

Unified the task card tracking retry timing chips in the dashboard, refactoring the display logic and styling for consistency across task states. The CSS was significantly simplified while tests were expanded to cover the new chip behavior.

Fusion-Task-Id: FN-4566
This commit is contained in:
Fusion
2026-05-15 00:44:15 -07:00
committed by gsxdsm
parent 3523788fed
commit 54ed639e3d
4 changed files with 142 additions and 134 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Task cards now show linked GitHub issue, retry count, and execution timing chips on one row with consistent chip sizing and a smaller GitHub icon for visual parity.

View File

@@ -131,15 +131,6 @@
}
.card-retry-badge {
display: inline-flex;
align-items: center;
gap: calc(var(--space-xs) / 2);
font-size: 0.625rem;
font-weight: 600;
line-height: 1;
padding: calc(var(--space-xs) / 2) var(--space-sm);
border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
cursor: pointer;
}
@@ -568,13 +559,6 @@
margin-top: var(--space-sm);
}
.card-bottom-row--split {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
}
.card-bottom-left-row {
display: flex;
align-items: center;
@@ -585,13 +569,6 @@
opacity: 1;
}
.card-bottom-right-row {
display: flex;
justify-content: flex-end;
align-items: center;
min-width: 0;
}
.card-provider-icons {
display: inline-flex;
align-items: center;
@@ -731,21 +708,29 @@
align-items: center;
}
.card-github-tracking-chip {
.card-time-indicator,
.card-github-tracking-chip,
.card-retry-badge {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
flex-shrink: 0;
padding: var(--space-xs) var(--space-sm);
border: 1px solid color-mix(in srgb, var(--text-muted) 30%, transparent);
height: var(--card-chip-height);
min-height: var(--card-chip-height);
border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
color: var(--text-muted);
font-size: 0.6875rem;
font-weight: 600;
font-family: var(--font-mono);
line-height: 1;
white-space: nowrap;
}
.card-github-tracking-chip {
border-color: color-mix(in srgb, var(--text-muted) 30%, transparent);
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
color: var(--text-muted);
text-decoration: none;
transition:
background-color var(--transition-fast),
@@ -768,27 +753,15 @@
}
.card-time-indicator {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
flex-shrink: 0;
padding: var(--space-xs) var(--space-sm);
height: var(--card-chip-height);
min-height: var(--card-chip-height);
border: 1px solid color-mix(in srgb, var(--text-muted) 30%, transparent);
border-radius: var(--radius-pill);
border-color: color-mix(in srgb, var(--text-muted) 30%, transparent);
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
color: var(--text-muted);
font-size: 0.6875rem;
font-weight: 600;
font-family: var(--font-mono);
line-height: 1;
white-space: nowrap;
}
.card-source-provenance + .card-time-indicator {
margin-left: 0;
.card-github-tracking-chip .provider-icon svg {
width: 12px;
height: 12px;
}
.card-progress-bar {
@@ -1290,26 +1263,19 @@
gap: var(--space-xs);
}
.card-time-indicator {
.card-time-indicator,
.card-github-tracking-chip,
.card-retry-badge {
padding: var(--space-xs) var(--space-sm);
font-size: 0.625rem;
height: var(--card-chip-height-mobile);
min-height: var(--card-chip-height-mobile);
}
.card-github-tracking-chip {
padding: var(--space-xs) var(--space-sm);
font-size: 0.625rem;
}
.card-bottom-row {
margin-top: var(--space-xs);
}
.card-bottom-row--split {
gap: var(--space-xs);
}
/* Card: smaller status badges for 280px width */
.card-status-badge,
.card-priority-badge,

View File

@@ -1742,7 +1742,7 @@ function TaskCardComponent({
</>
);
})()}
{(filesChangedButton || timeIndicator || isGitHubImportedTask) && (
{(filesChangedButton || isGitHubImportedTask || (showTrackingIndicator && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0 || timeIndicator) && (
<div className="card-footer-row">
{filesChangedButton}
{isGitHubImportedTask && (
@@ -1754,6 +1754,40 @@ function TaskCardComponent({
<ProviderIcon provider="github" size="sm" />
</span>
)}
{showTrackingIndicator && githubTrackedIssue && (
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
)}
{(task.retrySummary?.total ?? 0) > 0 && (
<span
className={`card-retry-badge${(retryWarningThreshold != null && (task.retrySummary?.total ?? 0) >= retryWarningThreshold) ? " card-retry-badge--error" : " card-retry-badge--warning"}`}
onClick={handleOpenRetries}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onOpenDetailWithTab?.(task, "retries");
}
}}
aria-label={`${task.retrySummary?.total ?? 0} retries`}
title="Open retry breakdown"
>
<RotateCw size={11} />
<span>{task.retrySummary?.total ?? 0}</span>
</span>
)}
{timeIndicator && (
<span
className="card-time-indicator"
@@ -1766,7 +1800,7 @@ function TaskCardComponent({
)}
</div>
)}
{(((task.retrySummary?.total ?? 0) > 0) || (task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy || task.overlapBlockedBy || (fanout && fanout.totalCount > 0)) && (
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy || task.overlapBlockedBy || (fanout && fanout.totalCount > 0)) && (
<div className="card-meta">
{task.dependencies && task.dependencies.length > 0 && (
<div className="card-dep-list">
@@ -1800,26 +1834,6 @@ function TaskCardComponent({
</span>
</span>
)}
{(task.retrySummary?.total ?? 0) > 0 && (
<span
className={`card-retry-badge${(retryWarningThreshold != null && (task.retrySummary?.total ?? 0) >= retryWarningThreshold) ? " card-retry-badge--error" : " card-retry-badge--warning"}`}
onClick={handleOpenRetries}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onOpenDetailWithTab?.(task, "retries");
}
}}
aria-label={`${task.retrySummary?.total ?? 0} retries`}
title="Open retry breakdown"
>
<RotateCw size={11} />
<span>{task.retrySummary?.total ?? 0}</span>
</span>
)}
{(queued || task.status === "queued") && task.column !== "in-progress" && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
</div>
)}
@@ -1846,55 +1860,37 @@ function TaskCardComponent({
)}
</div>
)}
{(showInReviewMoveControl || (showTrackingIndicator && githubTrackedIssue)) && (
<div className={`card-bottom-row${showInReviewMoveControl && showTrackingIndicator ? " card-bottom-row--split" : ""}`}>
{showInReviewMoveControl && (
<div className="card-bottom-left-row">
<div className="card-send-back" ref={sendBackRef}>
<button
className="card-send-back-btn"
onClick={handleSendBackClick}
title="Move task"
aria-label="Move task"
aria-haspopup="menu"
aria-expanded={showSendBackMenu}
>
Move
<ChevronDown size={10} />
</button>
{showSendBackMenu && (
<div className="card-send-back-menu" role="menu">
{VALID_TRANSITIONS["in-review"].map((col) => (
<button
key={col}
className="card-send-back-menu-item"
role="menuitem"
onClick={(e) => handleSendBackOptionClick(e, col)}
>
{col === "done" ? "Done (no merge)" : COLUMN_LABELS[col]}
</button>
))}
</div>
)}
</div>
</div>
)}
{showTrackingIndicator && githubTrackedIssue && (
<div className="card-bottom-right-row">
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
{showInReviewMoveControl && (
<div className="card-bottom-row">
<div className="card-bottom-left-row">
<div className="card-send-back" ref={sendBackRef}>
<button
className="card-send-back-btn"
onClick={handleSendBackClick}
title="Move task"
aria-label="Move task"
aria-haspopup="menu"
aria-expanded={showSendBackMenu}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
Move
<ChevronDown size={10} />
</button>
{showSendBackMenu && (
<div className="card-send-back-menu" role="menu">
{VALID_TRANSITIONS["in-review"].map((col) => (
<button
key={col}
className="card-send-back-menu-item"
role="menuitem"
onClick={(e) => handleSendBackOptionClick(e, col)}
>
{col === "done" ? "Done (no merge)" : COLUMN_LABELS[col]}
</button>
))}
</div>
)}
</div>
)}
</div>
</div>
)}
<PluginSlot slotId="task-card-badge" projectId={projectId} />

View File

@@ -1588,12 +1588,13 @@ describe("TaskCard", () => {
expect(link.getAttribute("title")).toBe("Linked GitHub issue: owner/repo#42");
expect(link).toHaveClass("card-github-tracking-chip", "card-github-tracking-link");
expect(link).toHaveTextContent("#42");
expect(bottomRightRow?.contains(link)).toBe(true);
expect(footerRow).toBeNull();
expect(footerRow).not.toBeNull();
expect(footerRow?.contains(link)).toBe(true);
expect(bottomRightRow).toBeNull();
expect(screen.getByTestId("provider-icon-github")).toBeDefined();
});
it("renders the GitHub tracking link below the queued badge in the bottom-right row", () => {
it("renders the GitHub tracking link in the unified footer row above queued metadata", () => {
const { container } = render(
<TaskCard
task={makeTask({
@@ -1616,12 +1617,50 @@ describe("TaskCard", () => {
);
const link = screen.getByRole("link", { name: "Linked GitHub issue #42" });
const bottomRightRow = container.querySelector(".card-bottom-right-row");
const footerRow = container.querySelector(".card-footer-row");
const queuedBadge = container.querySelector(".queued-badge");
expect(bottomRightRow).not.toBeNull();
expect(bottomRightRow?.contains(link)).toBe(true);
expect(footerRow).not.toBeNull();
expect(footerRow?.contains(link)).toBe(true);
expect(container.querySelector(".card-bottom-right-row")).toBeNull();
expect(queuedBadge).not.toBeNull();
expect(queuedBadge?.compareDocumentPosition(bottomRightRow as Node) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(queuedBadge?.compareDocumentPosition(footerRow as Node) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy();
});
it("renders tracking, retry, and timer chips in the same footer row", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "in-review",
sourceType: "dashboard_ui",
retrySummary: { total: 3 } as any,
githubTracking: {
issue: {
owner: "owner",
repo: "repo",
number: 42,
url: "https://github.com/owner/repo/issues/42",
createdAt: "2026-05-12T00:00:00.000Z",
},
},
executionStartedAt: "2026-04-25T12:00:00.000Z",
updatedAt: "2026-04-25T12:12:00.000Z",
})}
onOpenDetail={noop}
addToast={noop}
onOpenDetailWithTab={vi.fn()}
/>,
);
const footerRow = container.querySelector(".card-footer-row");
const trackingLink = container.querySelector(".card-github-tracking-chip");
const retryChip = container.querySelector(".card-retry-badge");
const timerChip = container.querySelector(".card-time-indicator");
expect(footerRow).not.toBeNull();
expect(footerRow?.contains(trackingLink)).toBe(true);
expect(footerRow?.contains(retryChip)).toBe(true);
expect(footerRow?.contains(timerChip)).toBe(true);
expect(container.querySelector(".card-bottom-right-row")).toBeNull();
});
it("keeps the GitHub tracking link keyboard focusable", () => {
@@ -1681,9 +1720,11 @@ describe("TaskCard", () => {
it("keeps GitHub tracking chip interaction-affordance CSS contract", () => {
const css = loadAllAppCssBaseOnly();
expect(css).toMatch(/\.card-github-tracking-chip\s*\{[^}]*display:\s*inline-flex;[^}]*font-family:\s*var\(--font-mono\);[^}]*\}/);
expect(css).toMatch(/\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*\{[^}]*display:\s*inline-flex;[^}]*font-family:\s*var\(--font-mono\);[^}]*\}/);
expect(css).toContain(".card-github-tracking-chip:hover");
expect(css).toMatch(/\.card-github-tracking-chip:focus-visible\s*\{[^}]*--focus-ring-strong/);
expect(css).toMatch(/\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*\{[^}]*padding:\s*var\(--space-xs\)\s+var\(--space-sm\);[^}]*height:\s*var\(--card-chip-height\);[^}]*border-radius:\s*var\(--radius-pill\);[^}]*font-size:\s*0\.6875rem;[^}]*line-height:\s*1;[^}]*\}/);
expect(css).toMatch(/\.card-github-tracking-chip\s+\.provider-icon\s+svg\s*\{[^}]*width:\s*12px;[^}]*height:\s*12px;[^}]*\}/);
render(
<TaskCard
@@ -1985,19 +2026,19 @@ describe("TaskCard", () => {
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-github-badge\s*\{[^}]*font-size:\s*0\.625rem;[^}]*\}/);
});
it("FN-4525 defines shared card-chip height tokens and applies them to both badges", () => {
it("FN-4525 defines shared card-chip height tokens and applies them to badges and chips", () => {
const baseCss = loadAllAppCssBaseOnly();
expect(baseCss).toMatch(/:root\s*\{[^}]*--card-chip-height:\s*22px;[^}]*--card-chip-height-mobile:\s*20px;[^}]*\}/);
expect(baseCss).toMatch(/\.card-github-badge\s*\{[^}]*height:\s*var\(--card-chip-height\);[^}]*\}/);
expect(baseCss).toMatch(/\.card-time-indicator\s*\{[^}]*height:\s*var\(--card-chip-height\);[^}]*\}/);
expect(baseCss).toMatch(/\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*\{[^}]*height:\s*var\(--card-chip-height\);[^}]*\}/);
});
it("FN-4525 applies shared mobile card-chip height token to both badges", () => {
it("FN-4525 applies shared mobile card-chip height token to badges and chips", () => {
const fullCss = loadAllAppCss();
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-github-badge\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-time-indicator\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
expect(fullCss).toMatch(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.card-time-indicator\s*,\s*\.card-github-tracking-chip\s*,\s*\.card-retry-badge\s*\{[^}]*height:\s*var\(--card-chip-height-mobile\);[^}]*\}/);
});
it("FN-4511 keeps GitHub badge and timer chip geometry in parity", () => {