@burdenoff/microfe-planmagnet 2026.927.6 → 2026.928.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +58 -54
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
  5. package/dist/planmagnet/board/ProjectBoard.js +313 -306
  6. package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
  7. package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
  8. package/dist/planmagnet/controls/ProjectControls.js +47 -40
  9. package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
  10. package/dist/planmagnet/controls/WorkItemControls.d.ts +9 -0
  11. package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
  12. package/dist/planmagnet/controls/WorkItemControls.js +87 -59
  13. package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
  14. package/dist/planmagnet/hooks/useProjectWorkflow.d.ts +32 -0
  15. package/dist/planmagnet/hooks/useProjectWorkflow.d.ts.map +1 -0
  16. package/dist/planmagnet/hooks/useProjectWorkflow.js +44 -0
  17. package/dist/planmagnet/hooks/useProjectWorkflow.js.map +1 -0
  18. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  19. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +4 -4
  20. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  21. package/dist/planmagnet/pages/ProjectsWorkflowPagePage.d.ts +2 -0
  22. package/dist/planmagnet/pages/ProjectsWorkflowPagePage.d.ts.map +1 -0
  23. package/dist/planmagnet/pages/ProjectsWorkflowPagePage.js +261 -0
  24. package/dist/planmagnet/pages/ProjectsWorkflowPagePage.js.map +1 -0
  25. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  26. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +8 -9
  27. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  28. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectBoard.js","names":[],"sources":["../../../src/planmagnet/board/ProjectBoard.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { ChevronLeft, ChevronRight, LayoutGrid, Pencil, Plus } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * A project's board (P1.2). The server owns placement: each column's `cards`\n * follow the rule in wspace-planmagnet-svc board.service.ts `columnCardsWhere`\n * (placed column, else the first column mapped to the item's status), and\n * `unplacedCount` says how many items are on no column. Moving a card into a\n * column mapped to a status changes that status on the server, and a column's\n * WIP limit is enforced there too. This component only draws and asks.\n */\n\nconst STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nconst statusLabel = (status: string) =>\n status.charAt(0) + status.slice(1).toLowerCase().replace(/_/g, ' ');\n\nconst CARDS_PER_COLUMN = 100;\n\nconst BOARDS = gql`\n query ProjectBoards($workspaceId: ID!, $projectId: String) {\n planmagnetBoards(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n isDefault\n }\n }\n }\n`;\n\n/** The next page of one column (P1.2: a column shows 100 cards and used to say only \"+N more\"). */\nconst COLUMN_PAGE = gql`\n query ProjectBoardColumnPage($boardId: ID!, $columnId: ID!, $after: String) {\n planmagnetBoardColumnCards(\n boardId: $boardId\n columnId: $columnId\n pagination: { first: 100, after: $after }\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n key\n title\n status\n priority\n storyPoints\n assigneeId\n statusSince\n }\n }\n }\n`;\n\nconst BOARD = gql`\n query ProjectBoard($id: ID!) {\n planmagnetBoard(id: $id) {\n id\n name\n unplacedCount\n columns {\n id\n name\n mappedStatus\n wipLimit\n sortOrder\n cards(pagination: { first: 100 }) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n key\n title\n status\n priority\n storyPoints\n assigneeId\n statusSince\n }\n }\n }\n }\n }\n`;\n\nconst MOVE_CARD = gql`\n mutation ProjectBoardMoveCard($boardId: ID!, $input: PlanmagnetMoveCardInput!) {\n planmagnetMoveCard(boardId: $boardId, input: $input) {\n id\n }\n }\n`;\nconst CREATE_BOARD = gql`\n mutation ProjectBoardCreate($input: PlanmagnetCreateBoardInput!) {\n planmagnetCreateBoard(input: $input) {\n id\n }\n }\n`;\nconst CREATE_COLUMN = gql`\n mutation ProjectBoardCreateColumn($input: PlanmagnetCreateBoardColumnInput!) {\n planmagnetCreateBoardColumn(input: $input) {\n id\n }\n }\n`;\nconst UPDATE_COLUMN = gql`\n mutation ProjectBoardUpdateColumn($id: ID!, $input: PlanmagnetUpdateBoardColumnInput!) {\n planmagnetUpdateBoardColumn(id: $id, input: $input) {\n id\n }\n }\n`;\nconst DELETE_COLUMN = gql`\n mutation ProjectBoardDeleteColumn($id: ID!) {\n planmagnetDeleteBoardColumn(id: $id)\n }\n`;\nconst REORDER_COLUMNS = gql`\n mutation ProjectBoardReorder($boardId: ID!, $input: PlanmagnetReorderColumnsInput!) {\n planmagnetReorderColumns(boardId: $boardId, input: $input) {\n id\n }\n }\n`;\n\n/** Every query whose numbers a board change can move. */\nconst REFETCH = [\n 'ProjectBoard',\n 'ProjectBoards',\n 'GetWorkItems',\n 'GetWorkItemCounts',\n 'GetDashboardData',\n];\n\ninterface BoardCard {\n id: string;\n key: string;\n title: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n /** When it entered its current status; null when unknown (changed by a path that records no history). */\n statusSince: string | null;\n}\n\ninterface PageInfo {\n hasNextPage: boolean;\n endCursor: string | null;\n}\n\ninterface BoardColumn {\n id: string;\n name: string;\n mappedStatus: string | null;\n wipLimit: number | null;\n sortOrder: number;\n cards: { totalCount: number; pageInfo: PageInfo; items: BoardCard[] };\n}\n\ntype LaneBy = 'none' | 'assignee' | 'priority';\nconst PRIORITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'NONE'];\nconst DAY_MS = 86_400_000;\n\n/** Whole days in the current status, or null when unknown. */\nexport function daysInStatus(statusSince: string | null, now: number): number | null {\n if (!statusSince) return null;\n return Math.max(0, Math.floor((now - new Date(statusSince).getTime()) / DAY_MS));\n}\n\n/** Card aging: quiet under a week, a warning from 7 days, stronger from 14. Semantic tokens only. */\nfunction ageClass(days: number): string {\n if (days >= 14) return 'text-destructive font-medium';\n if (days >= 7) return 'text-status-warning-text';\n return 'text-muted-foreground';\n}\n\nfunction laneOf(card: BoardCard, by: LaneBy): string {\n if (by === 'assignee') return card.assigneeId ?? '';\n if (by === 'priority') return card.priority || 'NONE';\n return '';\n}\n\ninterface BoardData {\n id: string;\n name: string;\n unplacedCount: number;\n columns: BoardColumn[];\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function ProjectBoard({\n projectId,\n projectName,\n}: {\n projectId: string;\n projectName: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const client = useApolloClient();\n const [chosenBoardId, setChosenBoardId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n // Card age is read against the moment the board opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n const [laneBy, setLaneBy] = useState<LaneBy>('none');\n // Pages loaded past each column's first 100, keyed by the board data they extend: a refetch starts over.\n const [more, setMore] = useState<{\n key: unknown;\n byColumn: Record<string, { items: BoardCard[]; pageInfo: PageInfo }>;\n }>({\n key: null,\n byColumn: {},\n });\n const [loadingMore, setLoadingMore] = useState<string | null>(null);\n\n const { data: boardsData, loading: boardsLoading } = useQuery<{\n planmagnetBoards: { items: Array<{ id: string; name: string; isDefault: boolean }> };\n }>(BOARDS, { variables: { workspaceId, projectId }, skip: !workspaceId });\n const boards = boardsData?.planmagnetBoards.items ?? [];\n const boardId =\n (chosenBoardId && boards.some((b) => b.id === chosenBoardId) ? chosenBoardId : null) ??\n boards.find((b) => b.isDefault)?.id ??\n boards[0]?.id ??\n null;\n\n const { data, loading } = useQuery<{ planmagnetBoard: BoardData | null }>(BOARD, {\n variables: { id: boardId },\n skip: !boardId,\n });\n const board = data?.planmagnetBoard ?? null;\n const extra = more.key === board ? more.byColumn : {};\n /** A column with every page loaded so far. */\n const cardsOf = (column: BoardColumn) => {\n const loaded = extra[column.id];\n return {\n items: loaded ? [...column.cards.items, ...loaded.items] : column.cards.items,\n pageInfo: loaded ? loaded.pageInfo : column.cards.pageInfo,\n totalCount: column.cards.totalCount,\n };\n };\n const loadMore = async (column: BoardColumn) => {\n const cursor = cardsOf(column).pageInfo.endCursor;\n if (!board || !cursor) return;\n setError(null);\n setLoadingMore(column.id);\n try {\n const res = await client.query<{\n planmagnetBoardColumnCards: { pageInfo: PageInfo; items: BoardCard[] };\n }>({\n query: COLUMN_PAGE,\n variables: { boardId: board.id, columnId: column.id, after: cursor },\n fetchPolicy: 'network-only',\n });\n const page = res.data?.planmagnetBoardColumnCards;\n if (!page) throw new Error(res.error?.message ?? 'The next cards could not be loaded.');\n const prior = extra[column.id]?.items ?? [];\n setMore({\n key: board,\n byColumn: {\n ...extra,\n [column.id]: { items: [...prior, ...page.items], pageInfo: page.pageInfo },\n },\n });\n } catch (e) {\n setError(messageOf(e, 'The next cards could not be loaded.'));\n } finally {\n setLoadingMore(null);\n }\n };\n\n const [createBoard, createBoardState] = useMutation<{ planmagnetCreateBoard: { id: string } }>(\n CREATE_BOARD,\n { refetchQueries: REFETCH, awaitRefetchQueries: true }\n );\n const [moveCard, moveState] = useMutation(MOVE_CARD, {\n refetchQueries: REFETCH,\n awaitRefetchQueries: true,\n });\n const [reorder] = useMutation(REORDER_COLUMNS, {\n refetchQueries: REFETCH,\n awaitRefetchQueries: true,\n });\n\n const run = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n } catch (e) {\n setError(messageOf(e, fallback));\n }\n };\n\n const move = (workItemId: string, targetColumnId: string) =>\n run(\n () => moveCard({ variables: { boardId, input: { workItemId, targetColumnId } } }),\n 'The card could not be moved.'\n );\n\n const shiftColumn = (index: number, delta: -1 | 1) => {\n if (!board) return;\n const order = board.columns.map((c) => c.id);\n const target = index + delta;\n if (target < 0 || target >= order.length) return;\n [order[index], order[target]] = [order[target], order[index]];\n return run(\n () => reorder({ variables: { boardId, input: { columnIds: order } } }),\n 'The columns could not be reordered.'\n );\n };\n\n if (boardsLoading && !boardsData) {\n return <p className=\"text-sm text-muted-foreground p-4\">Loading board…</p>;\n }\n\n if (!boardId) {\n return (\n <Card>\n <CardContent className=\"p-8 text-center space-y-3\" data-testid=\"board-empty\">\n <LayoutGrid className=\"h-8 w-8 mx-auto text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">{projectName} has no board yet.</p>\n <Button\n size=\"sm\"\n disabled={createBoardState.loading}\n onClick={() =>\n run(\n () =>\n createBoard({\n variables: {\n input: {\n projectId,\n name: `${projectName} board`,\n type: 'KANBAN',\n isDefault: true,\n withDefaultColumns: true,\n },\n },\n }),\n 'The board could not be created.'\n )\n }\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n {createBoardState.loading ? 'Creating…' : 'Create board'}\n </Button>\n <p className=\"text-xs text-muted-foreground\">\n Starts with one column per status: Backlog, To Do, In Progress, In Review, Testing,\n Done.\n </p>\n {error && <ErrorLine message={error} />}\n </CardContent>\n </Card>\n );\n }\n\n return (\n <div className=\"space-y-3\" data-testid=\"project-board\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {boards.length > 1 && (\n <select\n aria-label=\"Board\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={boardId}\n onChange={(e) => setChosenBoardId(e.target.value)}\n >\n {boards.map((b) => (\n <option key={b.id} value={b.id}>\n {b.name}\n </option>\n ))}\n </select>\n )}\n {board && (\n <label className=\"flex items-center gap-1 text-sm\">\n <span className=\"text-muted-foreground\">Swimlanes</span>\n <select\n aria-label=\"Swimlanes\"\n data-testid=\"board-lanes\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={laneBy}\n onChange={(e) => setLaneBy(e.target.value as LaneBy)}\n >\n <option value=\"none\">None</option>\n <option value=\"assignee\">By assignee</option>\n <option value=\"priority\">By priority</option>\n </select>\n </label>\n )}\n {board && board.unplacedCount > 0 && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"board-unplaced\">\n {board.unplacedCount} work item{board.unplacedCount === 1 ? ' is' : 's are'} not on this\n board: {board.unplacedCount === 1 ? 'its status has' : 'their status has'} no column.\n </p>\n )}\n {board && (\n <div className=\"ml-auto\">\n <ColumnDialog boardId={board.id} />\n </div>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n {loading && !board ? (\n <p className=\"text-sm text-muted-foreground p-4\">Loading board…</p>\n ) : board && laneBy !== 'none' ? (\n <SwimlaneBoard\n board={board}\n laneBy={laneBy}\n cardsOf={cardsOf}\n now={now}\n busy={moveState.loading}\n loadingMore={loadingMore}\n onLoadMore={(column) => void loadMore(column)}\n onMove={move}\n />\n ) : (\n <div className=\"flex gap-3 overflow-x-auto pb-2\">\n {board?.columns.map((column, index) => (\n <BoardColumnView\n key={column.id}\n boardId={board.id}\n column={column}\n columns={board.columns}\n cards={cardsOf(column)}\n isFirst={index === 0}\n isLast={index === board.columns.length - 1}\n busy={moveState.loading}\n now={now}\n loadingMore={loadingMore === column.id}\n onLoadMore={() => void loadMore(column)}\n onMove={move}\n onShift={(delta) => shiftColumn(index, delta)}\n />\n ))}\n {board && board.columns.length === 0 && (\n <p className=\"text-sm text-muted-foreground p-4\">\n This board has no columns. Add one to start placing work.\n </p>\n )}\n </div>\n )}\n </div>\n );\n}\n\ntype ColumnCards = { items: BoardCard[]; pageInfo: PageInfo; totalCount: number };\n\n/** One card: title, key, points, assignee, age in status, and a keyboard \"Move…\" equal to drag-and-drop. */\nfunction CardView({\n card,\n column,\n columns,\n busy,\n now,\n onMove,\n}: {\n card: BoardCard;\n column: BoardColumn;\n columns: BoardColumn[];\n busy: boolean;\n now: number;\n onMove: (workItemId: string, targetColumnId: string) => void;\n}) {\n const navigate = usePlanMagnetNavigate();\n const days = daysInStatus(card.statusSince, now);\n return (\n <li\n draggable\n onDragStart={(e) => {\n e.dataTransfer.setData('text/plain', card.id);\n e.dataTransfer.effectAllowed = 'move';\n }}\n className=\"rounded-md border bg-background p-2 space-y-1 cursor-grab\"\n data-testid=\"board-card\"\n >\n <button\n type=\"button\"\n className=\"text-left text-sm font-medium leading-tight hover:underline\"\n onClick={() => navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(card.id)}`)}\n >\n {card.title}\n </button>\n <div className=\"flex items-center justify-between text-[11px] text-muted-foreground\">\n <span>{card.key}</span>\n {/* Card aging (P1.2): how long it has had this status. Unknown when its status changed off the record. */}\n {days !== null && (\n <span\n data-testid=\"card-age\"\n className={ageClass(days)}\n title={`In ${statusLabel(card.status)} since ${new Date(card.statusSince ?? '').toLocaleDateString()}`}\n >\n {days === 0 ? 'today' : `${days}d in status`}\n </span>\n )}\n <span>{card.storyPoints ?? 0} pts</span>\n </div>\n <div className=\"flex items-center justify-between gap-1 text-[11px] text-muted-foreground\">\n <span className=\"truncate\">\n {card.assigneeId ? <PersonName id={card.assigneeId} /> : 'Unassigned'}\n </span>\n {/* The same move as drag-and-drop, for keyboard and assistive tech. */}\n <select\n aria-label={`Move ${card.key} to column`}\n className=\"rounded border border-input bg-background px-1 py-0.5 text-[11px]\"\n value=\"\"\n disabled={busy}\n onChange={(e) => e.target.value && onMove(card.id, e.target.value)}\n >\n <option value=\"\">Move…</option>\n {columns\n .filter((c) => c.id !== column.id)\n .map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n </div>\n </li>\n );\n}\n\n/** A drop target: dropping a card here moves it into `column`. */\nfunction DropZone({\n column,\n onMove,\n className,\n children,\n label,\n}: {\n column: BoardColumn;\n onMove: (workItemId: string, targetColumnId: string) => void;\n className: string;\n children: ReactNode;\n label: string;\n}) {\n const [dragOver, setDragOver] = useState(false);\n return (\n <section\n aria-label={label}\n className={`${className} ${dragOver ? 'ring-2 ring-primary' : ''}`}\n onDragOver={(e) => {\n e.preventDefault();\n setDragOver(true);\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragOver(false);\n const workItemId = e.dataTransfer.getData('text/plain');\n if (workItemId) onMove(workItemId, column.id);\n }}\n >\n {children}\n </section>\n );\n}\n\n/** \"Showing N of M — Show more\" for a column with cards past the ones loaded. */\nfunction MoreCards({\n cards,\n loading,\n onLoadMore,\n}: {\n cards: ColumnCards;\n loading: boolean;\n onLoadMore: () => void;\n}) {\n const hidden = cards.totalCount - cards.items.length;\n if (hidden <= 0 || !cards.pageInfo.hasNextPage) return null;\n return (\n <div className=\"px-2 pb-2 space-y-1\">\n <p className=\"text-xs text-muted-foreground\">\n Showing {cards.items.length} of {cards.totalCount}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n data-testid=\"board-column-more\"\n disabled={loading}\n onClick={onLoadMore}\n >\n {loading ? 'Loading…' : `Show ${Math.min(hidden, CARDS_PER_COLUMN)} more`}\n </Button>\n </div>\n );\n}\n\n/**\n * Swimlanes (P1.2): rows by assignee or priority across the same columns. Moving a card changes its column (and the\n * status that column sets); its lane follows its assignee or priority, which are changed on the card itself.\n */\nfunction SwimlaneBoard({\n board,\n laneBy,\n cardsOf,\n now,\n busy,\n loadingMore,\n onLoadMore,\n onMove,\n}: {\n board: BoardData;\n laneBy: LaneBy;\n cardsOf: (column: BoardColumn) => ColumnCards;\n now: number;\n busy: boolean;\n loadingMore: string | null;\n onLoadMore: (column: BoardColumn) => void;\n onMove: (workItemId: string, targetColumnId: string) => void;\n}) {\n const perColumn = board.columns.map((column) => ({ column, cards: cardsOf(column) }));\n const lanes = [\n ...new Set(perColumn.flatMap(({ cards }) => cards.items.map((c) => laneOf(c, laneBy)))),\n ].sort((a, b) =>\n laneBy === 'priority'\n ? PRIORITY_ORDER.indexOf(a) - PRIORITY_ORDER.indexOf(b)\n : a === ''\n ? 1\n : b === ''\n ? -1\n : a.localeCompare(b)\n );\n const laneName = (lane: string) =>\n laneBy === 'priority' ? statusLabel(lane) : lane ? <PersonName id={lane} /> : 'Unassigned';\n return (\n <div className=\"overflow-x-auto pb-2 space-y-3\" data-testid=\"board-swimlanes\">\n <p className=\"text-xs text-muted-foreground\">\n Dragging moves a card between columns. Its lane follows its{' '}\n {laneBy === 'priority' ? 'priority' : 'assignee'}.\n </p>\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <div key={column.id} className=\"min-w-[240px] w-[240px] shrink-0 px-2\">\n <h3 className=\"text-sm font-medium truncate\">\n {column.name}{' '}\n <span className=\"text-xs text-muted-foreground\">\n {column.wipLimit ? `${cards.totalCount} / ${column.wipLimit}` : cards.totalCount}\n </span>\n </h3>\n </div>\n ))}\n </div>\n {lanes.length === 0 && (\n <p className=\"text-sm text-muted-foreground p-4\">No cards on this board yet.</p>\n )}\n {lanes.map((lane) => (\n <div key={lane || 'none'} data-testid=\"board-lane\" className=\"space-y-1\">\n <p className=\"text-xs font-medium\">\n {laneName(lane)}{' '}\n <span className=\"text-muted-foreground\">\n (\n {perColumn.reduce(\n (n, { cards }) => n + cards.items.filter((c) => laneOf(c, laneBy) === lane).length,\n 0\n )}\n )\n </span>\n </p>\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <DropZone\n key={column.id}\n column={column}\n onMove={onMove}\n label={`${column.name} column`}\n className=\"min-w-[240px] w-[240px] shrink-0 rounded-md border bg-muted/30\"\n >\n <ul className=\"p-2 space-y-2 min-h-[48px]\">\n {cards.items\n .filter((c) => laneOf(c, laneBy) === lane)\n .map((card) => (\n <CardView\n key={card.id}\n card={card}\n column={column}\n columns={board.columns}\n busy={busy}\n now={now}\n onMove={onMove}\n />\n ))}\n </ul>\n </DropZone>\n ))}\n </div>\n </div>\n ))}\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <div key={column.id} className=\"min-w-[240px] w-[240px] shrink-0\">\n <MoreCards\n cards={cards}\n loading={loadingMore === column.id}\n onLoadMore={() => onLoadMore(column)}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n\nfunction BoardColumnView({\n boardId,\n column,\n columns,\n cards,\n isFirst,\n isLast,\n busy,\n now,\n loadingMore,\n onLoadMore,\n onMove,\n onShift,\n}: {\n boardId: string;\n column: BoardColumn;\n columns: BoardColumn[];\n cards: ColumnCards;\n isFirst: boolean;\n isLast: boolean;\n busy: boolean;\n now: number;\n loadingMore: boolean;\n onLoadMore: () => void;\n onMove: (workItemId: string, targetColumnId: string) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const count = cards.totalCount;\n const atLimit = column.wipLimit != null && column.wipLimit > 0 && count >= column.wipLimit;\n\n return (\n <DropZone\n column={column}\n onMove={onMove}\n label={`${column.name} column`}\n className=\"min-w-[240px] w-[240px] shrink-0 rounded-md border bg-muted/30\"\n >\n <div data-testid={`board-column-${column.name}`}>\n <header className=\"p-2 border-b space-y-1\">\n <div className=\"flex items-center justify-between gap-1\">\n <h3 className=\"text-sm font-medium truncate\">{column.name}</h3>\n <span\n data-testid={`board-column-count-${column.name}`}\n className={`text-xs rounded-full px-1.5 ${atLimit ? 'bg-status-warning-subtle text-status-warning' : 'bg-muted'}`}\n title={column.wipLimit ? `WIP limit ${column.wipLimit}` : undefined}\n >\n {column.wipLimit ? `${count} / ${column.wipLimit}` : count}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-1\">\n <span className=\"text-[11px] text-muted-foreground\">\n {column.mappedStatus\n ? `Sets status: ${statusLabel(column.mappedStatus)}`\n : 'No status'}\n </span>\n <span className=\"flex\">\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Move ${column.name} column left`}\n disabled={isFirst}\n onClick={() => onShift(-1)}\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Move ${column.name} column right`}\n disabled={isLast}\n onClick={() => onShift(1)}\n >\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </Button>\n <ColumnDialog boardId={boardId} column={column} />\n </span>\n </div>\n </header>\n <ul className=\"p-2 space-y-2 min-h-[80px]\">\n {cards.items.map((card) => (\n <CardView\n key={card.id}\n card={card}\n column={column}\n columns={columns}\n busy={busy}\n now={now}\n onMove={onMove}\n />\n ))}\n {count === 0 && <li className=\"text-xs text-muted-foreground text-center py-4\">Empty</li>}\n </ul>\n <MoreCards cards={cards} loading={loadingMore} onLoadMore={onLoadMore} />\n </div>\n </DropZone>\n );\n}\n\nfunction ColumnDialog({ boardId, column }: { boardId: string; column?: BoardColumn }) {\n const editing = column !== undefined;\n const [open, setOpen] = useState(false);\n const [name, setName] = useState(column?.name ?? '');\n const [mappedStatus, setMappedStatus] = useState(column?.mappedStatus ?? '');\n const [wip, setWip] = useState(column?.wipLimit ? String(column.wipLimit) : '');\n const [error, setError] = useState<string | null>(null);\n const options = { refetchQueries: REFETCH, awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_COLUMN, options);\n const [update, updateState] = useMutation(UPDATE_COLUMN, options);\n const [remove, removeState] = useMutation(DELETE_COLUMN, options);\n const busy = createState.loading || updateState.loading || removeState.loading;\n\n const wipValue = wip.trim() === '' ? null : Number(wip);\n const invalidWip = wipValue !== null && (!Number.isInteger(wipValue) || wipValue < 1);\n const canSave = name.trim() !== '' && !invalidWip && !busy;\n\n const reset = () => {\n setName(column?.name ?? '');\n setMappedStatus(column?.mappedStatus ?? '');\n setWip(column?.wipLimit ? String(column.wipLimit) : '');\n setError(null);\n };\n\n const save = async () => {\n setError(null);\n const fields = {\n name: name.trim(),\n mappedStatus: mappedStatus || null,\n wipLimit: wipValue,\n };\n try {\n if (editing) {\n await throwOnError(update({ variables: { id: column.id, input: fields } }));\n } else {\n await throwOnError(\n create({ variables: { input: { boardId, type: 'CUSTOM', ...fields } } })\n );\n }\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The column could not be saved.'));\n }\n };\n\n const idPrefix = editing ? `column-${column.id}` : 'new-column';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {editing ? (\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Edit ${column.name} column`}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add column\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {editing ? `Edit ${column.name}` : 'Add column'}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Moving a card into a column that sets a status changes the work item&apos;s status.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor={`${idPrefix}-name`} className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id={`${idPrefix}-name`}\n className=\"mt-1\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-status`} className=\"text-sm font-medium\">\n Sets status\n </label>\n <select\n id={`${idPrefix}-status`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={mappedStatus}\n onChange={(e) => setMappedStatus(e.target.value)}\n >\n <option value=\"\">No status (cards keep theirs)</option>\n {STATUSES.map((s) => (\n <option key={s} value={s}>\n {statusLabel(s)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-wip`} className=\"text-sm font-medium\">\n WIP limit (blank for none)\n </label>\n <Input\n id={`${idPrefix}-wip`}\n type=\"number\"\n min={1}\n className=\"mt-1\"\n value={wip}\n onChange={(e) => setWip(e.target.value)}\n />\n {invalidWip && <ErrorLine message=\"The WIP limit must be a whole number, 1 or more.\" />}\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter className=\"gap-2\">\n {editing && (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button variant=\"outline\" disabled={busy}>\n Delete column\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete {column.name}?</AlertDialogTitle>\n <AlertDialogDescription>\n Its cards are not deleted: each goes back to the column for its status, or off\n the board if no column sets that status.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n try {\n await throwOnError(remove({ variables: { id: column.id } }));\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The column could not be deleted.'));\n }\n }}\n >\n Delete column\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n )}\n <Button onClick={save} disabled={!canSave} data-testid={`${idPrefix}-save`}>\n {busy ? 'Saving…' : editing ? 'Save' : 'Add column'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectBoard.js","names":[],"sources":["../../../src/planmagnet/board/ProjectBoard.tsx"],"sourcesContent":["import { createContext, useContext, useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { ChevronLeft, ChevronRight, LayoutGrid, Pencil, Plus } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\nimport {\n type ProjectWorkflow,\n allowedTargets,\n useProjectWorkflow,\n workflowLabel,\n} from '../hooks/useProjectWorkflow';\n\n/**\n * A project's board (P1.2). The server owns placement: each column's `cards`\n * follow the rule in wspace-planmagnet-svc board.service.ts `columnCardsWhere`\n * (placed column, else the first column mapped to the item's status), and\n * `unplacedCount` says how many items are on no column. Moving a card into a\n * column mapped to a status changes that status on the server, and a column's\n * WIP limit is enforced there too. This component only draws and asks.\n */\n\nconst STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\n/** The project's workflow for everything drawn on its board (BOFF-8043): status names and allowed moves. */\nconst BoardWorkflow = createContext<ProjectWorkflow | null>(null);\nconst useStatusName = () => {\n const workflow = useContext(BoardWorkflow);\n return (status: string) => workflowLabel(workflow, status);\n};\n\nconst statusLabel = (status: string) =>\n status.charAt(0) + status.slice(1).toLowerCase().replace(/_/g, ' ');\n\nconst CARDS_PER_COLUMN = 100;\n\nconst BOARDS = gql`\n query ProjectBoards($workspaceId: ID!, $projectId: String) {\n planmagnetBoards(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 20 }\n ) {\n items {\n id\n name\n isDefault\n }\n }\n }\n`;\n\n/** The next page of one column (P1.2: a column shows 100 cards and used to say only \"+N more\"). */\nconst COLUMN_PAGE = gql`\n query ProjectBoardColumnPage($boardId: ID!, $columnId: ID!, $after: String) {\n planmagnetBoardColumnCards(\n boardId: $boardId\n columnId: $columnId\n pagination: { first: 100, after: $after }\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n key\n title\n status\n priority\n storyPoints\n assigneeId\n statusSince\n }\n }\n }\n`;\n\nconst BOARD = gql`\n query ProjectBoard($id: ID!) {\n planmagnetBoard(id: $id) {\n id\n name\n unplacedCount\n columns {\n id\n name\n mappedStatus\n wipLimit\n sortOrder\n cards(pagination: { first: 100 }) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n key\n title\n status\n priority\n storyPoints\n assigneeId\n statusSince\n }\n }\n }\n }\n }\n`;\n\nconst MOVE_CARD = gql`\n mutation ProjectBoardMoveCard($boardId: ID!, $input: PlanmagnetMoveCardInput!) {\n planmagnetMoveCard(boardId: $boardId, input: $input) {\n id\n }\n }\n`;\nconst CREATE_BOARD = gql`\n mutation ProjectBoardCreate($input: PlanmagnetCreateBoardInput!) {\n planmagnetCreateBoard(input: $input) {\n id\n }\n }\n`;\nconst CREATE_COLUMN = gql`\n mutation ProjectBoardCreateColumn($input: PlanmagnetCreateBoardColumnInput!) {\n planmagnetCreateBoardColumn(input: $input) {\n id\n }\n }\n`;\nconst UPDATE_COLUMN = gql`\n mutation ProjectBoardUpdateColumn($id: ID!, $input: PlanmagnetUpdateBoardColumnInput!) {\n planmagnetUpdateBoardColumn(id: $id, input: $input) {\n id\n }\n }\n`;\nconst DELETE_COLUMN = gql`\n mutation ProjectBoardDeleteColumn($id: ID!) {\n planmagnetDeleteBoardColumn(id: $id)\n }\n`;\nconst REORDER_COLUMNS = gql`\n mutation ProjectBoardReorder($boardId: ID!, $input: PlanmagnetReorderColumnsInput!) {\n planmagnetReorderColumns(boardId: $boardId, input: $input) {\n id\n }\n }\n`;\n\n/** Every query whose numbers a board change can move. */\nconst REFETCH = [\n 'ProjectBoard',\n 'ProjectBoards',\n 'GetWorkItems',\n 'GetWorkItemCounts',\n 'GetDashboardData',\n];\n\ninterface BoardCard {\n id: string;\n key: string;\n title: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n /** When it entered its current status; null when unknown (changed by a path that records no history). */\n statusSince: string | null;\n}\n\ninterface PageInfo {\n hasNextPage: boolean;\n endCursor: string | null;\n}\n\ninterface BoardColumn {\n id: string;\n name: string;\n mappedStatus: string | null;\n wipLimit: number | null;\n sortOrder: number;\n cards: { totalCount: number; pageInfo: PageInfo; items: BoardCard[] };\n}\n\ntype LaneBy = 'none' | 'assignee' | 'priority';\nconst PRIORITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'NONE'];\nconst DAY_MS = 86_400_000;\n\n/** Whole days in the current status, or null when unknown. */\nexport function daysInStatus(statusSince: string | null, now: number): number | null {\n if (!statusSince) return null;\n return Math.max(0, Math.floor((now - new Date(statusSince).getTime()) / DAY_MS));\n}\n\n/** Card aging: quiet under a week, a warning from 7 days, stronger from 14. Semantic tokens only. */\nfunction ageClass(days: number): string {\n if (days >= 14) return 'text-destructive font-medium';\n if (days >= 7) return 'text-status-warning-text';\n return 'text-muted-foreground';\n}\n\nfunction laneOf(card: BoardCard, by: LaneBy): string {\n if (by === 'assignee') return card.assigneeId ?? '';\n if (by === 'priority') return card.priority || 'NONE';\n return '';\n}\n\ninterface BoardData {\n id: string;\n name: string;\n unplacedCount: number;\n columns: BoardColumn[];\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function ProjectBoard({\n projectId,\n projectName,\n}: {\n projectId: string;\n projectName: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const client = useApolloClient();\n const [chosenBoardId, setChosenBoardId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n // Card age is read against the moment the board opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n const [laneBy, setLaneBy] = useState<LaneBy>('none');\n // Pages loaded past each column's first 100, keyed by the board data they extend: a refetch starts over.\n const [more, setMore] = useState<{\n key: unknown;\n byColumn: Record<string, { items: BoardCard[]; pageInfo: PageInfo }>;\n }>({\n key: null,\n byColumn: {},\n });\n const [loadingMore, setLoadingMore] = useState<string | null>(null);\n const workflow = useProjectWorkflow(projectId);\n\n const { data: boardsData, loading: boardsLoading } = useQuery<{\n planmagnetBoards: { items: Array<{ id: string; name: string; isDefault: boolean }> };\n }>(BOARDS, { variables: { workspaceId, projectId }, skip: !workspaceId });\n const boards = boardsData?.planmagnetBoards.items ?? [];\n const boardId =\n (chosenBoardId && boards.some((b) => b.id === chosenBoardId) ? chosenBoardId : null) ??\n boards.find((b) => b.isDefault)?.id ??\n boards[0]?.id ??\n null;\n\n const { data, loading } = useQuery<{ planmagnetBoard: BoardData | null }>(BOARD, {\n variables: { id: boardId },\n skip: !boardId,\n });\n const board = data?.planmagnetBoard ?? null;\n const extra = more.key === board ? more.byColumn : {};\n /** A column with every page loaded so far. */\n const cardsOf = (column: BoardColumn) => {\n const loaded = extra[column.id];\n return {\n items: loaded ? [...column.cards.items, ...loaded.items] : column.cards.items,\n pageInfo: loaded ? loaded.pageInfo : column.cards.pageInfo,\n totalCount: column.cards.totalCount,\n };\n };\n const loadMore = async (column: BoardColumn) => {\n const cursor = cardsOf(column).pageInfo.endCursor;\n if (!board || !cursor) return;\n setError(null);\n setLoadingMore(column.id);\n try {\n const res = await client.query<{\n planmagnetBoardColumnCards: { pageInfo: PageInfo; items: BoardCard[] };\n }>({\n query: COLUMN_PAGE,\n variables: { boardId: board.id, columnId: column.id, after: cursor },\n fetchPolicy: 'network-only',\n });\n const page = res.data?.planmagnetBoardColumnCards;\n if (!page) throw new Error(res.error?.message ?? 'The next cards could not be loaded.');\n const prior = extra[column.id]?.items ?? [];\n setMore({\n key: board,\n byColumn: {\n ...extra,\n [column.id]: { items: [...prior, ...page.items], pageInfo: page.pageInfo },\n },\n });\n } catch (e) {\n setError(messageOf(e, 'The next cards could not be loaded.'));\n } finally {\n setLoadingMore(null);\n }\n };\n\n const [createBoard, createBoardState] = useMutation<{ planmagnetCreateBoard: { id: string } }>(\n CREATE_BOARD,\n { refetchQueries: REFETCH, awaitRefetchQueries: true }\n );\n const [moveCard, moveState] = useMutation(MOVE_CARD, {\n refetchQueries: REFETCH,\n awaitRefetchQueries: true,\n });\n const [reorder] = useMutation(REORDER_COLUMNS, {\n refetchQueries: REFETCH,\n awaitRefetchQueries: true,\n });\n\n const run = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n } catch (e) {\n setError(messageOf(e, fallback));\n }\n };\n\n const move = (workItemId: string, targetColumnId: string) =>\n run(\n () => moveCard({ variables: { boardId, input: { workItemId, targetColumnId } } }),\n 'The card could not be moved.'\n );\n\n const shiftColumn = (index: number, delta: -1 | 1) => {\n if (!board) return;\n const order = board.columns.map((c) => c.id);\n const target = index + delta;\n if (target < 0 || target >= order.length) return;\n [order[index], order[target]] = [order[target], order[index]];\n return run(\n () => reorder({ variables: { boardId, input: { columnIds: order } } }),\n 'The columns could not be reordered.'\n );\n };\n\n if (boardsLoading && !boardsData) {\n return <p className=\"text-sm text-muted-foreground p-4\">Loading board…</p>;\n }\n\n if (!boardId) {\n return (\n <Card>\n <CardContent className=\"p-8 text-center space-y-3\" data-testid=\"board-empty\">\n <LayoutGrid className=\"h-8 w-8 mx-auto text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">{projectName} has no board yet.</p>\n <Button\n size=\"sm\"\n disabled={createBoardState.loading}\n onClick={() =>\n run(\n () =>\n createBoard({\n variables: {\n input: {\n projectId,\n name: `${projectName} board`,\n type: 'KANBAN',\n isDefault: true,\n withDefaultColumns: true,\n },\n },\n }),\n 'The board could not be created.'\n )\n }\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n {createBoardState.loading ? 'Creating…' : 'Create board'}\n </Button>\n <p className=\"text-xs text-muted-foreground\">\n Starts with one column per status: Backlog, To Do, In Progress, In Review, Testing,\n Done.\n </p>\n {error && <ErrorLine message={error} />}\n </CardContent>\n </Card>\n );\n }\n\n return (\n <BoardWorkflow.Provider value={workflow}>\n <div className=\"space-y-3\" data-testid=\"project-board\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {boards.length > 1 && (\n <select\n aria-label=\"Board\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={boardId}\n onChange={(e) => setChosenBoardId(e.target.value)}\n >\n {boards.map((b) => (\n <option key={b.id} value={b.id}>\n {b.name}\n </option>\n ))}\n </select>\n )}\n {board && (\n <label className=\"flex items-center gap-1 text-sm\">\n <span className=\"text-muted-foreground\">Swimlanes</span>\n <select\n aria-label=\"Swimlanes\"\n data-testid=\"board-lanes\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={laneBy}\n onChange={(e) => setLaneBy(e.target.value as LaneBy)}\n >\n <option value=\"none\">None</option>\n <option value=\"assignee\">By assignee</option>\n <option value=\"priority\">By priority</option>\n </select>\n </label>\n )}\n {board && board.unplacedCount > 0 && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"board-unplaced\">\n {board.unplacedCount} work item{board.unplacedCount === 1 ? ' is' : 's are'} not on\n this board: {board.unplacedCount === 1 ? 'its status has' : 'their status has'} no\n column.\n </p>\n )}\n {board && (\n <div className=\"ml-auto\">\n <ColumnDialog boardId={board.id} />\n </div>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n {loading && !board ? (\n <p className=\"text-sm text-muted-foreground p-4\">Loading board…</p>\n ) : board && laneBy !== 'none' ? (\n <SwimlaneBoard\n board={board}\n laneBy={laneBy}\n cardsOf={cardsOf}\n now={now}\n busy={moveState.loading}\n loadingMore={loadingMore}\n onLoadMore={(column) => void loadMore(column)}\n onMove={move}\n />\n ) : (\n <div className=\"flex gap-3 overflow-x-auto pb-2\">\n {board?.columns.map((column, index) => (\n <BoardColumnView\n key={column.id}\n boardId={board.id}\n column={column}\n columns={board.columns}\n cards={cardsOf(column)}\n isFirst={index === 0}\n isLast={index === board.columns.length - 1}\n busy={moveState.loading}\n now={now}\n loadingMore={loadingMore === column.id}\n onLoadMore={() => void loadMore(column)}\n onMove={move}\n onShift={(delta) => shiftColumn(index, delta)}\n />\n ))}\n {board && board.columns.length === 0 && (\n <p className=\"text-sm text-muted-foreground p-4\">\n This board has no columns. Add one to start placing work.\n </p>\n )}\n </div>\n )}\n </div>\n </BoardWorkflow.Provider>\n );\n}\n\ntype ColumnCards = { items: BoardCard[]; pageInfo: PageInfo; totalCount: number };\n\n/** One card: title, key, points, assignee, age in status, and a keyboard \"Move…\" equal to drag-and-drop. */\nfunction CardView({\n card,\n column,\n columns,\n busy,\n now,\n onMove,\n}: {\n card: BoardCard;\n column: BoardColumn;\n columns: BoardColumn[];\n busy: boolean;\n now: number;\n onMove: (workItemId: string, targetColumnId: string) => void;\n}) {\n const navigate = usePlanMagnetNavigate();\n const workflow = useContext(BoardWorkflow);\n const days = daysInStatus(card.statusSince, now);\n // Only columns this card may move into: unmapped ones (no status change) and those whose status the workflow allows.\n const reachable = allowedTargets(workflow, card.status, STATUSES);\n const targets = columns.filter(\n (c) => c.id !== column.id && (!c.mappedStatus || reachable.includes(c.mappedStatus))\n );\n return (\n <li\n draggable\n onDragStart={(e) => {\n e.dataTransfer.setData('text/plain', card.id);\n e.dataTransfer.effectAllowed = 'move';\n }}\n className=\"rounded-md border bg-background p-2 space-y-1 cursor-grab\"\n data-testid=\"board-card\"\n >\n <button\n type=\"button\"\n className=\"text-left text-sm font-medium leading-tight hover:underline\"\n onClick={() => navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(card.id)}`)}\n >\n {card.title}\n </button>\n <div className=\"flex items-center justify-between text-[11px] text-muted-foreground\">\n <span>{card.key}</span>\n {/* Card aging (P1.2): how long it has had this status. Unknown when its status changed off the record. */}\n {days !== null && (\n <span\n data-testid=\"card-age\"\n className={ageClass(days)}\n title={`In ${workflowLabel(workflow, card.status)} since ${new Date(card.statusSince ?? '').toLocaleDateString()}`}\n >\n {days === 0 ? 'today' : `${days}d in status`}\n </span>\n )}\n <span>{card.storyPoints ?? 0} pts</span>\n </div>\n <div className=\"flex items-center justify-between gap-1 text-[11px] text-muted-foreground\">\n <span className=\"truncate\">\n {card.assigneeId ? <PersonName id={card.assigneeId} /> : 'Unassigned'}\n </span>\n {/* The same move as drag-and-drop, for keyboard and assistive tech. */}\n <select\n aria-label={`Move ${card.key} to column`}\n className=\"rounded border border-input bg-background px-1 py-0.5 text-[11px]\"\n value=\"\"\n disabled={busy}\n onChange={(e) => e.target.value && onMove(card.id, e.target.value)}\n >\n <option value=\"\">Move…</option>\n {targets.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n </div>\n </li>\n );\n}\n\n/** A drop target: dropping a card here moves it into `column`. */\nfunction DropZone({\n column,\n onMove,\n className,\n children,\n label,\n}: {\n column: BoardColumn;\n onMove: (workItemId: string, targetColumnId: string) => void;\n className: string;\n children: ReactNode;\n label: string;\n}) {\n const [dragOver, setDragOver] = useState(false);\n return (\n <section\n aria-label={label}\n className={`${className} ${dragOver ? 'ring-2 ring-primary' : ''}`}\n onDragOver={(e) => {\n e.preventDefault();\n setDragOver(true);\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragOver(false);\n const workItemId = e.dataTransfer.getData('text/plain');\n if (workItemId) onMove(workItemId, column.id);\n }}\n >\n {children}\n </section>\n );\n}\n\n/** \"Showing N of M — Show more\" for a column with cards past the ones loaded. */\nfunction MoreCards({\n cards,\n loading,\n onLoadMore,\n}: {\n cards: ColumnCards;\n loading: boolean;\n onLoadMore: () => void;\n}) {\n const hidden = cards.totalCount - cards.items.length;\n if (hidden <= 0 || !cards.pageInfo.hasNextPage) return null;\n return (\n <div className=\"px-2 pb-2 space-y-1\">\n <p className=\"text-xs text-muted-foreground\">\n Showing {cards.items.length} of {cards.totalCount}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n data-testid=\"board-column-more\"\n disabled={loading}\n onClick={onLoadMore}\n >\n {loading ? 'Loading…' : `Show ${Math.min(hidden, CARDS_PER_COLUMN)} more`}\n </Button>\n </div>\n );\n}\n\n/**\n * Swimlanes (P1.2): rows by assignee or priority across the same columns. Moving a card changes its column (and the\n * status that column sets); its lane follows its assignee or priority, which are changed on the card itself.\n */\nfunction SwimlaneBoard({\n board,\n laneBy,\n cardsOf,\n now,\n busy,\n loadingMore,\n onLoadMore,\n onMove,\n}: {\n board: BoardData;\n laneBy: LaneBy;\n cardsOf: (column: BoardColumn) => ColumnCards;\n now: number;\n busy: boolean;\n loadingMore: string | null;\n onLoadMore: (column: BoardColumn) => void;\n onMove: (workItemId: string, targetColumnId: string) => void;\n}) {\n const perColumn = board.columns.map((column) => ({ column, cards: cardsOf(column) }));\n const lanes = [\n ...new Set(perColumn.flatMap(({ cards }) => cards.items.map((c) => laneOf(c, laneBy)))),\n ].sort((a, b) =>\n laneBy === 'priority'\n ? PRIORITY_ORDER.indexOf(a) - PRIORITY_ORDER.indexOf(b)\n : a === ''\n ? 1\n : b === ''\n ? -1\n : a.localeCompare(b)\n );\n const laneName = (lane: string) =>\n laneBy === 'priority' ? statusLabel(lane) : lane ? <PersonName id={lane} /> : 'Unassigned';\n return (\n <div className=\"overflow-x-auto pb-2 space-y-3\" data-testid=\"board-swimlanes\">\n <p className=\"text-xs text-muted-foreground\">\n Dragging moves a card between columns. Its lane follows its{' '}\n {laneBy === 'priority' ? 'priority' : 'assignee'}.\n </p>\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <div key={column.id} className=\"min-w-[240px] w-[240px] shrink-0 px-2\">\n <h3 className=\"text-sm font-medium truncate\">\n {column.name}{' '}\n <span className=\"text-xs text-muted-foreground\">\n {column.wipLimit ? `${cards.totalCount} / ${column.wipLimit}` : cards.totalCount}\n </span>\n </h3>\n </div>\n ))}\n </div>\n {lanes.length === 0 && (\n <p className=\"text-sm text-muted-foreground p-4\">No cards on this board yet.</p>\n )}\n {lanes.map((lane) => (\n <div key={lane || 'none'} data-testid=\"board-lane\" className=\"space-y-1\">\n <p className=\"text-xs font-medium\">\n {laneName(lane)}{' '}\n <span className=\"text-muted-foreground\">\n (\n {perColumn.reduce(\n (n, { cards }) => n + cards.items.filter((c) => laneOf(c, laneBy) === lane).length,\n 0\n )}\n )\n </span>\n </p>\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <DropZone\n key={column.id}\n column={column}\n onMove={onMove}\n label={`${column.name} column`}\n className=\"min-w-[240px] w-[240px] shrink-0 rounded-md border bg-muted/30\"\n >\n <ul className=\"p-2 space-y-2 min-h-[48px]\">\n {cards.items\n .filter((c) => laneOf(c, laneBy) === lane)\n .map((card) => (\n <CardView\n key={card.id}\n card={card}\n column={column}\n columns={board.columns}\n busy={busy}\n now={now}\n onMove={onMove}\n />\n ))}\n </ul>\n </DropZone>\n ))}\n </div>\n </div>\n ))}\n <div className=\"flex gap-3\">\n {perColumn.map(({ column, cards }) => (\n <div key={column.id} className=\"min-w-[240px] w-[240px] shrink-0\">\n <MoreCards\n cards={cards}\n loading={loadingMore === column.id}\n onLoadMore={() => onLoadMore(column)}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n\nfunction BoardColumnView({\n boardId,\n column,\n columns,\n cards,\n isFirst,\n isLast,\n busy,\n now,\n loadingMore,\n onLoadMore,\n onMove,\n onShift,\n}: {\n boardId: string;\n column: BoardColumn;\n columns: BoardColumn[];\n cards: ColumnCards;\n isFirst: boolean;\n isLast: boolean;\n busy: boolean;\n now: number;\n loadingMore: boolean;\n onLoadMore: () => void;\n onMove: (workItemId: string, targetColumnId: string) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const count = cards.totalCount;\n const atLimit = column.wipLimit != null && column.wipLimit > 0 && count >= column.wipLimit;\n const statusName = useStatusName();\n\n return (\n <DropZone\n column={column}\n onMove={onMove}\n label={`${column.name} column`}\n className=\"min-w-[240px] w-[240px] shrink-0 rounded-md border bg-muted/30\"\n >\n <div data-testid={`board-column-${column.name}`}>\n <header className=\"p-2 border-b space-y-1\">\n <div className=\"flex items-center justify-between gap-1\">\n <h3 className=\"text-sm font-medium truncate\">{column.name}</h3>\n <span\n data-testid={`board-column-count-${column.name}`}\n className={`text-xs rounded-full px-1.5 ${atLimit ? 'bg-status-warning-subtle text-status-warning' : 'bg-muted'}`}\n title={column.wipLimit ? `WIP limit ${column.wipLimit}` : undefined}\n >\n {column.wipLimit ? `${count} / ${column.wipLimit}` : count}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-1\">\n <span className=\"text-[11px] text-muted-foreground\">\n {column.mappedStatus\n ? `Sets status: ${statusName(column.mappedStatus)}`\n : 'No status'}\n </span>\n <span className=\"flex\">\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Move ${column.name} column left`}\n disabled={isFirst}\n onClick={() => onShift(-1)}\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" />\n </Button>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Move ${column.name} column right`}\n disabled={isLast}\n onClick={() => onShift(1)}\n >\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </Button>\n <ColumnDialog boardId={boardId} column={column} />\n </span>\n </div>\n </header>\n <ul className=\"p-2 space-y-2 min-h-[80px]\">\n {cards.items.map((card) => (\n <CardView\n key={card.id}\n card={card}\n column={column}\n columns={columns}\n busy={busy}\n now={now}\n onMove={onMove}\n />\n ))}\n {count === 0 && <li className=\"text-xs text-muted-foreground text-center py-4\">Empty</li>}\n </ul>\n <MoreCards cards={cards} loading={loadingMore} onLoadMore={onLoadMore} />\n </div>\n </DropZone>\n );\n}\n\nfunction ColumnDialog({ boardId, column }: { boardId: string; column?: BoardColumn }) {\n const editing = column !== undefined;\n const statusName = useStatusName();\n const [open, setOpen] = useState(false);\n const [name, setName] = useState(column?.name ?? '');\n const [mappedStatus, setMappedStatus] = useState(column?.mappedStatus ?? '');\n const [wip, setWip] = useState(column?.wipLimit ? String(column.wipLimit) : '');\n const [error, setError] = useState<string | null>(null);\n const options = { refetchQueries: REFETCH, awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_COLUMN, options);\n const [update, updateState] = useMutation(UPDATE_COLUMN, options);\n const [remove, removeState] = useMutation(DELETE_COLUMN, options);\n const busy = createState.loading || updateState.loading || removeState.loading;\n\n const wipValue = wip.trim() === '' ? null : Number(wip);\n const invalidWip = wipValue !== null && (!Number.isInteger(wipValue) || wipValue < 1);\n const canSave = name.trim() !== '' && !invalidWip && !busy;\n\n const reset = () => {\n setName(column?.name ?? '');\n setMappedStatus(column?.mappedStatus ?? '');\n setWip(column?.wipLimit ? String(column.wipLimit) : '');\n setError(null);\n };\n\n const save = async () => {\n setError(null);\n const fields = {\n name: name.trim(),\n mappedStatus: mappedStatus || null,\n wipLimit: wipValue,\n };\n try {\n if (editing) {\n await throwOnError(update({ variables: { id: column.id, input: fields } }));\n } else {\n await throwOnError(\n create({ variables: { input: { boardId, type: 'CUSTOM', ...fields } } })\n );\n }\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The column could not be saved.'));\n }\n };\n\n const idPrefix = editing ? `column-${column.id}` : 'new-column';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {editing ? (\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-6 w-6\"\n aria-label={`Edit ${column.name} column`}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add column\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {editing ? `Edit ${column.name}` : 'Add column'}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Moving a card into a column that sets a status changes the work item&apos;s status.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor={`${idPrefix}-name`} className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id={`${idPrefix}-name`}\n className=\"mt-1\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-status`} className=\"text-sm font-medium\">\n Sets status\n </label>\n <select\n id={`${idPrefix}-status`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={mappedStatus}\n onChange={(e) => setMappedStatus(e.target.value)}\n >\n <option value=\"\">No status (cards keep theirs)</option>\n {STATUSES.map((s) => (\n <option key={s} value={s}>\n {statusName(s)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-wip`} className=\"text-sm font-medium\">\n WIP limit (blank for none)\n </label>\n <Input\n id={`${idPrefix}-wip`}\n type=\"number\"\n min={1}\n className=\"mt-1\"\n value={wip}\n onChange={(e) => setWip(e.target.value)}\n />\n {invalidWip && <ErrorLine message=\"The WIP limit must be a whole number, 1 or more.\" />}\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter className=\"gap-2\">\n {editing && (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button variant=\"outline\" disabled={busy}>\n Delete column\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete {column.name}?</AlertDialogTitle>\n <AlertDialogDescription>\n Its cards are not deleted: each goes back to the column for its status, or off\n the board if no column sets that status.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n try {\n await throwOnError(remove({ variables: { id: column.id } }));\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The column could not be deleted.'));\n }\n }}\n >\n Delete column\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n )}\n <Button onClick={save} disabled={!canSave} data-testid={`${idPrefix}-save`}>\n {busy ? 'Saving…' : editing ? 'Save' : 'Add column'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;;AA6CA,IAAM,IAAW;CACf;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IAAgB,EAAsC,KAAK,EAC3D,UAAsB;CAC1B,IAAM,IAAW,EAAW,EAAc;AAC1C,SAAQ,MAAmB,EAAc,GAAU,EAAO;GAGtD,KAAe,MACnB,EAAO,OAAO,EAAE,GAAG,EAAO,MAAM,EAAE,CAAC,aAAa,CAAC,QAAQ,MAAM,IAAI,EAE/D,IAAmB,KAEnB,IAAS,CAAG;;;;;;;;;;;;;;GAiBZ,IAAc,CAAG;;;;;;;;;;;;;;;;;;;;;;;GAyBjB,IAAQ,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCX,IAAY,CAAG;;;;;;GAOf,IAAe,CAAG;;;;;;GAOlB,IAAgB,CAAG;;;;;;GAOnB,IAAgB,CAAG;;;;;;GAOnB,KAAgB,CAAG;;;;GAKnB,KAAkB,CAAG;;;;;;GASrB,IAAU;CACd;CACA;CACA;CACA;CACA;CACD,EA6BK,IAAiB;CAAC;CAAY;CAAQ;CAAU;CAAO;CAAO,EAC9D,KAAS;AAGf,SAAgB,EAAa,GAA4B,GAA4B;AAEnF,QADK,IACE,KAAK,IAAI,GAAG,KAAK,OAAO,IAAM,IAAI,KAAK,EAAY,CAAC,SAAS,IAAI,GAAO,CAAC,GADvD;;AAK3B,SAAS,GAAS,GAAsB;AAGtC,QAFI,KAAQ,KAAW,iCACnB,KAAQ,IAAU,6BACf;;AAGT,SAAS,EAAO,GAAiB,GAAoB;AAGnD,QAFI,MAAO,aAAmB,EAAK,cAAc,KAC7C,MAAO,aAAmB,EAAK,YAAY,SACxC;;AAUT,IAAM,KAAa,GAAY,MAAsB,aAAa,QAAQ,EAAE,UAAU;AAEtF,SAAS,EAAU,EAAE,cAAgC;AACnD,QACE,kBAAC,KAAD;EAAG,MAAK;EAAQ,WAAU;YACvB;EACC,CAAA;;AAIR,SAAgB,GAAa,EAC3B,cACA,kBAIC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,IAAS,GAAiB,EAC1B,CAAC,GAAe,KAAoB,EAAwB,KAAK,EACjE,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,CAAC,KAAO,QAAe,KAAK,KAAK,CAAC,EAClC,CAAC,GAAQ,KAAa,EAAiB,OAAO,EAE9C,CAAC,GAAM,MAAW,EAGrB;EACD,KAAK;EACL,UAAU,EAAE;EACb,CAAC,EACI,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,KAAW,EAAmB,EAAU,EAExC,EAAE,MAAM,GAAY,SAAS,MAAkB,EAElD,GAAQ;EAAE,WAAW;GAAE;GAAa;GAAW;EAAE,MAAM,CAAC;EAAa,CAAC,EACnE,IAAS,GAAY,iBAAiB,SAAS,EAAE,EACjD,KACH,KAAiB,EAAO,MAAM,MAAM,EAAE,OAAO,EAAc,GAAG,IAAgB,SAC/E,EAAO,MAAM,MAAM,EAAE,UAAU,EAAE,MACjC,EAAO,IAAI,MACX,MAEI,EAAE,UAAM,gBAAY,EAAgD,GAAO;EAC/E,WAAW,EAAE,IAAI,GAAS;EAC1B,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,IAAM,mBAAmB,MACjC,IAAQ,EAAK,QAAQ,IAAQ,EAAK,WAAW,EAAE,EAE/C,KAAW,MAAwB;EACvC,IAAM,IAAS,EAAM,EAAO;AAC5B,SAAO;GACL,OAAO,IAAS,CAAC,GAAG,EAAO,MAAM,OAAO,GAAG,EAAO,MAAM,GAAG,EAAO,MAAM;GACxE,UAAU,IAAS,EAAO,WAAW,EAAO,MAAM;GAClD,YAAY,EAAO,MAAM;GAC1B;IAEG,IAAW,OAAO,MAAwB;EAC9C,IAAM,IAAS,EAAQ,EAAO,CAAC,SAAS;AACpC,SAAC,KAAS,CAAC,IAEf;GADA,EAAS,KAAK,EACd,EAAe,EAAO,GAAG;AACzB,OAAI;IACF,IAAM,IAAM,MAAM,EAAO,MAEtB;KACD,OAAO;KACP,WAAW;MAAE,SAAS,EAAM;MAAI,UAAU,EAAO;MAAI,OAAO;MAAQ;KACpE,aAAa;KACd,CAAC,EACI,IAAO,EAAI,MAAM;AACvB,QAAI,CAAC,EAAM,OAAU,MAAM,EAAI,OAAO,WAAW,sCAAsC;IACvF,IAAM,IAAQ,EAAM,EAAO,KAAK,SAAS,EAAE;AAC3C,OAAQ;KACN,KAAK;KACL,UAAU;MACR,GAAG;OACF,EAAO,KAAK;OAAE,OAAO,CAAC,GAAG,GAAO,GAAG,EAAK,MAAM;OAAE,UAAU,EAAK;OAAU;MAC3E;KACF,CAAC;YACK,GAAG;AACV,MAAS,EAAU,GAAG,sCAAsC,CAAC;aACrD;AACR,MAAe,KAAK;;;IAIlB,CAAC,GAAa,KAAoB,EACtC,GACA;EAAE,gBAAgB;EAAS,qBAAqB;EAAM,CACvD,EACK,CAAC,GAAU,KAAa,EAAY,GAAW;EACnD,gBAAgB;EAChB,qBAAqB;EACtB,CAAC,EACI,CAAC,KAAW,EAAY,IAAiB;EAC7C,gBAAgB;EAChB,qBAAqB;EACtB,CAAC,EAEI,IAAM,OAAO,GAA4C,MAAqB;AAClF,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,GAAQ,CAAC;WACrB,GAAG;AACV,KAAS,EAAU,GAAG,EAAS,CAAC;;IAI9B,KAAQ,GAAoB,MAChC,QACQ,EAAS,EAAE,WAAW;EAAE;EAAS,OAAO;GAAE;GAAY;GAAgB;EAAE,EAAE,CAAC,EACjF,+BACD,EAEG,MAAe,GAAe,MAAkB;AACpD,MAAI,CAAC,EAAO;EACZ,IAAM,IAAQ,EAAM,QAAQ,KAAK,MAAM,EAAE,GAAG,EACtC,IAAS,IAAQ;AACnB,YAAS,KAAK,KAAU,EAAM,QAElC,QADA,CAAC,EAAM,IAAQ,EAAM,MAAW,CAAC,EAAM,IAAS,EAAM,GAAO,EACtD,QACC,EAAQ,EAAE,WAAW;GAAE;GAAS,OAAO,EAAE,WAAW,GAAO;GAAE,EAAE,CAAC,EACtE,sCACD;;AA+CH,QA5CI,KAAiB,CAAC,IACb,kBAAC,KAAD;EAAG,WAAU;YAAoC;EAAkB,CAAA,GAGvE,IAyCH,kBAAC,EAAc,UAAf;EAAwB,OAAO;YAC7B,kBAAC,OAAD;GAAK,WAAU;GAAY,eAAY;aAAvC;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAO,SAAS,KACf,kBAAC,UAAD;OACE,cAAW;OACX,WAAU;OACV,OAAO;OACP,WAAW,MAAM,EAAiB,EAAE,OAAO,MAAM;iBAEhD,EAAO,KAAK,MACX,kBAAC,UAAD;QAAmB,OAAO,EAAE;kBACzB,EAAE;QACI,EAFI,EAAE,GAEN,CACT;OACK,CAAA;MAEV,KACC,kBAAC,SAAD;OAAO,WAAU;iBAAjB,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAgB,CAAA,EACxD,kBAAC,UAAD;QACE,cAAW;QACX,eAAY;QACZ,WAAU;QACV,OAAO;QACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAgB;kBALtD;SAOE,kBAAC,UAAD;UAAQ,OAAM;oBAAO;UAAa,CAAA;SAClC,kBAAC,UAAD;UAAQ,OAAM;oBAAW;UAAoB,CAAA;SAC7C,kBAAC,UAAD;UAAQ,OAAM;oBAAW;UAAoB,CAAA;SACtC;UACH;;MAET,KAAS,EAAM,gBAAgB,KAC9B,kBAAC,KAAD;OAAG,WAAU;OAAgC,eAAY;iBAAzD;QACG,EAAM;QAAc;QAAW,EAAM,kBAAkB,IAAI,QAAQ;QAAQ;QAC/D,EAAM,kBAAkB,IAAI,mBAAmB;QAAmB;QAE7E;;MAEL,KACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,IAAD,EAAc,SAAS,EAAM,IAAM,CAAA;OAC/B,CAAA;MAEJ;;IACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;IACtC,MAAW,CAAC,IACX,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAkB,CAAA,GACjE,KAAS,MAAW,SACtB,kBAAC,IAAD;KACS;KACC;KACC;KACJ;KACL,MAAM,EAAU;KACH;KACb,aAAa,MAAW,KAAK,EAAS,EAAO;KAC7C,QAAQ;KACR,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,GAAO,QAAQ,KAAK,GAAQ,MAC3B,kBAAC,IAAD;MAEE,SAAS,EAAM;MACP;MACR,SAAS,EAAM;MACf,OAAO,EAAQ,EAAO;MACtB,SAAS,MAAU;MACnB,QAAQ,MAAU,EAAM,QAAQ,SAAS;MACzC,MAAM,EAAU;MACX;MACL,aAAa,MAAgB,EAAO;MACpC,kBAAkB,KAAK,EAAS,EAAO;MACvC,QAAQ;MACR,UAAU,MAAU,GAAY,GAAO,EAAM;MAC7C,EAbK,EAAO,GAaZ,CACF,EACD,KAAS,EAAM,QAAQ,WAAW,KACjC,kBAAC,KAAD;MAAG,WAAU;gBAAoC;MAE7C,CAAA,CAEF;;IAEJ;;EACiB,CAAA,GA9HvB,kBAAC,GAAD,EAAA,UACE,kBAAC,IAAD;EAAa,WAAU;EAA4B,eAAY;YAA/D;GACE,kBAAC,GAAD,EAAY,WAAU,yCAA0C,CAAA;GAChE,kBAAC,KAAD;IAAG,WAAU;cAAb,CAA8C,GAAY,qBAAsB;;GAChF,kBAAC,GAAD;IACE,MAAK;IACL,UAAU,EAAiB;IAC3B,eACE,QAEI,EAAY,EACV,WAAW,EACT,OAAO;KACL;KACA,MAAM,GAAG,EAAY;KACrB,MAAM;KACN,WAAW;KACX,oBAAoB;KACrB,EACF,EACF,CAAC,EACJ,kCACD;cAlBL,CAqBE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,EAAiB,UAAU,cAAc,eACnC;;GACT,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAGzC,CAAA;GACH,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;GAC3B;KACT,CAAA;;AAmGb,SAAS,GAAS,EAChB,SACA,WACA,YACA,SACA,QACA,aAQC;CACD,IAAM,IAAW,GAAuB,EAClC,IAAW,EAAW,EAAc,EACpC,IAAO,EAAa,EAAK,aAAa,EAAI,EAE1C,IAAY,EAAe,GAAU,EAAK,QAAQ,EAAS,EAC3D,IAAU,EAAQ,QACrB,MAAM,EAAE,OAAO,EAAO,OAAO,CAAC,EAAE,gBAAgB,EAAU,SAAS,EAAE,aAAa,EACpF;AACD,QACE,kBAAC,MAAD;EACE,WAAA;EACA,cAAc,MAAM;AAElB,GADA,EAAE,aAAa,QAAQ,cAAc,EAAK,GAAG,EAC7C,EAAE,aAAa,gBAAgB;;EAEjC,WAAU;EACV,eAAY;YAPd;GASE,kBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,eAAe,EAAS,kCAAkC,mBAAmB,EAAK,GAAG,GAAG;cAEvF,EAAK;IACC,CAAA;GACT,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAA,UAAO,EAAK,KAAW,CAAA;KAEtB,MAAS,QACR,kBAAC,QAAD;MACE,eAAY;MACZ,WAAW,GAAS,EAAK;MACzB,OAAO,MAAM,EAAc,GAAU,EAAK,OAAO,CAAC,SAAS,IAAI,KAAK,EAAK,eAAe,GAAG,CAAC,oBAAoB;gBAE/G,MAAS,IAAI,UAAU,GAAG,EAAK;MAC3B,CAAA;KAET,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAK,eAAe,GAAE,OAAW,EAAA,CAAA;KACpC;;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAK,aAAa,kBAAC,GAAD,EAAY,IAAI,EAAK,YAAc,CAAA,GAAG;KACpD,CAAA,EAEP,kBAAC,UAAD;KACE,cAAY,QAAQ,EAAK,IAAI;KAC7B,WAAU;KACV,OAAM;KACN,UAAU;KACV,WAAW,MAAM,EAAE,OAAO,SAAS,EAAO,EAAK,IAAI,EAAE,OAAO,MAAM;eALpE,CAOE,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAc,CAAA,EAC9B,EAAQ,KAAK,MACZ,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBACzB,EAAE;MACI,EAFI,EAAE,GAEN,CACT,CACK;OACL;;GACH;;;AAKT,SAAS,GAAS,EAChB,WACA,WACA,cACA,aACA,YAOC;CACD,IAAM,CAAC,GAAU,KAAe,EAAS,GAAM;AAC/C,QACE,kBAAC,WAAD;EACE,cAAY;EACZ,WAAW,GAAG,EAAU,GAAG,IAAW,wBAAwB;EAC9D,aAAa,MAAM;AAEjB,GADA,EAAE,gBAAgB,EAClB,EAAY,GAAK;;EAEnB,mBAAmB,EAAY,GAAM;EACrC,SAAS,MAAM;AAEb,GADA,EAAE,gBAAgB,EAClB,EAAY,GAAM;GAClB,IAAM,IAAa,EAAE,aAAa,QAAQ,aAAa;AACvD,GAAI,KAAY,EAAO,GAAY,EAAO,GAAG;;EAG9C;EACO,CAAA;;AAKd,SAAS,GAAU,EACjB,UACA,YACA,iBAKC;CACD,IAAM,IAAS,EAAM,aAAa,EAAM,MAAM;AAE9C,QADI,KAAU,KAAK,CAAC,EAAM,SAAS,cAAoB,OAErD,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,KAAD;GAAG,WAAU;aAAb;IAA6C;IAClC,EAAM,MAAM;IAAO;IAAK,EAAM;IACrC;MACJ,kBAAC,GAAD;GACE,MAAK;GACL,SAAQ;GACR,WAAU;GACV,eAAY;GACZ,UAAU;GACV,SAAS;aAER,IAAU,aAAa,QAAQ,KAAK,IAAI,GAAQ,EAAiB,CAAC;GAC5D,CAAA,CACL;;;AAQV,SAAS,GAAc,EACrB,UACA,WACA,YACA,QACA,SACA,gBACA,eACA,aAUC;CACD,IAAM,IAAY,EAAM,QAAQ,KAAK,OAAY;EAAE;EAAQ,OAAO,EAAQ,EAAO;EAAE,EAAE,EAC/E,IAAQ,CACZ,GAAG,IAAI,IAAI,EAAU,SAAS,EAAE,eAAY,EAAM,MAAM,KAAK,MAAM,EAAO,GAAG,EAAO,CAAC,CAAC,CAAC,CACxF,CAAC,MAAM,GAAG,MACT,MAAW,aACP,EAAe,QAAQ,EAAE,GAAG,EAAe,QAAQ,EAAE,GACrD,MAAM,KACJ,IACA,MAAM,KACJ,KACA,EAAE,cAAc,EAAE,CAC3B,EACK,KAAY,MAChB,MAAW,aAAa,EAAY,EAAK,GAAG,IAAO,kBAAC,GAAD,EAAY,IAAI,GAAQ,CAAA,GAAG;AAChF,QACE,kBAAC,OAAD;EAAK,WAAU;EAAiC,eAAY;YAA5D;GACE,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KACiB;KAC3D,MAAW,aAAa,aAAa;KAAW;KAC/C;;GACJ,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAU,KAAK,EAAE,WAAQ,eACxB,kBAAC,OAAD;KAAqB,WAAU;eAC7B,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACG,EAAO;OAAM;OACd,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAO,WAAW,GAAG,EAAM,WAAW,KAAK,EAAO,aAAa,EAAM;QACjE,CAAA;OACJ;;KACD,EAPI,EAAO,GAOX,CACN;IACE,CAAA;GACL,EAAM,WAAW,KAChB,kBAAC,KAAD;IAAG,WAAU;cAAoC;IAA+B,CAAA;GAEjF,EAAM,KAAK,MACV,kBAAC,OAAD;IAA0B,eAAY;IAAa,WAAU;cAA7D,CACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAS,EAAK;MAAE;MACjB,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QAAwC;QAErC,EAAU,QACR,GAAG,EAAE,eAAY,IAAI,EAAM,MAAM,QAAQ,MAAM,EAAO,GAAG,EAAO,KAAK,EAAK,CAAC,QAC5E,EACD;QAAC;QAEG;;MACL;QACJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAU,KAAK,EAAE,WAAQ,eACxB,kBAAC,IAAD;MAEU;MACA;MACR,OAAO,GAAG,EAAO,KAAK;MACtB,WAAU;gBAEV,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAM,MACJ,QAAQ,MAAM,EAAO,GAAG,EAAO,KAAK,EAAK,CACzC,KAAK,MACJ,kBAAC,IAAD;QAEQ;QACE;QACR,SAAS,EAAM;QACT;QACD;QACG;QACR,EAPK,EAAK,GAOV,CACF;OACD,CAAA;MACI,EArBJ,EAAO,GAqBH,CACX;KACE,CAAA,CACF;MAvCI,KAAQ,OAuCZ,CACN;GACF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAU,KAAK,EAAE,WAAQ,eACxB,kBAAC,OAAD;KAAqB,WAAU;eAC7B,kBAAC,IAAD;MACS;MACP,SAAS,MAAgB,EAAO;MAChC,kBAAkB,EAAW,EAAO;MACpC,CAAA;KACE,EANI,EAAO,GAMX,CACN;IACE,CAAA;GACF;;;AAIV,SAAS,GAAgB,EACvB,YACA,WACA,YACA,UACA,YACA,WACA,SACA,QACA,gBACA,eACA,WACA,cAcC;CACD,IAAM,IAAQ,EAAM,YACd,IAAU,EAAO,YAAY,QAAQ,EAAO,WAAW,KAAK,KAAS,EAAO,UAC5E,IAAa,GAAe;AAElC,QACE,kBAAC,IAAD;EACU;EACA;EACR,OAAO,GAAG,EAAO,KAAK;EACtB,WAAU;YAEV,kBAAC,OAAD;GAAK,eAAa,gBAAgB,EAAO;aAAzC;IACE,kBAAC,UAAD;KAAQ,WAAU;eAAlB,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAgC,EAAO;OAAU,CAAA,EAC/D,kBAAC,QAAD;OACE,eAAa,sBAAsB,EAAO;OAC1C,WAAW,+BAA+B,IAAU,iDAAiD;OACrG,OAAO,EAAO,WAAW,aAAa,EAAO,aAAa,KAAA;iBAEzD,EAAO,WAAW,GAAG,EAAM,KAAK,EAAO,aAAa;OAChD,CAAA,CACH;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAO,eACJ,gBAAgB,EAAW,EAAO,aAAa,KAC/C;OACC,CAAA,EACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,WAAU;SACV,cAAY,QAAQ,EAAO,KAAK;SAChC,UAAU;SACV,eAAe,EAAQ,GAAG;mBAE1B,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA;SAChC,CAAA;QACT,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,WAAU;SACV,cAAY,QAAQ,EAAO,KAAK;SAChC,UAAU;SACV,eAAe,EAAQ,EAAE;mBAEzB,kBAAC,GAAD,EAAc,WAAU,eAAgB,CAAA;SACjC,CAAA;QACT,kBAAC,IAAD;SAAuB;SAAiB;SAAU,CAAA;QAC7C;SACH;QACC;;IACT,kBAAC,MAAD;KAAI,WAAU;eAAd,CACG,EAAM,MAAM,KAAK,MAChB,kBAAC,IAAD;MAEQ;MACE;MACC;MACH;MACD;MACG;MACR,EAPK,EAAK,GAOV,CACF,EACD,MAAU,KAAK,kBAAC,MAAD;MAAI,WAAU;gBAAiD;MAAU,CAAA,CACtF;;IACL,kBAAC,IAAD;KAAkB;KAAO,SAAS;KAAyB;KAAc,CAAA;IACrE;;EACG,CAAA;;AAIf,SAAS,GAAa,EAAE,YAAS,aAAqD;CACpF,IAAM,IAAU,MAAW,KAAA,GACrB,IAAa,GAAe,EAC5B,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAM,KAAW,EAAS,GAAQ,QAAQ,GAAG,EAC9C,CAAC,GAAc,KAAmB,EAAS,GAAQ,gBAAgB,GAAG,EACtE,CAAC,GAAK,KAAU,EAAS,GAAQ,WAAW,OAAO,EAAO,SAAS,GAAG,GAAG,EACzE,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAU;EAAE,gBAAgB;EAAS,qBAAqB;EAAM,EAChE,CAAC,IAAQ,KAAe,EAAY,GAAe,EAAQ,EAC3D,CAAC,GAAQ,KAAe,EAAY,GAAe,EAAQ,EAC3D,CAAC,GAAQ,KAAe,EAAY,IAAe,EAAQ,EAC3D,IAAO,EAAY,WAAW,EAAY,WAAW,EAAY,SAEjE,IAAW,EAAI,MAAM,KAAK,KAAK,OAAO,OAAO,EAAI,EACjD,IAAa,MAAa,SAAS,CAAC,OAAO,UAAU,EAAS,IAAI,IAAW,IAC7E,KAAU,EAAK,MAAM,KAAK,MAAM,CAAC,KAAc,CAAC,GAEhD,UAAc;AAIlB,EAHA,EAAQ,GAAQ,QAAQ,GAAG,EAC3B,EAAgB,GAAQ,gBAAgB,GAAG,EAC3C,EAAO,GAAQ,WAAW,OAAO,EAAO,SAAS,GAAG,GAAG,EACvD,EAAS,KAAK;IAGV,KAAO,YAAY;AACvB,IAAS,KAAK;EACd,IAAM,IAAS;GACb,MAAM,EAAK,MAAM;GACjB,cAAc,KAAgB;GAC9B,UAAU;GACX;AACD,MAAI;AAQF,GAPI,IACF,MAAM,EAAa,EAAO,EAAE,WAAW;IAAE,IAAI,EAAO;IAAI,OAAO;IAAQ,EAAE,CAAC,CAAC,GAE3E,MAAM,EACJ,GAAO,EAAE,WAAW,EAAE,OAAO;IAAE;IAAS,MAAM;IAAU,GAAG;IAAQ,EAAE,EAAE,CAAC,CACzE,EAEH,EAAQ,GAAM;WACP,GAAG;AACV,KAAS,EAAU,GAAG,iCAAiC,CAAC;;IAItD,IAAW,IAAU,UAAU,EAAO,OAAO;AAEnD,QACE,kBAAC,IAAD;EACQ;EACN,eAAe,MAAS;AAEtB,GADA,EAAQ,EAAK,EACT,KAAM,GAAO;;YAJrB,CAOE,kBAAC,GAAD;GAAyB,SAAA;aACtB,IACC,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,WAAU;IACV,cAAY,QAAQ,EAAO,KAAK;cAEhC,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA;IAC3B,CAAA,GAET,kBAAC,GAAD;IAAQ,MAAK;IAAK,SAAQ;cAA1B,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,aAE1B;;GAEa,CAAA,EAC1B,kBAAC,IAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,IAAU,QAAQ,EAAO,SAAS,cACb,CAAA,EACxB,kBAAC,GAAD,EAAA,UAA6B,kFAEC,CAAA,CACP,EAAA,CAAA;IACzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAQ,WAAU;iBAAsB;OAE5D,CAAA,EACR,kBAAC,GAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAU;OACV,OAAO;OACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;OACxC,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAU,WAAU;iBAAsB;OAE9D,CAAA,EACR,kBAAC,UAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAU;OACV,OAAO;OACP,WAAW,MAAM,EAAgB,EAAE,OAAO,MAAM;iBAJlD,CAME,kBAAC,UAAD;QAAQ,OAAM;kBAAG;QAAsC,CAAA,EACtD,EAAS,KAAK,MACb,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAW,EAAE;QACP,EAFI,EAEJ,CACT,CACK;SACL,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAO,WAAU;kBAAsB;QAE3D,CAAA;OACR,kBAAC,GAAD;QACE,IAAI,GAAG,EAAS;QAChB,MAAK;QACL,KAAK;QACL,WAAU;QACV,OAAO;QACP,WAAW,MAAM,EAAO,EAAE,OAAO,MAAM;QACvC,CAAA;OACD,KAAc,kBAAC,GAAD,EAAW,SAAQ,oDAAqD,CAAA;OACnF,EAAA,CAAA;MACF;;IACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;IACvC,kBAAC,GAAD;KAAwB,WAAU;eAAlC,CACG,KACC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAoB,SAAA;gBAClB,kBAAC,GAAD;OAAQ,SAAQ;OAAU,UAAU;iBAAM;OAEjC,CAAA;MACU,CAAA,EACrB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA;MAAkB;MAAQ,EAAO;MAAK;MAAoB,EAAA,CAAA,EAC1D,kBAAC,GAAD,EAAA,UAAwB,2HAGC,CAAA,CACP,EAAA,CAAA,EACpB,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,IAAD,EAAA,UAAmB,WAA2B,CAAA,EAC9C,kBAAC,GAAD;MACE,WAAU;MACV,SAAS,YAAY;AACnB,WAAI;AAEF,QADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,EAAO,IAAI,EAAE,CAAC,CAAC,EAC5D,EAAQ,GAAM;gBACP,GAAG;AACV,UAAS,EAAU,GAAG,mCAAmC,CAAC;;;gBAG/D;MAEmB,CAAA,CACF,EAAA,CAAA,CACD,EAAA,CAAA,CACT,EAAA,CAAA,EAEhB,kBAAC,GAAD;MAAQ,SAAS;MAAM,UAAU,CAAC;MAAS,eAAa,GAAG,EAAS;gBACjE,IAAO,YAAY,IAAU,SAAS;MAChC,CAAA,CACc;;IACD;KACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/ProjectControls.tsx"],"names":[],"mappings":"AAWA;;;;;GAKG;AAEH,eAAO,MAAM,gBAAgB,kFAOnB,CAAC;AAyBX,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,IAAI,EACJ,MAAM,GACP,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,2CA6EA"}
1
+ {"version":3,"file":"ProjectControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/ProjectControls.tsx"],"names":[],"mappings":"AAWA;;;;;GAKG;AAEH,eAAO,MAAM,gBAAgB,kFAOnB,CAAC;AAyBX,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,IAAI,EACJ,MAAM,GACP,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,2CAuFA"}
@@ -4,104 +4,111 @@ import { detailPath as r } from "../utils/rowNavigation.js";
4
4
  import { ConfirmButton as i, ErrorLine as a } from "./ConfirmButton.js";
5
5
  import { enumLabel as o } from "./WorkItemControls.js";
6
6
  import { useState as s } from "react";
7
- import { Pencil as c, Trash2 as l } from "lucide-react";
8
- import { gql as u } from "@apollo/client";
9
- import { useMutation as d } from "@apollo/client/react";
10
- import { jsx as f, jsxs as p } from "react/jsx-runtime";
11
- import { Button as m } from "@burdenoff/fe-libs/ui";
7
+ import { Pencil as c, Trash2 as l, Workflow as u } from "lucide-react";
8
+ import { gql as d } from "@apollo/client";
9
+ import { useMutation as f } from "@apollo/client/react";
10
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
11
+ import { Button as h } from "@burdenoff/fe-libs/ui";
12
12
  //#region src/planmagnet/controls/ProjectControls.tsx
13
- var h = [
13
+ var g = [
14
14
  "PLANNING",
15
15
  "ACTIVE",
16
16
  "ON_HOLD",
17
17
  "COMPLETED",
18
18
  "CANCELLED",
19
19
  "ARCHIVED"
20
- ], g = u`
20
+ ], _ = d`
21
21
  mutation ProjectControlsStatus($id: ID!, $input: PlanmagnetUpdateProjectInput!) {
22
22
  planmagnetUpdateProject(id: $id, input: $input) {
23
23
  id
24
24
  status
25
25
  }
26
26
  }
27
- `, _ = u`
27
+ `, v = d`
28
28
  mutation ProjectControlsDelete($id: ID!) {
29
29
  planmagnetDeleteProject(id: $id)
30
30
  }
31
- `, v = [
31
+ `, y = [
32
32
  "GetProjects",
33
33
  "GetActiveProjects",
34
34
  "GetBoardProjects",
35
35
  "GetDashboardData",
36
36
  "AdminPlanmagnetProjects"
37
37
  ];
38
- function y({ projectId: u, name: y, status: b }) {
39
- let x = e(), [S, C] = d(g, {
40
- refetchQueries: ["GetProjectDetail", ...v],
38
+ function b({ projectId: d, name: b, status: x }) {
39
+ let S = e(), [C, w] = f(_, {
40
+ refetchQueries: ["GetProjectDetail", ...y],
41
41
  awaitRefetchQueries: !0
42
- }), [w, T] = d(_, {
43
- refetchQueries: v,
42
+ }), [T, E] = f(v, {
43
+ refetchQueries: y,
44
44
  context: t
45
- }), [E, D] = s(null), O = C.loading || T.loading, k = h.includes(b) ? h : [b, ...h], A = async (e) => {
46
- D(null);
45
+ }), [D, O] = s(null), k = w.loading || E.loading, A = g.includes(x) ? g : [x, ...g], j = async (e) => {
46
+ O(null);
47
47
  try {
48
- await n(S({ variables: {
49
- id: u,
48
+ await n(C({ variables: {
49
+ id: d,
50
50
  input: { status: e }
51
51
  } }));
52
52
  } catch (e) {
53
- D(e instanceof Error ? e.message : "The status could not be changed.");
53
+ O(e instanceof Error ? e.message : "The status could not be changed.");
54
54
  }
55
- }, j = async () => {
56
- D(null);
55
+ }, M = async () => {
56
+ O(null);
57
57
  try {
58
- await n(w({ variables: { id: u } })), x("/projects/projects-page");
58
+ await n(T({ variables: { id: d } })), S("/projects/projects-page");
59
59
  } catch (e) {
60
- D(e instanceof Error ? e.message : "The project could not be deleted.");
60
+ O(e instanceof Error ? e.message : "The project could not be deleted.");
61
61
  }
62
62
  };
63
- return /* @__PURE__ */ p("div", {
63
+ return /* @__PURE__ */ m("div", {
64
64
  className: "flex flex-col items-end gap-2",
65
- children: [/* @__PURE__ */ p("div", {
65
+ children: [/* @__PURE__ */ m("div", {
66
66
  className: "flex flex-wrap items-center justify-end gap-2",
67
67
  children: [
68
- /* @__PURE__ */ f("select", {
68
+ /* @__PURE__ */ p("select", {
69
69
  "aria-label": "Project status",
70
70
  "data-testid": "project-status",
71
71
  className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
72
- value: b,
73
- disabled: O,
72
+ value: x,
73
+ disabled: k,
74
74
  onChange: (e) => {
75
- e.target.value !== b && A(e.target.value);
75
+ e.target.value !== x && j(e.target.value);
76
76
  },
77
- children: k.map((e) => /* @__PURE__ */ f("option", {
77
+ children: A.map((e) => /* @__PURE__ */ p("option", {
78
78
  value: e,
79
79
  children: o(e)
80
80
  }, e))
81
81
  }),
82
- /* @__PURE__ */ p(m, {
82
+ /* @__PURE__ */ m(h, {
83
83
  size: "sm",
84
84
  variant: "outline",
85
85
  "data-testid": "project-edit",
86
- onClick: () => x(r("/projects/edit-project-page", u)),
87
- children: [/* @__PURE__ */ f(c, { className: "mr-1 h-4 w-4" }), "Edit"]
86
+ onClick: () => S(r("/projects/edit-project-page", d)),
87
+ children: [/* @__PURE__ */ p(c, { className: "mr-1 h-4 w-4" }), "Edit"]
88
88
  }),
89
- /* @__PURE__ */ f(i, {
89
+ /* @__PURE__ */ m(h, {
90
+ size: "sm",
91
+ variant: "outline",
92
+ "data-testid": "project-workflow",
93
+ onClick: () => S(r("/projects/project-workflow-page", d)),
94
+ children: [/* @__PURE__ */ p(u, { className: "mr-1 h-4 w-4" }), "Workflow"]
95
+ }),
96
+ /* @__PURE__ */ p(i, {
90
97
  label: "Delete",
91
98
  variant: "outline",
92
- icon: /* @__PURE__ */ f(l, { className: "mr-1 h-4 w-4" }),
93
- title: `Delete ${y}?`,
99
+ icon: /* @__PURE__ */ p(l, { className: "mr-1 h-4 w-4" }),
100
+ title: `Delete ${b}?`,
94
101
  description: "The project is deleted. Its tasks, sprints, boards, milestones and budgets disappear from every list, and so do releases that belong to no other project. This cannot be undone here.",
95
102
  confirmLabel: "Delete project",
96
103
  destructive: !0,
97
- disabled: O,
98
- onConfirm: () => void j()
104
+ disabled: k,
105
+ onConfirm: () => void M()
99
106
  })
100
107
  ]
101
- }), E && /* @__PURE__ */ f(a, { message: E })]
108
+ }), D && /* @__PURE__ */ p(a, { message: D })]
102
109
  });
103
110
  }
104
111
  //#endregion
105
- export { h as PROJECT_STATUSES, y as ProjectHeaderActions };
112
+ export { g as PROJECT_STATUSES, b as ProjectHeaderActions };
106
113
 
107
114
  //# sourceMappingURL=ProjectControls.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectControls.js","names":[],"sources":["../../../src/planmagnet/controls/ProjectControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Project controls (BOFF-7940). A new project starts in PLANNING and, until 2026-09-27, nothing in the UI could\n * move it: no status control, and the Edit Project page (which had no Status field) was linked from nowhere. So\n * Projects > Active, the Dashboard's project status card and the admin Bulk Archive never showed a project created\n * in the UI.\n */\n\nexport const PROJECT_STATUSES = [\n 'PLANNING',\n 'ACTIVE',\n 'ON_HOLD',\n 'COMPLETED',\n 'CANCELLED',\n 'ARCHIVED',\n] as const;\n\nconst UPDATE_PROJECT_STATUS = gql`\n mutation ProjectControlsStatus($id: ID!, $input: PlanmagnetUpdateProjectInput!) {\n planmagnetUpdateProject(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DELETE_PROJECT = gql`\n mutation ProjectControlsDelete($id: ID!) {\n planmagnetDeleteProject(id: $id)\n }\n`;\n\n/** Every query that lists projects or shows a project's status. */\nconst PROJECT_LIST_QUERIES = [\n 'GetProjects',\n 'GetActiveProjects',\n 'GetBoardProjects',\n 'GetDashboardData',\n 'AdminPlanmagnetProjects',\n];\n\nexport function ProjectHeaderActions({\n projectId,\n name,\n status,\n}: {\n projectId: string;\n name: string;\n status: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [updateStatus, statusState] = useMutation(UPDATE_PROJECT_STATUS, {\n refetchQueries: ['GetProjectDetail', ...PROJECT_LIST_QUERIES],\n awaitRefetchQueries: true,\n });\n const [remove, removeState] = useMutation(DELETE_PROJECT, {\n refetchQueries: PROJECT_LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = statusState.loading || removeState.loading;\n const statuses = (PROJECT_STATUSES as readonly string[]).includes(status)\n ? PROJECT_STATUSES\n : [status, ...PROJECT_STATUSES];\n\n const onStatus = async (next: string) => {\n setError(null);\n try {\n await throwOnError(updateStatus({ variables: { id: projectId, input: { status: next } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: projectId } }));\n navigate('/projects/projects-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The project could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Project status\"\n data-testid=\"project-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"project-edit\"\n onClick={() => navigate(detailPath('/projects/edit-project-page', projectId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${name}?`}\n description=\"The project is deleted. Its tasks, sprints, boards, milestones and budgets disappear from every list, and so do releases that belong to no other project. This cannot be undone here.\"\n confirmLabel=\"Delete project\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,IAAmB;CAC9B;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IAAwB,CAAG;;;;;;;GAQ3B,IAAiB,CAAG;;;;GAOpB,IAAuB;CAC3B;CACA;CACA;CACA;CACA;CACD;AAED,SAAgB,EAAqB,EACnC,cACA,SACA,aAKC;CACD,IAAM,IAAW,GAAuB,EAClC,CAAC,GAAc,KAAe,EAAY,GAAuB;EACrE,gBAAgB,CAAC,oBAAoB,GAAG,EAAqB;EAC7D,qBAAqB;EACtB,CAAC,EACI,CAAC,GAAQ,KAAe,EAAY,GAAgB;EACxD,gBAAgB;EAChB,SAAS;EACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAO,EAAY,WAAW,EAAY,SAC1C,IAAY,EAAuC,SAAS,EAAO,GACrE,IACA,CAAC,GAAQ,GAAG,EAAiB,EAE3B,IAAW,OAAO,MAAiB;AACvC,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,EAAa,EAAE,WAAW;IAAE,IAAI;IAAW,OAAO,EAAE,QAAQ,GAAM;IAAE,EAAE,CAAC,CAAC;WACpF,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,mCAAmC;;IAG3E,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,GAAW,EAAE,CAAC,CAAC,EAC5D,EAAS,0BAA0B;WAC5B,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,oCAAoC;;;AAIlF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,UAAD;KACE,cAAW;KACX,eAAY;KACZ,WAAU;KACV,OAAO;KACP,UAAU;KACV,WAAW,MAAM;AACf,MAAI,EAAE,OAAO,UAAU,KAAa,EAAS,EAAE,OAAO,MAAM;;eAG7D,EAAS,KAAK,MACb,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAU,EAAE;MACN,EAFI,EAEJ,CACT;KACK,CAAA;IACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KACZ,eAAe,EAAS,EAAW,+BAA+B,EAAU,CAAC;eAJ/E,CAME,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;;IACT,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;KACzC,OAAO,UAAU,EAAK;KACtB,aAAY;KACZ,cAAa;KACb,aAAA;KACA,UAAU;KACV,iBAAiB,KAAK,GAAU;KAChC,CAAA;IACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC"}
1
+ {"version":3,"file":"ProjectControls.js","names":[],"sources":["../../../src/planmagnet/controls/ProjectControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2, Workflow } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Project controls (BOFF-7940). A new project starts in PLANNING and, until 2026-09-27, nothing in the UI could\n * move it: no status control, and the Edit Project page (which had no Status field) was linked from nowhere. So\n * Projects > Active, the Dashboard's project status card and the admin Bulk Archive never showed a project created\n * in the UI.\n */\n\nexport const PROJECT_STATUSES = [\n 'PLANNING',\n 'ACTIVE',\n 'ON_HOLD',\n 'COMPLETED',\n 'CANCELLED',\n 'ARCHIVED',\n] as const;\n\nconst UPDATE_PROJECT_STATUS = gql`\n mutation ProjectControlsStatus($id: ID!, $input: PlanmagnetUpdateProjectInput!) {\n planmagnetUpdateProject(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DELETE_PROJECT = gql`\n mutation ProjectControlsDelete($id: ID!) {\n planmagnetDeleteProject(id: $id)\n }\n`;\n\n/** Every query that lists projects or shows a project's status. */\nconst PROJECT_LIST_QUERIES = [\n 'GetProjects',\n 'GetActiveProjects',\n 'GetBoardProjects',\n 'GetDashboardData',\n 'AdminPlanmagnetProjects',\n];\n\nexport function ProjectHeaderActions({\n projectId,\n name,\n status,\n}: {\n projectId: string;\n name: string;\n status: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [updateStatus, statusState] = useMutation(UPDATE_PROJECT_STATUS, {\n refetchQueries: ['GetProjectDetail', ...PROJECT_LIST_QUERIES],\n awaitRefetchQueries: true,\n });\n const [remove, removeState] = useMutation(DELETE_PROJECT, {\n refetchQueries: PROJECT_LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = statusState.loading || removeState.loading;\n const statuses = (PROJECT_STATUSES as readonly string[]).includes(status)\n ? PROJECT_STATUSES\n : [status, ...PROJECT_STATUSES];\n\n const onStatus = async (next: string) => {\n setError(null);\n try {\n await throwOnError(updateStatus({ variables: { id: projectId, input: { status: next } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: projectId } }));\n navigate('/projects/projects-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The project could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Project status\"\n data-testid=\"project-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"project-edit\"\n onClick={() => navigate(detailPath('/projects/edit-project-page', projectId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n {/* BOFF-8043: status names, hidden statuses and allowed moves for this project. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"project-workflow\"\n onClick={() => navigate(detailPath('/projects/project-workflow-page', projectId))}\n >\n <Workflow className=\"mr-1 h-4 w-4\" />\n Workflow\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${name}?`}\n description=\"The project is deleted. Its tasks, sprints, boards, milestones and budgets disappear from every list, and so do releases that belong to no other project. This cannot be undone here.\"\n confirmLabel=\"Delete project\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,IAAmB;CAC9B;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IAAwB,CAAG;;;;;;;GAQ3B,IAAiB,CAAG;;;;GAOpB,IAAuB;CAC3B;CACA;CACA;CACA;CACA;CACD;AAED,SAAgB,EAAqB,EACnC,cACA,SACA,aAKC;CACD,IAAM,IAAW,GAAuB,EAClC,CAAC,GAAc,KAAe,EAAY,GAAuB;EACrE,gBAAgB,CAAC,oBAAoB,GAAG,EAAqB;EAC7D,qBAAqB;EACtB,CAAC,EACI,CAAC,GAAQ,KAAe,EAAY,GAAgB;EACxD,gBAAgB;EAChB,SAAS;EACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAO,EAAY,WAAW,EAAY,SAC1C,IAAY,EAAuC,SAAS,EAAO,GACrE,IACA,CAAC,GAAQ,GAAG,EAAiB,EAE3B,IAAW,OAAO,MAAiB;AACvC,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,EAAa,EAAE,WAAW;IAAE,IAAI;IAAW,OAAO,EAAE,QAAQ,GAAM;IAAE,EAAE,CAAC,CAAC;WACpF,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,mCAAmC;;IAG3E,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,GAAW,EAAE,CAAC,CAAC,EAC5D,EAAS,0BAA0B;WAC5B,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,oCAAoC;;;AAIlF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,UAAD;KACE,cAAW;KACX,eAAY;KACZ,WAAU;KACV,OAAO;KACP,UAAU;KACV,WAAW,MAAM;AACf,MAAI,EAAE,OAAO,UAAU,KAAa,EAAS,EAAE,OAAO,MAAM;;eAG7D,EAAS,KAAK,MACb,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAU,EAAE;MACN,EAFI,EAEJ,CACT;KACK,CAAA;IACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KACZ,eAAe,EAAS,EAAW,+BAA+B,EAAU,CAAC;eAJ/E,CAME,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;;IAET,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KACZ,eAAe,EAAS,EAAW,mCAAmC,EAAU,CAAC;eAJnF,CAME,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EAAA,WAE9B;;IACT,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;KACzC,OAAO,UAAU,EAAK;KACtB,aAAY;KACZ,cAAa;KACb,aAAA;KACA,UAAU;KACV,iBAAiB,KAAK,GAAU;KAChC,CAAA;IACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC"}
@@ -18,6 +18,15 @@ export declare function WorkItemEnumSelect({ workItemId, field, value, options,
18
18
  options: readonly string[];
19
19
  label: string;
20
20
  }): import("react/jsx-runtime").JSX.Element;
21
+ /**
22
+ * The status select (BOFF-8043): the project's names for its statuses, and only the moves its workflow allows — the
23
+ * server refuses anything else anyway, with the reason shown below the select.
24
+ */
25
+ export declare function WorkItemStatusSelect({ workItemId, projectId, value, }: {
26
+ workItemId: string;
27
+ projectId: string;
28
+ value: string;
29
+ }): import("react/jsx-runtime").JSX.Element;
21
30
  /**
22
31
  * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any
23
32
  * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet
@@ -1 +1 @@
1
- {"version":3,"file":"WorkItemControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"names":[],"mappings":"AAWA;;;;;;;;GAQG;AAEH,eAAO,MAAM,kBAAkB,0FAQrB,CAAC;AACX,eAAO,MAAM,oBAAoB,wDAAyD,CAAC;AAE3F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C;AA0CD,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,EACV,KAAK,EACL,KAAK,EACL,OAAO,EACP,KAAK,GACN,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,2CAyBA;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;;;;;;;;EAmBtE;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CAkCA;AAED,2DAA2D;AAC3D,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,UAAU,GACX,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,2CAcA;AAED,kDAAkD;AAClD,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,KAAK,GACN,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,2CA6CA"}
1
+ {"version":3,"file":"WorkItemControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AAEH,eAAO,MAAM,kBAAkB,0FAQrB,CAAC;AACX,eAAO,MAAM,oBAAoB,wDAAyD,CAAC;AAE3F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C;AA0CD,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,EACV,KAAK,EACL,KAAK,EACL,OAAO,EACP,KAAK,GACN,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,2CAyBA;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,SAAS,EACT,KAAK,GACN,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,2CA8BA;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;;;;;;;;EAmBtE;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CAkCA;AAED,2DAA2D;AAC3D,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,UAAU,GACX,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,2CAcA;AAED,kDAAkD;AAClD,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,KAAK,GACN,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,2CA6CA"}