feat(FN-814): add awaiting-approval board highlight with distinct visuals
- Add amber/orange highlight styling for tasks with awaiting-approval status on the board - Update TaskCard component to render awaiting-approval status badge and border styling - Add comprehensive unit tests for awaiting-approval card rendering scenarios - Document awaiting-approval board highlight behavior in README
This commit is contained in:
@@ -315,8 +315,9 @@ function TaskCardComponent({
|
||||
|
||||
const isFailed = task.status === "failed";
|
||||
const isPaused = task.paused === true;
|
||||
const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
|
||||
const isArchived = task.column === "archived";
|
||||
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && !isAwaitingApproval && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||
const isDraggable = !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit or if archived
|
||||
|
||||
// Check if this card can be edited inline
|
||||
@@ -502,7 +503,7 @@ function TaskCardComponent({
|
||||
setShowSteps((current) => !current);
|
||||
}, []);
|
||||
|
||||
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${fileDragOver ? " file-drop-target" : ""}${isEditing ? " card-editing" : ""}${isSaving ? " card-saving" : ""}`;
|
||||
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${isAwaitingApproval ? " awaiting-approval" : ""}${fileDragOver ? " file-drop-target" : ""}${isEditing ? " card-editing" : ""}${isSaving ? " card-saving" : ""}`;
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
@@ -564,13 +565,15 @@ function TaskCardComponent({
|
||||
)}
|
||||
{!isPaused && task.status && task.status !== "queued" && (
|
||||
<span
|
||||
className={`card-status-badge${ACTIVE_STATUSES.has(task.status) ? " pulsing" : ""}${isFailed ? " failed" : ""}`}
|
||||
style={isFailed
|
||||
? { background: "rgba(218,54,51,0.15)", color: "#da3633" }
|
||||
: { background: COLUMN_COLOR_MAP[task.column], color: COLUMN_TEXT_COLOR_MAP[task.column] }
|
||||
className={`card-status-badge${isAwaitingApproval ? " awaiting-approval" : ""}${ACTIVE_STATUSES.has(task.status) ? " pulsing" : ""}${isFailed ? " failed" : ""}`}
|
||||
style={isAwaitingApproval
|
||||
? { background: "rgba(210,153,34,0.2)", color: "var(--triage)" }
|
||||
: isFailed
|
||||
? { background: "rgba(218,54,51,0.15)", color: "#da3633" }
|
||||
: { background: COLUMN_COLOR_MAP[task.column], color: COLUMN_TEXT_COLOR_MAP[task.column] }
|
||||
}
|
||||
>
|
||||
{task.status}
|
||||
{isAwaitingApproval ? "Awaiting Approval" : task.status}
|
||||
</span>
|
||||
)}
|
||||
{hasGitHubBadge && (
|
||||
|
||||
@@ -2907,3 +2907,152 @@ describe("TaskCard title display", () => {
|
||||
expect(cardTitle).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests for awaiting-approval visual state in TaskCard.
|
||||
* Tasks in triage with status "awaiting-approval" receive a distinct
|
||||
* highlight and approval-specific badge text on the board.
|
||||
*/
|
||||
describe("TaskCard awaiting-approval state", () => {
|
||||
const noopToast = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("applies awaiting-approval class when task is in triage with awaiting-approval status", () => {
|
||||
const task = makeTask({ column: "triage", status: "awaiting-approval" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-id="FN-099"]');
|
||||
expect(card?.classList.contains("awaiting-approval")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT apply awaiting-approval class for triage tasks with other statuses", () => {
|
||||
const task = makeTask({ column: "triage", status: "specifying" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-id="FN-099"]');
|
||||
expect(card?.classList.contains("awaiting-approval")).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT apply awaiting-approval class for non-triage columns", () => {
|
||||
const columns: Column[] = ["todo", "in-progress", "in-review", "done"];
|
||||
|
||||
for (const column of columns) {
|
||||
const task = makeTask({ column, status: "awaiting-approval" });
|
||||
|
||||
const { unmount } = render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-id="FN-099"]');
|
||||
expect(card?.classList.contains("awaiting-approval")).toBe(false);
|
||||
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows 'Awaiting Approval' badge text for awaiting-approval tasks", () => {
|
||||
const task = makeTask({ column: "triage", status: "awaiting-approval" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText("Awaiting Approval");
|
||||
expect(badge).toBeDefined();
|
||||
expect(badge.classList.contains("card-status-badge")).toBe(true);
|
||||
expect(badge.classList.contains("awaiting-approval")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows raw status text for other triage statuses", () => {
|
||||
const task = makeTask({ column: "triage", status: "specifying" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show raw status text, not "Awaiting Approval"
|
||||
expect(screen.getByText("specifying")).toBeDefined();
|
||||
expect(screen.queryByText("Awaiting Approval")).toBeNull();
|
||||
});
|
||||
|
||||
it("does NOT apply agent-active class for awaiting-approval tasks", () => {
|
||||
const task = makeTask({ column: "triage", status: "awaiting-approval" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-id="FN-099"]');
|
||||
expect(card?.classList.contains("agent-active")).toBe(false);
|
||||
expect(card?.classList.contains("awaiting-approval")).toBe(true);
|
||||
});
|
||||
|
||||
it("awaiting-approval badge uses triage color styling", () => {
|
||||
const task = makeTask({ column: "triage", status: "awaiting-approval" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText("Awaiting Approval") as HTMLElement;
|
||||
// The badge should use the triage amber color
|
||||
expect(badge.style.color).toBe("var(--triage)");
|
||||
// JSDOM may normalize rgba() with or without spaces, so normalize for comparison
|
||||
expect(badge.style.background.replace(/\s+/g, "")).toBe("rgba(210,153,34,0.2)");
|
||||
});
|
||||
|
||||
it("awaiting-approval card does NOT look like other states (no agent-active, failed, or paused)", () => {
|
||||
const task = makeTask({ column: "triage", status: "awaiting-approval" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const card = document.querySelector('[data-id="FN-099"]');
|
||||
expect(card?.classList.contains("awaiting-approval")).toBe(true);
|
||||
expect(card?.classList.contains("agent-active")).toBe(false);
|
||||
expect(card?.classList.contains("failed")).toBe(false);
|
||||
expect(card?.classList.contains("paused")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -967,6 +967,24 @@ body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Awaiting manual plan approval: amber left border + subtle pulsing background to signal human review needed.
|
||||
Distinct from agent-active (purple glow), failed (red left border + error block), and paused (dimmed opacity). */
|
||||
.card.awaiting-approval {
|
||||
border-left: 3px solid var(--triage);
|
||||
background: rgba(210, 153, 34, 0.06);
|
||||
animation: approval-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes approval-pulse {
|
||||
0%,
|
||||
100% {
|
||||
background: rgba(210, 153, 34, 0.06);
|
||||
}
|
||||
50% {
|
||||
background: rgba(210, 153, 34, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.card.paused {
|
||||
opacity: 0.55;
|
||||
border-left: 3px solid var(--text-secondary, #888);
|
||||
|
||||
Reference in New Issue
Block a user