diff --git a/.changeset/taskdetail-fanout-lanes.md b/.changeset/taskdetail-fanout-lanes.md new file mode 100644 index 0000000000..0a7e6ac530 --- /dev/null +++ b/.changeset/taskdetail-fanout-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Task Detail's "is blocking N todo task(s)" now counts your own lane names instead of reading zero. +category: fix +dev: `TaskDetailContent` takes an optional `columnFlagsByTaskId`, forwarded from `App` through `AppModals`; the fan-out useMemo passes it to the wrapper. Omitted, behaviour is byte-identical. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index aa714be5b3..bef0963ad4 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -2199,6 +2199,7 @@ function AppInner() { ; projects: ProjectInfo[]; currentProject: ProjectInfo | null; addToast: (message: string, type?: ToastType) => void; @@ -103,6 +106,7 @@ interface AppModalsProps { export function AppModals({ projectId, tasks, + columnFlagsByTaskId, projects, currentProject, addToast, @@ -320,6 +324,7 @@ export function AppModals({ task={detailTask} projectId={projectId} tasks={tasks} + columnFlagsByTaskId={columnFlagsByTaskId} onClose={closeDetailWithNav} onOpenDetail={openDetailTaskWithNav} mobileHeaderMode={modalManager.detailTaskOrigin === "list-mobile" ? "back" : "close"} diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index e3090889c6..bb466151c3 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -72,7 +72,7 @@ import type { SessionTerminalMode, SessionTerminalPosture } from "./SessionTermi import { usePluginUiSlots } from "../hooks/usePluginUiSlots"; import { appendTokenQuery } from "../auth"; import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete"; -import { MAX_AUTO_MERGE_RETRIES, computeBlockerFanoutMap } from "../hooks/useBlockerFanout"; +import { MAX_AUTO_MERGE_RETRIES, computeBlockerFanoutMap, type BlockerFanoutColumnFlags } from "../hooks/useBlockerFanout"; import { resolveEffectiveGithubRepoDefault } from "./githubTracking"; import type { TFunction } from "i18next"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; @@ -363,6 +363,8 @@ export interface TaskDetailModalProps { task: Task | TaskDetail; projectId?: string; tasks?: Task[]; + /* Per-task lifecycle traits for the blocker fan-out; see the useMemo that consumes it. */ + columnFlagsByTaskId?: ReadonlyMap; onClose: () => void; onOpenDetail: (task: Task | TaskDetail) => void; // For clicking dependencies onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise; @@ -722,6 +724,7 @@ export function TaskDetailContent({ task, projectId, tasks = [], + columnFlagsByTaskId, onOpenDetail, onMoveTask, onDeleteTask, @@ -3700,7 +3703,21 @@ export function TaskDetailContent({ return bNum - aNum; }); - const blockerFanoutMap = useMemo(() => computeBlockerFanoutMap(tasks), [tasks]); + /* + FNXC:WorkflowResolvedColumns 2026-07-30-23:20 (third fan-out surface): + Without resolved traits this classified against `todo`/`in-review`/`done`, so on a renamed board + the "blocking N todo task(s)" line counted zero and the `stale` marker on each blocking dependent + was decided against lanes the operator does not use. The dependent LIST itself is lane-independent + (core pushes `dependentIds` unconditionally), which is why the modal still looked broadly right — + only the count and the staleness were wrong. + + Optional: a card with no entry keeps the documented legacy fallback, so the remote-node case and + the pre-load window are byte-identical. + */ + const blockerFanoutMap = useMemo( + () => computeBlockerFanoutMap(tasks, columnFlagsByTaskId ? { columnFlagsByTaskId } : {}), + [tasks, columnFlagsByTaskId], + ); const blockingEntry = blockerFanoutMap.get(task.id); const blockingDependents = useMemo(() => { if (!blockingEntry) return [] as Array<{ id: string; label: string; stale: boolean }>; diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx index 6027b278dd..f0127473d5 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx @@ -1021,6 +1021,42 @@ describe("TaskDetailModal", () => { expect(screen.getByText("File scope overlap blocker: FN-OVER (stale)")).toBeInTheDocument(); }); + it("counts the overlap blockedBy summary using the board's OWN lane names", () => { + /* + FNXC:WorkflowResolvedColumns 2026-07-30-23:35: + The case below renamed and nothing else. Without resolved traits the count is taken against the + literal `todo`, which no card is in, so this line read "blocking 0 todo task(s)" while two cards + were in fact blocked. The dependent LIST stayed correct throughout (core builds it without + consulting lanes), which is what made the wrong number easy to miss. + */ + const tasks = [ + makeTask({ id: "FN-B", column: "building" }), + makeTask({ id: "FN-1", column: "drafting", blockedBy: "FN-B" }), + makeTask({ id: "FN-2", column: "drafting", blockedBy: "FN-B" }), + ]; + const columnFlagsByTaskId = new Map(tasks.map((task) => [ + task.id, + task.column === "building" ? { countsTowardWip: true } : { hold: true }, + ])); + + render( + , + ); + + expect(screen.getByText("FN-B is blocking 2 todo task(s) via blockedBy overlap")).toBeInTheDocument(); + }); + it("shows overlap blockedBy summary in Blocking section", () => { render(