diff --git a/services/jtype-web/frontend/src/pages/Kanban.tsx b/services/jtype-web/frontend/src/pages/Kanban.tsx index 05da072..d46e25d 100644 --- a/services/jtype-web/frontend/src/pages/Kanban.tsx +++ b/services/jtype-web/frontend/src/pages/Kanban.tsx @@ -152,6 +152,7 @@ export function Kanban() { columns: board.columns.slice().sort((a, b) => a.position - b.position).map(c => ({ key: c.id, name: c.name, color: c.color, limit: c.wipLimit })), groupBy: (view.groupBy as BoardViewConfig['groupBy']) ?? 'status', viewType: view.viewType ?? 'board', + swimlaneBy: view.swimlaneBy, colorColumns: view.colorColumns, doneColumn: view.doneColumn, } diff --git a/services/jtype-web/frontend/src/pages/WebBoardView.tsx b/services/jtype-web/frontend/src/pages/WebBoardView.tsx index 1758f44..eb43797 100644 --- a/services/jtype-web/frontend/src/pages/WebBoardView.tsx +++ b/services/jtype-web/frontend/src/pages/WebBoardView.tsx @@ -25,6 +25,7 @@ type BoardConfigJSON = { doneColumn?: string colorColumns?: boolean viewType?: 'board' | 'table' + swimlaneBy?: 'status' | 'priority' | 'assignee' } function rand() { @@ -180,6 +181,7 @@ export function WebBoardView({ doneColumn: config.doneColumn, colorColumns: config.colorColumns, viewType: config.viewType, + swimlaneBy: config.swimlaneBy as BoardViewConfig['swimlaneBy'], groupBy: (config.groupBy as BoardViewConfig['groupBy']) || 'status', } : { title: boardDir, columns: [] }, diff --git a/shared/components/board/BoardSurface.tsx b/shared/components/board/BoardSurface.tsx index 83eaa5a..fa51154 100644 --- a/shared/components/board/BoardSurface.tsx +++ b/shared/components/board/BoardSurface.tsx @@ -24,6 +24,7 @@ import { MagnifyingGlassIcon, FunnelIcon, BarsArrowDownIcon, + Bars3Icon, RectangleGroupIcon, XMarkIcon, TableCellsIcon, @@ -49,6 +50,7 @@ import { } from "../../lib/board"; import { BoardPeek } from "./BoardPeek"; import { BoardTable } from "./BoardTable"; +import { BoardSwimlanes } from "./BoardSwimlanes"; import type { BoardSurfaceProps } from "./types"; type DropTarget = { col: string; index: number }; @@ -118,6 +120,15 @@ export function BoardSurface({ () => effectiveColumns(config, cards, groupKey, t`Unassigned`), [config, cards, groupKey], ); + // Swimlanes: a second grouping dimension rendered as rows (must differ from + // the column dimension). Only meaningful in the board view. + const swimlaneKey: BoardGroupKey | null = + config.swimlaneBy && config.swimlaneBy !== groupKey ? config.swimlaneBy : null; + const swimlaneActive = viewType === "board" && !!swimlaneKey; + const lanes = useMemo( + () => (swimlaneKey ? effectiveColumns(config, cards, swimlaneKey, t`Unassigned`) : []), + [config, cards, swimlaneKey], + ); const vis = useMemo(() => visibleCardsFn(cards, search, filter), [cards, search, filter]); // Blocker counts resolve against ALL cards (a blocker may be filtered out of view). const blockers = useMemo(() => blockedCounts(cards, config.doneColumn), [cards, config.doneColumn]); @@ -344,6 +355,25 @@ export function BoardSurface({ )} + {viewType === "board" && ( + + )}