FN-6806: show workflow counts only when expanded

Keep workflow dropdown triggers compact until the menu opens.

- Hide Todo, In Progress, and Done count badges from the collapsed workflow switcher trigger.
- Preserve active-workflow and option counts while the dropdown is expanded for comparison.
- Update Board, List, switcher tests, and dashboard docs for the expanded-only count behavior.

Files changed:
 docs/dashboard-guide.md                            |  2 +-
 .../dashboard/app/components/WorkflowSwitcher.tsx  |  8 +++--
 .../app/components/__tests__/Board.test.tsx        |  3 +-
 .../app/components/__tests__/ListView.test.tsx     |  5 ++-
 .../components/__tests__/WorkflowSwitcher.test.tsx | 36 +++++++++++++++++-----
 5 files changed, 41 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-6806

Fusion-Task-Lineage: a3c2db33-fe08-4207-9aae-548b292d98bd
This commit is contained in:
gsxdsm
2026-06-20 19:36:01 -07:00
parent 7fa3cf17f6
commit 5d9162e138
5 changed files with 41 additions and 13 deletions

View File

@@ -77,7 +77,7 @@ Features:
- Task card header meta badges group priority, fast mode, agent-created provenance, and elapsed/created-time chips into one wrapping row; agent labels prefer `sourceMetadata.agentName` over raw agent IDs
- Column ordering semantics: `todo` mirrors scheduler pickup order (priority descending, then oldest `createdAt`, then task ID); `triage`, `in-progress`, `in-review`, and `archived` remain priority-first with task-ID tie-breaks; `done` is ordered by most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback)
- On mobile, both default and workflow-mode boards fill the project viewport while the column strip remains the internal horizontal scroller with contained edge overscroll.
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger and each workflow option show compact Todo / In Progress / Done counts derived from workflow column flags, excluding archived columns.
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger shows the workflow name and chevron only; compact Todo / In Progress / Done counts derived from workflow column flags (excluding archived columns) appear while the dropdown is expanded, including on each workflow option.
- When workflow columns are enabled, Board and List hydrate the last successful workflow-lane payload from a per-project session cache; cold loads show a neutral skeleton until settings and workflow metadata are known, avoiding a legacy single-lane flash.
![Board view](./screenshots/dashboard-overview.png)

View File

