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:
5
.changeset/fn-4566-task-card-chip-row.md
Normal file
5
.changeset/fn-4566-task-card-chip-row.md
Normal 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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user