FN-7406: fix All workflows dropdown counts
Ensure All workflows dropdown counters report the shared aggregate without double-counting tasks. - Reuse computeWorkflowStatusCounts directly in Board instead of re-summing a map that already includes the All workflows sentinel. - Document the aggregate count invariant and add a published patch changeset. - Extend Board, List, header, graph, and workflow count tests to cover exact All workflows totals. Files changed: .changeset/fn-7406-workflow-dropdown-counts.md | 7 +++ docs/dashboard-guide.md | 5 +- packages/dashboard/app/components/Board.tsx | 17 +++--- .../app/components/__tests__/Board.test.tsx | 56 ++++++++++++++++++- .../__tests__/GraphWorkflowSwitcherSlot.test.tsx | 8 ++- .../__tests__/HeaderWorkflowSwitcherSlot.test.tsx | 8 ++- .../app/components/__tests__/ListView.test.tsx | 62 ++++++++++++++++++++++ .../__tests__/workflowStatusCounts.test.ts | 48 +++++++++++++++-- 8 files changed, 188 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-7406 Fusion-Task-Lineage: bb344c03-c349-47ea-9e8c-c445975f8e02 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7406-workflow-dropdown-counts.md
Normal file
7
.changeset/fn-7406-workflow-dropdown-counts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix the All workflows dropdown counter so it no longer double-counts tasks.
|
||||
category: fix
|
||||
dev: Board now uses the shared workflow status count aggregate directly, with regression coverage for Board/List and header/graph selector count behavior.
|
||||
@@ -159,9 +159,10 @@ FNXC:TaskCardMobileSelection 2026-07-01-00:00: Mobile Board long-press is a task
|
||||
FNXC:WorkflowSelection 2026-06-30-00:00: The view preference persists either a real workflow id or the All workflows sentinel so refresh/remount restores the operator's last top-level workflow context without treating the sentinel as a backend workflow id.
|
||||
FNXC:WorkflowSelection 2026-07-01-00:00: All workflows is available on Board, List, Planning, Missions, and Graph top-level selectors; Planning/Missions task creation receives default/no-specific-workflow behavior instead of the sentinel. -->
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-23:58: All workflows quick-create must use a real workflow intake/default column rather than a synthesized lifecycle column, so custom-default boards do not create tasks into invalid or disappearing columns. -->
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-23:59: Workflow counts and All workflows grouping resolve each task's effective workflow before evaluating column visibility, so a shared column id hidden in one workflow does not leak that workflow's hidden tasks into another workflow's visible aggregate lane. -->
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-23:59: Workflow counts and All workflows grouping resolve each task's effective workflow before evaluating column visibility, so a shared column id hidden in one workflow does not leak that workflow's hidden tasks into another workflow's visible aggregate lane.
|
||||
FNXC:WorkflowSelection 2026-07-01-23:04: Board/List dropdown counts use computeWorkflowStatusCounts as the single source of truth. The All workflows row reports the helper-owned aggregate exactly once and must not be recomputed by summing the map that already contains the aggregate sentinel. -->
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-21:40: Refinement creation from Task Detail and done-task chat must preserve both the source task workflow and the operator's selected Board/List lane, so non-default workflow users do not get bounced back to Coding/default after refinement. -->
|
||||
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger shows the workflow identity (Fusion icon for built-ins, optional custom icon for custom workflows), name, and chevron only; compact Todo / In Progress / Done counts derived from workflow column flags (excluding archived and board-hidden columns) refresh each time the dropdown opens and appear while the dropdown is expanded, including on each workflow option. Built-in lanes with synthesized trait-less lifecycle columns fall back to canonical column ids (`todo`, `in-progress`, `done`, and `archived`) for those counts. Board and List also show **All workflows** before real workflows as a dashboard-only aggregate view with combined counts and a deterministic union of visible workflow columns; shared column ids use the default workflow label/flags when available, otherwise the first workflow definition that declares the column. Hidden columns stay workflow-scoped in the aggregate: a task whose effective workflow hides a shared column is omitted from that aggregate column even if another workflow exposes the same column id. That option is not editable, persists as top-level workflow view state, and quick-create/Plan/Subtask/Mission handoffs translate it to a real default workflow id or no-specific-workflow behavior so task creation never sends the sentinel. Each real workflow option row also exposes an inline edit action, and a persistent **New workflow** footer stays visible below the scrollable option list. The open listbox grows from the longest workflow name plus its count/edit decorations while remaining viewport-bounded; the closed trigger stays narrow and ellipsized. Those inline count badges intentionally use the same board column color tokens as cards: `--todo`, `--in-progress`, and `--done`.
|
||||
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger shows the workflow identity (Fusion icon for built-ins, optional custom icon for custom workflows), name, and chevron only; compact Todo / In Progress / Done counts derived from workflow column flags (excluding archived and board-hidden columns) refresh each time the dropdown opens and appear while the dropdown is expanded, including on each workflow option. Built-in lanes with synthesized trait-less lifecycle columns fall back to canonical column ids (`todo`, `in-progress`, `done`, and `archived`) for those counts. Board and List also show **All workflows** before real workflows as a dashboard-only aggregate view with combined counts that sum only the real visible workflow rows exactly once and a deterministic union of visible workflow columns; shared column ids use the default workflow label/flags when available, otherwise the first workflow definition that declares the column. Hidden columns stay workflow-scoped in the aggregate: a task whose effective workflow hides a shared column is omitted from that aggregate column even if another workflow exposes the same column id. That option is not editable, persists as top-level workflow view state, and quick-create/Plan/Subtask/Mission handoffs translate it to a real default workflow id or no-specific-workflow behavior so task creation never sends the sentinel. Each real workflow option row also exposes an inline edit action, and a persistent **New workflow** footer stays visible below the scrollable option list. The open listbox grows from the longest workflow name plus its count/edit decorations while remaining viewport-bounded; the closed trigger stays narrow and ellipsized. Those inline count badges intentionally use the same board column color tokens as cards: `--todo`, `--in-progress`, and `--done`.
|
||||
- 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. The selected workflow is remembered per project in durable browser storage and restored when returning to Board/List after task refreshes, route changes, respecification flows, or refinement creation from Task Detail and done-task chat; Board, List, Planning/Missions header selectors, and Graph also restore the dashboard-only **All workflows** aggregate view when that was the last selected top-level workflow context. If a saved real workflow is later deleted, Fusion falls back to a valid default/first workflow so tasks remain visible.
|
||||
- Briefly leaving Board/List for a task detail or another non-task-SSE view preserves the current in-memory task snapshot. Returning to Board/List reuses that fresh snapshot immediately and restores live SSE updates without an extra all-task fetch; Fusion still runs one catch-up fetch when task data is missing, stale, or from a failed refresh.
|
||||
<!-- FNXC:BoardTaskCache 2026-06-29-20:05: Board/List returns from non-task-SSE views should reuse a fresh in-memory task snapshot to avoid redundant all-task fetches and loading flashes, while stale, missing, or errored snapshots still trigger one catch-up fetch and restore SSE updates. -->
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MOBILE_MEDIA_QUERY, useViewportMode } from "../hooks/useViewportMode";
|
||||
import { recordResumeEvent } from "../utils/resumeInstrumentation";
|
||||
import { getBoardCanDropTaskRejection } from "./boardCanDropTask";
|
||||
import { WorkflowSwitcher } from "./WorkflowSwitcher";
|
||||
import { computeWorkflowStatusCounts, type WorkflowStatusCounts } from "./workflowStatusCounts";
|
||||
import { computeWorkflowStatusCounts } from "./workflowStatusCounts";
|
||||
import { writeBoardWorkflowsCache } from "../utils/boardWorkflowsCache";
|
||||
import { useBoardWorkflows } from "../hooks/useBoardWorkflows";
|
||||
import {
|
||||
@@ -400,16 +400,11 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o
|
||||
const getDraggingTaskId = useCallback(() => draggingTaskIdRef.current, []);
|
||||
|
||||
const workflowStatusCounts = useMemo(() => {
|
||||
const counts = computeWorkflowStatusCounts(tasks, boardWorkflows);
|
||||
const aggregateCounts: WorkflowStatusCounts = { todo: 0, inProgress: 0, done: 0, merging: 0 };
|
||||
for (const workflowCounts of counts.values()) {
|
||||
aggregateCounts.todo += workflowCounts.todo;
|
||||
aggregateCounts.inProgress += workflowCounts.inProgress;
|
||||
aggregateCounts.done += workflowCounts.done;
|
||||
aggregateCounts.merging += workflowCounts.merging;
|
||||
}
|
||||
counts.set(ALL_WORKFLOWS_BOARD_VIEW_ID, aggregateCounts);
|
||||
return counts;
|
||||
/*
|
||||
FNXC:WorkflowSwitcher 2026-07-01-23:04:
|
||||
computeWorkflowStatusCounts already owns the dashboard-only All workflows aggregate sentinel. Board must pass the helper result through directly instead of re-summing every map entry, because the map includes the sentinel and summing it again doubles the dropdown aggregate row.
|
||||
*/
|
||||
return computeWorkflowStatusCounts(tasks, boardWorkflows);
|
||||
}, [boardWorkflows, tasks]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
||||
import { Board } from "../Board";
|
||||
import { COLUMNS } from "@fusion/core";
|
||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID, BOARD_WORKFLOW_SELECTION_STORAGE_KEY } from "../../utils/boardWorkflowSelection";
|
||||
@@ -1472,6 +1472,60 @@ describe("Board", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("renders the all-workflows dropdown count as a single aggregate of real workflow rows", async () => {
|
||||
enableFlag(
|
||||
{
|
||||
"FN-default-todo": "builtin:coding",
|
||||
"FN-default-active": "builtin:coding",
|
||||
"FN-default-done": "builtin:coding",
|
||||
"FN-custom-todo": "wf-custom",
|
||||
"FN-custom-done": "wf-custom",
|
||||
"FN-stale": "wf-deleted",
|
||||
"FN-hidden": "builtin:coding",
|
||||
"FN-archived": "builtin:coding",
|
||||
},
|
||||
[
|
||||
{
|
||||
...DEFAULT_WORKFLOW,
|
||||
columns: [
|
||||
...DEFAULT_WORKFLOW.columns,
|
||||
{ id: "quiet", name: "Quiet", flags: { hiddenFromBoard: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
...CUSTOM_WORKFLOW,
|
||||
name: "Coding",
|
||||
},
|
||||
],
|
||||
);
|
||||
renderBoard({
|
||||
tasks: [
|
||||
mkTask({ id: "FN-default-todo", column: "todo" }),
|
||||
mkTask({ id: "FN-default-active", column: "in-progress", status: "merging" }),
|
||||
mkTask({ id: "FN-default-done", column: "done" }),
|
||||
mkTask({ id: "FN-custom-todo", column: "intake" }),
|
||||
mkTask({ id: "FN-custom-done", column: "done" }),
|
||||
mkTask({ id: "FN-stale", column: "todo" }),
|
||||
mkTask({ id: "FN-hidden", column: "quiet" }),
|
||||
mkTask({ id: "FN-archived", column: "archived" }),
|
||||
],
|
||||
});
|
||||
|
||||
await openWorkflowSwitcher();
|
||||
const aggregateOption = screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`);
|
||||
expect(aggregateOption).toHaveTextContent("All workflows");
|
||||
expect(within(aggregateOption).getByTitle("Todo: 3")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("In Progress: 1")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("Done: 2")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("1 merging")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("Todo: 2")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("In Progress: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("Done: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("Todo: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("In Progress: 0")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("Done: 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders one selected workflow at a time and switches workflows from the dropdown", async () => {
|
||||
const onCreateWorkflow = vi.fn();
|
||||
const onOpenWorkflowEditor = vi.fn();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BoardWorkflowDefinition, BoardWorkflowsPayload } from "../../api";
|
||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../../utils/boardWorkflowSelection";
|
||||
@@ -220,7 +220,11 @@ describe("GraphWorkflowSwitcherSlot", () => {
|
||||
render(<GraphWorkflowSwitcherSlot projectId="project-graph-all" onWorkflowSelectionChange={onWorkflowSelectionChange} onOpenWorkflowEditor={onOpenWorkflowEditor} />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId("workflow-switcher"));
|
||||
expect(screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toHaveTextContent("All workflows");
|
||||
const aggregateOption = screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`);
|
||||
expect(aggregateOption).toHaveTextContent("All workflows");
|
||||
expect(within(aggregateOption).getByTitle("Todo: 0")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("In Progress: 0")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("Done: 0")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`workflow-switcher-edit-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toBeNull();
|
||||
fireEvent.click(screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`));
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ The shared header workflow slot is the canonical desktop workflow selector for P
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BoardWorkflowDefinition, BoardWorkflowsPayload } from "../../api";
|
||||
import { HeaderWorkflowSwitcherSlot, type HeaderWorkflowSelection } from "../HeaderWorkflowSwitcherSlot";
|
||||
@@ -144,7 +144,11 @@ describe("HeaderWorkflowSwitcherSlot", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByTestId("workflow-switcher"));
|
||||
expect(screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toHaveTextContent("All workflows");
|
||||
const aggregateOption = screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`);
|
||||
expect(aggregateOption).toHaveTextContent("All workflows");
|
||||
expect(within(aggregateOption).getByTitle("Todo: 0")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("In Progress: 0")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("Done: 0")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(`workflow-switcher-edit-${ALL_WORKFLOWS_BOARD_VIEW_ID}`)).toBeNull();
|
||||
fireEvent.click(screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`));
|
||||
|
||||
|
||||
@@ -1201,6 +1201,68 @@ describe("ListView", () => {
|
||||
mobileSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("shows the same single aggregate all-workflows dropdown count in ListView", async () => {
|
||||
vi.mocked(fetchBoardWorkflows).mockResolvedValue({
|
||||
flagEnabled: true,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
workflows: [
|
||||
{
|
||||
id: "builtin:coding",
|
||||
name: "Coding",
|
||||
columns: [
|
||||
{ id: "triage", name: "Triage", flags: { intake: true } },
|
||||
{ id: "in-progress", name: "In Progress", flags: { countsTowardWip: true } },
|
||||
{ id: "done", name: "Done", flags: { complete: true } },
|
||||
{ id: "archived", name: "Archived", flags: { archived: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "wf-custom",
|
||||
name: "Coding",
|
||||
columns: [
|
||||
{ id: "backlog", name: "Backlog", flags: { intake: true } },
|
||||
{ id: "complete", name: "Complete", flags: { complete: true } },
|
||||
{ id: "hidden", name: "Hidden", flags: { hiddenFromBoard: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
taskWorkflowIds: {
|
||||
"FN-001": "builtin:coding",
|
||||
"FN-002": "builtin:coding",
|
||||
"FN-003": "wf-custom",
|
||||
"FN-004": "wf-custom",
|
||||
"FN-005": "missing-workflow",
|
||||
"FN-006": "wf-custom",
|
||||
"FN-007": "builtin:coding",
|
||||
},
|
||||
});
|
||||
|
||||
renderListView({
|
||||
tasks: [
|
||||
createMockTask({ id: "FN-001", column: "triage", title: "Coding todo" }),
|
||||
createMockTask({ id: "FN-002", column: "in-progress", title: "Coding active", status: "merging" }),
|
||||
createMockTask({ id: "FN-003", column: "backlog", title: "Custom todo" }),
|
||||
createMockTask({ id: "FN-004", column: "complete", title: "Custom done" }),
|
||||
createMockTask({ id: "FN-005", column: "done", title: "Stale done" }),
|
||||
createMockTask({ id: "FN-006", column: "hidden", title: "Hidden custom" }),
|
||||
createMockTask({ id: "FN-007", column: "archived", title: "Archived coding" }),
|
||||
],
|
||||
});
|
||||
|
||||
await openWorkflowSwitcher();
|
||||
const aggregateOption = screen.getByTestId(`workflow-switcher-option-${ALL_WORKFLOWS_BOARD_VIEW_ID}`);
|
||||
expect(within(aggregateOption).getByTitle("Todo: 2")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("In Progress: 1")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("Done: 2")).toBeInTheDocument();
|
||||
expect(within(aggregateOption).getByTitle("1 merging")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("Todo: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("In Progress: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-builtin:coding")).getByTitle("Done: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("Todo: 1")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("In Progress: 0")).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId("workflow-switcher-option-wf-custom")).getByTitle("Done: 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows all workflows in ListView without submitting the aggregate sentinel", async () => {
|
||||
const mockOnQuickCreate = vi.fn().mockResolvedValue({ id: "FN-new" });
|
||||
vi.mocked(fetchBoardWorkflows).mockResolvedValue({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type Task,
|
||||
} from "@fusion/core";
|
||||
import type { BoardWorkflowColumn, BoardWorkflowsPayload } from "../../api";
|
||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../../utils/boardWorkflowSelection";
|
||||
import { computeWorkflowStatusCounts } from "../workflowStatusCounts";
|
||||
|
||||
const boardWorkflows: BoardWorkflowsPayload = {
|
||||
@@ -110,12 +111,13 @@ describe("computeWorkflowStatusCounts", () => {
|
||||
expect(computeWorkflowStatusCounts(undefined, undefined).size).toBe(0);
|
||||
});
|
||||
|
||||
it("initializes every workflow with zero counts for empty and duplicate/populated states", () => {
|
||||
it("initializes every workflow and the aggregate sentinel with zero counts for empty and duplicate/populated states", () => {
|
||||
const counts = computeWorkflowStatusCounts([], boardWorkflows);
|
||||
|
||||
expect(counts.get("default")).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
expect(counts.get("design")).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
expect(counts.get("empty")).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
expect(counts.get(ALL_WORKFLOWS_BOARD_VIEW_ID)).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
});
|
||||
|
||||
it("classifies todo, in-progress, and done buckets from workflow column flags", () => {
|
||||
@@ -176,25 +178,61 @@ describe("computeWorkflowStatusCounts", () => {
|
||||
expect(counts.get("default")).toEqual({ todo: 0, inProgress: 0, done: 1, merging: 0 });
|
||||
});
|
||||
|
||||
it("counts tasks independently for their assigned workflow", () => {
|
||||
it("counts tasks independently for their assigned workflow and aggregates real rows exactly once", () => {
|
||||
const duplicateNameWorkflow = {
|
||||
id: "design-copy",
|
||||
name: "Design",
|
||||
columns: [
|
||||
{ id: "copy-todo", name: "Todo", flags: { intake: true } },
|
||||
{ id: "copy-active", name: "Active", flags: { countsTowardWip: true } },
|
||||
{ id: "copy-done", name: "Done", flags: { complete: true } },
|
||||
],
|
||||
};
|
||||
const counts = computeWorkflowStatusCounts(
|
||||
[
|
||||
task("FN-default-todo", "todo"),
|
||||
task("FN-missing-assignment", "ready"),
|
||||
task("FN-stale-assignment", "done"),
|
||||
task("FN-hidden", "quiet"),
|
||||
task("FN-archived", "archived"),
|
||||
task("FN-unknown-column", "missing"),
|
||||
task("FN-design-todo", "design-todo"),
|
||||
task("FN-design-active", "design-active"),
|
||||
taskWithStatus("FN-design-active", "design-active", "merging"),
|
||||
task("FN-design-done", "design-done"),
|
||||
taskWithStatus("FN-copy-active", "copy-active", "merging-fix"),
|
||||
],
|
||||
{
|
||||
...boardWorkflows,
|
||||
workflows: [
|
||||
{
|
||||
...boardWorkflows.workflows[0],
|
||||
columns: [
|
||||
...boardWorkflows.workflows[0].columns,
|
||||
{ id: "quiet", name: "Quiet", flags: { hiddenFromBoard: true } },
|
||||
],
|
||||
},
|
||||
boardWorkflows.workflows[1],
|
||||
duplicateNameWorkflow,
|
||||
boardWorkflows.workflows[2],
|
||||
],
|
||||
taskWorkflowIds: {
|
||||
"FN-stale-assignment": "deleted-workflow",
|
||||
"FN-hidden": "default",
|
||||
"FN-archived": "default",
|
||||
"FN-unknown-column": "design",
|
||||
"FN-design-todo": "design",
|
||||
"FN-design-active": "design",
|
||||
"FN-design-done": "design",
|
||||
"FN-copy-active": "design-copy",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(counts.get("design")).toEqual({ todo: 1, inProgress: 1, done: 1, merging: 0 });
|
||||
expect(counts.get("default")).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
expect(counts.get("default")).toEqual({ todo: 2, inProgress: 0, done: 1, merging: 0 });
|
||||
expect(counts.get("design")).toEqual({ todo: 1, inProgress: 1, done: 1, merging: 1 });
|
||||
expect(counts.get("design-copy")).toEqual({ todo: 0, inProgress: 1, done: 0, merging: 1 });
|
||||
expect(counts.get("empty")).toEqual({ todo: 0, inProgress: 0, done: 0, merging: 0 });
|
||||
expect(counts.get(ALL_WORKFLOWS_BOARD_VIEW_ID)).toEqual({ todo: 3, inProgress: 2, done: 2, merging: 2 });
|
||||
});
|
||||
|
||||
it("tracks actively merging tasks per workflow separately from bucket counts", () => {
|
||||
|
||||
Reference in New Issue
Block a user