@@ -32,6 +32,10 @@ function getCounts(counts: Map<string, WorkflowStatusCounts>, workflowId: string
* FNXC:WorkflowSwitcher 2026-06-20-00:09:
* The board/list workflow switcher must be a fully rendered themed dropdown rather than a native select so each workflow option can include compact inline Todo, In Progress, and Done counts.
* The component owns only presentation and accessible dropdown behavior; all status-bucket semantics stay in computeWorkflowStatusCounts so Board and ListView cannot drift.
*
* FNXC:WorkflowSwitcher 2026-06-20-00:31:
* Counts are contextual detail, so the collapsed trigger must stay visually and accessibly scoped to the active workflow name plus chevron.
* Render Todo, In Progress, and Done counts only while the dropdown is expanded; option rows keep their count text because the listbox is the comparison surface.
*/
export function WorkflowSwitcher({ workflows, value, onChange, counts, label: labelProp }: WorkflowSwitcherProps) {
const { t } = useTranslation("app");
@@ -259,8 +263,8 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
>
<span className="workflow-switcher-trigger-main">
<span className="workflow-switcher-current-name">{selectedWorkflow.name}</span>
{renderCountBadges(selectedCounts, "trigger")}
{renderAccessibleCounts(selectedCounts)}
{isOpen ? renderCountBadges(selectedCounts, "trigger") : null}
{isOpen ? renderAccessibleCounts(selectedCounts) : null}
</span>
<ChevronDown className="workflow-switcher-chevron" aria-hidden="true" />
</button>

View File

@@ -1114,8 +1114,9 @@ describe("Board", () => {
});
const selector = await screen.findByTestId("workflow-switcher");
expect(selector).toHaveTextContent("Coding");
expect(selector).toHaveTextContent("1");
expect(selector.querySelector(".workflow-switcher-counts")).toBeNull();
await openWorkflowSwitcher();
expect(selector).toHaveTextContent("1");
expect(screen.getByTestId("workflow-switcher-option-wf-custom")).toHaveTextContent("2");
fireEvent.keyDown(selector, { key: "Escape" });
expect(workflowToolbarActionNames()).toEqual(["Edit workflows", "New workflow"]);

View File

@@ -747,8 +747,9 @@ describe("ListView", () => {
const desktopTrigger = await screen.findByTestId("workflow-switcher");
expect(desktopTrigger).toHaveTextContent("Coding");
expect(desktopTrigger).toHaveTextContent("1");
expect(desktopTrigger.querySelector(".workflow-switcher-counts")).toBeNull();
await openWorkflowSwitcher();
expect(desktopTrigger).toHaveTextContent("1");
expect(screen.getByTestId("workflow-switcher-option-wf-custom")).toHaveTextContent("1");
fireEvent.keyDown(desktopTrigger, { key: "Escape" });
desktop.unmount();
@@ -765,6 +766,8 @@ describe("ListView", () => {
const mobileTrigger = await screen.findByTestId("workflow-switcher");
expect(mobileTrigger).toHaveTextContent("Coding");
expect(mobileTrigger.querySelector(".workflow-switcher-counts")).toBeNull();
await openWorkflowSwitcher();
expect(mobileTrigger).toHaveTextContent("1");
mobileSpy.mockRestore();
});

View File

@@ -22,7 +22,7 @@ function countMap(entries: Array<[string, WorkflowStatusCounts]> = []) {
}
describe("WorkflowSwitcher", () => {
it("renders the active workflow with compact inline counts", () => {
it("renders the active workflow without compact counts while collapsed", () => {
render(
<WorkflowSwitcher
workflows={workflows}
@@ -34,9 +34,10 @@ describe("WorkflowSwitcher", () => {
const trigger = screen.getByTestId("workflow-switcher");
expect(trigger).toHaveTextContent("Coding");
expect(trigger).toHaveTextContent("3");
expect(trigger).toHaveTextContent("1");
expect(trigger).toHaveTextContent("5");
expect(within(trigger).queryByText("3", { selector: ".workflow-switcher-count--todo" })).not.toBeInTheDocument();
expect(within(trigger).queryByText("1", { selector: ".workflow-switcher-count--in-progress" })).not.toBeInTheDocument();
expect(within(trigger).queryByText("5", { selector: ".workflow-switcher-count--done" })).not.toBeInTheDocument();
expect(trigger.querySelector(".workflow-switcher-counts--trigger")).toBeNull();
expect(trigger).toHaveAccessibleName("Select workflow. Current workflow: Coding");
});
@@ -79,12 +80,31 @@ describe("WorkflowSwitcher", () => {
expect(screen.queryByRole("listbox", { name: "Workflow" })).not.toBeInTheDocument();
});
it("renders zero counts for workflows absent from the counts map", () => {
render(<WorkflowSwitcher workflows={workflows} value="coding" onChange={vi.fn()} counts={countMap()} />);
it("renders populated and zero counts only after the dropdown expands", () => {
render(
<WorkflowSwitcher
workflows={workflows}
value="coding"
onChange={vi.fn()}
counts={countMap([["coding", { todo: 3, inProgress: 1, done: 5 }]])}
/>,
);
const trigger = screen.getByTestId("workflow-switcher");
expect(trigger.querySelector(".workflow-switcher-counts")).not.toBeInTheDocument();
fireEvent.click(trigger);
expect(within(trigger).getByText("3", { selector: ".workflow-switcher-count--todo" })).toBeInTheDocument();
expect(within(trigger).getByText("1", { selector: ".workflow-switcher-count--in-progress" })).toBeInTheDocument();
expect(within(trigger).getByText("5", { selector: ".workflow-switcher-count--done" })).toBeInTheDocument();
const codingOption = screen.getByTestId("workflow-switcher-option-coding");
expect(within(codingOption).getByText("3", { selector: ".workflow-switcher-count--todo" })).toBeInTheDocument();
expect(within(codingOption).getByText("1", { selector: ".workflow-switcher-count--in-progress" })).toBeInTheDocument();
expect(within(codingOption).getByText("5", { selector: ".workflow-switcher-count--done" })).toBeInTheDocument();
fireEvent.click(screen.getByTestId("workflow-switcher"));
const designOption = screen.getByTestId("workflow-switcher-option-design");
expect(within(designOption).getByText("0", { selector: ".workflow-switcher-count--todo" })).toBeInTheDocument();
expect(within(designOption).getByText("0", { selector: ".workflow-switcher-count--in-progress" })).toBeInTheDocument();
expect(within(designOption).getByText("0", { selector: ".workflow-switcher-count--done" })).toBeInTheDocument();