feat(FN-4122): simplify fan-out badge label, inherit count color from paren
Simplified the fan-out badge label to match the design spec and removed the now-redundant CSS overrides in TaskCard.css, inheriting the count color from the task column instead. Also stabilized verification tests and added a changeset for the patch release. Fusion-Task-Id: FN-4122 Fusion-Task-Lineage: 7dba6f5b-5143-49e9-8edb-2f54a1e2b608
This commit is contained in:
5
.changeset/fn-4122-fanout-badge-label.md
Normal file
5
.changeset/fn-4122-fanout-badge-label.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Simplify the task-card fan-out badge label by dropping the trailing "(N todo)" parenthetical from the visible text while keeping the hover tooltip context intact. The badge count now inherits the same fan-out meta text color as the surrounding label.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { mkdtempSync, existsSync } from "node:fs";
|
import { mkdtempSync, existsSync, readdirSync } from "node:fs";
|
||||||
import { rm, readFile } from "node:fs/promises";
|
import { rm, readFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { isAbsolute, join } from "node:path";
|
import { isAbsolute, join } from "node:path";
|
||||||
|
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { TaskStore } from "../store.js";
|
import { TaskStore } from "../store.js";
|
||||||
import {
|
import {
|
||||||
@@ -127,6 +127,25 @@ describe("test-project fixture", () => {
|
|||||||
expect(fetched.description).toContain("Validate real TaskStore operations");
|
expect(fetched.description).toContain("Validate real TaskStore operations");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("cleans up auto-created temp dirs when setup fails before returning a fixture", async () => {
|
||||||
|
const countTmpDirs = (prefix: string) =>
|
||||||
|
readdirSync(tmpdir()).filter((entry) => entry.startsWith(prefix)).length;
|
||||||
|
|
||||||
|
const projectCountBefore = countTmpDirs("fusion-test-project-");
|
||||||
|
const globalCountBefore = countTmpDirs("fusion-test-global-");
|
||||||
|
const error = new Error("boom");
|
||||||
|
const spy = vi.spyOn(TaskStore.prototype, "updateSettings").mockRejectedValueOnce(error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(createTestProject()).rejects.toThrow(error);
|
||||||
|
} finally {
|
||||||
|
spy.mockRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(countTmpDirs("fusion-test-project-")).toBe(projectCountBefore);
|
||||||
|
expect(countTmpDirs("fusion-test-global-")).toBe(globalCountBefore);
|
||||||
|
});
|
||||||
|
|
||||||
it("createTestProject({ seedTasks }) pre-seeds tasks during setup", async () => {
|
it("createTestProject({ seedTasks }) pre-seeds tasks during setup", async () => {
|
||||||
const fixture = await createFixture({ seedTasks: 4 });
|
const fixture = await createFixture({ seedTasks: 4 });
|
||||||
|
|
||||||
|
|||||||
@@ -71,13 +71,17 @@ export async function createTestProject(
|
|||||||
const globalDir = options.globalSettingsDir
|
const globalDir = options.globalSettingsDir
|
||||||
? options.globalSettingsDir
|
? options.globalSettingsDir
|
||||||
: mkdtempSync(join(tmpdir(), "fusion-test-global-"));
|
: mkdtempSync(join(tmpdir(), "fusion-test-global-"));
|
||||||
|
const ownsGlobalDir = !options.globalSettingsDir;
|
||||||
|
|
||||||
assertAbsolutePath(rootDir, "rootDir");
|
assertAbsolutePath(rootDir, "rootDir");
|
||||||
assertAbsolutePath(globalDir, "globalSettingsDir");
|
assertAbsolutePath(globalDir, "globalSettingsDir");
|
||||||
|
|
||||||
|
let store: TaskStore | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
await mkdir(globalDir, { recursive: true });
|
await mkdir(globalDir, { recursive: true });
|
||||||
|
|
||||||
const store = new TaskStore(rootDir, globalDir);
|
store = new TaskStore(rootDir, globalDir);
|
||||||
await store.init();
|
await store.init();
|
||||||
|
|
||||||
const { globalPatch, projectPatch } = splitSettings(options.settings);
|
const { globalPatch, projectPatch } = splitSettings(options.settings);
|
||||||
@@ -92,16 +96,27 @@ export async function createTestProject(
|
|||||||
await seedTasks(store, requestedSeedCount);
|
await seedTasks(store, requestedSeedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initializedStore = store;
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
store.close();
|
initializedStore.close();
|
||||||
await destroyTestProject(rootDir);
|
await destroyTestProject(rootDir);
|
||||||
|
|
||||||
if (!options.globalSettingsDir) {
|
if (ownsGlobalDir) {
|
||||||
await destroyTestProject(globalDir);
|
await destroyTestProject(globalDir);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { rootDir, store, globalDir, cleanup };
|
return { rootDir, store: initializedStore, globalDir, cleanup };
|
||||||
|
} catch (error) {
|
||||||
|
store?.close();
|
||||||
|
await destroyTestProject(rootDir);
|
||||||
|
|
||||||
|
if (ownsGlobalDir) {
|
||||||
|
await destroyTestProject(globalDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -416,7 +416,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(var(--space-xs) / 2);
|
gap: calc(var(--space-xs) / 2);
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
color: var(--color-info);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@@ -432,23 +431,6 @@
|
|||||||
padding-inline: calc(var(--space-xs) / 2);
|
padding-inline: calc(var(--space-xs) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-fanout-badge--high-impact {
|
|
||||||
color: var(--color-warning);
|
|
||||||
background: color-mix(in srgb, var(--color-warning) 16%, transparent);
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
padding-inline: var(--space-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-fanout-badge--high-impact .card-fanout-count {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-fanout-badge--escalated {
|
|
||||||
color: var(--color-error);
|
|
||||||
background: color-mix(in srgb, var(--color-error) 16%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-scope-badge[data-tooltip]:hover::after,
|
.card-scope-badge[data-tooltip]:hover::after,
|
||||||
.card-fanout-badge[data-tooltip]:hover::after {
|
.card-fanout-badge[data-tooltip]:hover::after {
|
||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
@@ -562,9 +544,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-fanout-badge--high-impact {
|
|
||||||
padding-inline: calc(var(--space-xs) / 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-agent-badge--loading {
|
.card-agent-badge--loading {
|
||||||
|
|||||||
@@ -1649,14 +1649,13 @@ function TaskCardComponent({
|
|||||||
)}
|
)}
|
||||||
{fanout && fanout.totalCount > 0 && (
|
{fanout && fanout.totalCount > 0 && (
|
||||||
<span
|
<span
|
||||||
className={`card-fanout-badge${fanout.staleBlockedByDependentIds.length > 0 ? " card-fanout-badge--stale" : ""}${fanout.isHighFanout ? " card-fanout-badge--high-impact" : ""}${fanout.escalation ? " card-fanout-badge--escalated" : ""}`}
|
className={`card-fanout-badge${fanout.staleBlockedByDependentIds.length > 0 ? " card-fanout-badge--stale" : ""}`}
|
||||||
data-tooltip={`Blocking ${fanout.totalCount} active task(s); ${fanout.activeTodoCount} waiting in todo${fanout.isHighFanout ? ` (high fan-out threshold: ${HIGH_FANOUT_BLOCKER_TODO_THRESHOLD})` : ""}${fanout.escalation ? ` · escalated after ${Math.floor(fanout.escalation.blockingAgeMs / 60000)}m in blocking column` : ""}`}
|
data-tooltip={`Blocking ${fanout.totalCount} active task(s); ${fanout.activeTodoCount} waiting in todo${fanout.isHighFanout ? ` (high fan-out threshold: ${HIGH_FANOUT_BLOCKER_TODO_THRESHOLD})` : ""}${fanout.escalation ? ` · escalated after ${Math.floor(fanout.escalation.blockingAgeMs / 60000)}m in blocking column` : ""}`}
|
||||||
>
|
>
|
||||||
<GitBranch size={12} style={{ verticalAlign: "middle" }} />
|
<GitBranch size={12} style={{ verticalAlign: "middle" }} />
|
||||||
<span>
|
<span>
|
||||||
{fanout.escalation ? "Escalated" : fanout.isHighFanout ? "High fan-out" : "Blocks"}{" "}
|
{fanout.escalation ? "Escalated" : fanout.isHighFanout ? "High fan-out" : "Blocks"}{" "}
|
||||||
<span className="card-fanout-count">{fanout.totalCount}</span>
|
<span className="card-fanout-count">{fanout.totalCount}</span>
|
||||||
{fanout.isHighFanout ? ` (${fanout.activeTodoCount} todo)` : ""}
|
|
||||||
{fanout.staleBlockedByDependentIds.length > 0 ? ` (${fanout.staleBlockedByDependentIds.length} stale)` : ""}
|
{fanout.staleBlockedByDependentIds.length > 0 ? ` (${fanout.staleBlockedByDependentIds.length} stale)` : ""}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
|
const highFanout = {
|
||||||
|
totalCount: 7,
|
||||||
|
activeTodoCount: 3,
|
||||||
|
dependentIds: ["FN-002", "FN-003"],
|
||||||
|
staleBlockedByDependentIds: [],
|
||||||
|
isHighFanout: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
@@ -234,16 +242,32 @@ describe("TaskCard", () => {
|
|||||||
expect(badge.textContent).toContain("(1 stale)");
|
expect(badge.textContent).toContain("(1 stale)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders high fan-out badge without visible todo suffix while keeping tooltip context", () => {
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={makeTask({ column: "in-progress" })}
|
||||||
|
fanout={highFanout}
|
||||||
|
onOpenDetail={noop}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const badge = screen.getByText("High fan-out").closest(".card-fanout-badge") as HTMLElement;
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
expect(badge.textContent).toContain("High fan-out 7");
|
||||||
|
expect(badge.textContent).not.toContain("todo)");
|
||||||
|
expect(badge.getAttribute("data-tooltip")).toContain("3 waiting in todo");
|
||||||
|
});
|
||||||
|
|
||||||
it("escalates only threshold-crossing fan-out badges", () => {
|
it("escalates only threshold-crossing fan-out badges", () => {
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "in-progress" })}
|
task={makeTask({ column: "in-progress" })}
|
||||||
fanout={{
|
fanout={{
|
||||||
|
...highFanout,
|
||||||
totalCount: 8,
|
totalCount: 8,
|
||||||
activeTodoCount: 5,
|
activeTodoCount: 5,
|
||||||
dependentIds: ["FN-003"],
|
dependentIds: ["FN-003"],
|
||||||
staleBlockedByDependentIds: [],
|
|
||||||
isHighFanout: true,
|
|
||||||
escalation: { blockerId: "FN-001", activeTodoCount: 5, totalActiveCount: 8, blockingAgeMs: 3_600_000 },
|
escalation: { blockerId: "FN-001", activeTodoCount: 5, totalActiveCount: 8, blockingAgeMs: 3_600_000 },
|
||||||
}}
|
}}
|
||||||
onOpenDetail={noop}
|
onOpenDetail={noop}
|
||||||
@@ -251,11 +275,11 @@ describe("TaskCard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
let badge = document.querySelector(".card-fanout-badge--high-impact") as HTMLElement;
|
let badge = screen.getByText("Escalated").closest(".card-fanout-badge") as HTMLElement;
|
||||||
expect(badge).not.toBeNull();
|
expect(badge).not.toBeNull();
|
||||||
expect(badge).toHaveClass("card-fanout-badge--escalated");
|
|
||||||
expect(badge.textContent).toContain("Escalated");
|
expect(badge.textContent).toContain("Escalated");
|
||||||
expect(badge.textContent).toContain("(5 todo)");
|
expect(badge.textContent).toContain("8");
|
||||||
|
expect(badge.textContent).not.toContain("todo)");
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
@@ -267,7 +291,7 @@ describe("TaskCard", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
badge = screen.getByText("Blocks").closest(".card-fanout-badge") as HTMLElement;
|
badge = screen.getByText("Blocks").closest(".card-fanout-badge") as HTMLElement;
|
||||||
expect(badge).not.toHaveClass("card-fanout-badge--high-impact");
|
expect(badge).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows plain paused label when pausedByAgentId is not set", () => {
|
it("shows plain paused label when pausedByAgentId is not set", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user