@burdenoff/microfe-planmagnet 2026.1001.7 → 2026.1001.8
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.
- package/dist/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
- package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
- package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
- package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoadmapNowNextLater.js","names":[],"sources":["../../../src/planmagnet/roadmaps/RoadmapNowNextLater.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type DragEvent } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n Calendar,\n Gauge,\n Inbox,\n Loader2,\n ShieldAlert,\n ShieldCheck,\n} from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { RoadmapItemCommentsButton } from '../comments/RoadmapComments';\n\n/**\n * Now/Next/Later (PRM-06, PRM-09, PRM-10): a VIEW of a roadmap's items grouped by `horizon`, alongside the\n * existing Timeline view (`ProductsRoadmapDetailPagePage.tsx`) — not a separate roadmap type. Placement is\n * changed by dragging a card, or by the keyboard \"Move…\" select every card also carries (the same mutation\n * either way: `planmagnetMoveRoadmapItem`), never optimistically: the list shown is always what\n * `planmagnetRoadmapItems` last returned, so a refused move has nothing local to \"undo\" — the card simply never\n * left its column.\n *\n * `planmagnetMoveRoadmapItem`'s `index` is scoped to the (roadmap, horizon, **swimlane**) group, not to the\n * column as a whole — the same lane-scoping the Timeline view treats as first-class (`swimlaneGroups` there).\n * Each column therefore renders its cards **grouped by lane** (one sub-list per lane, in the same order in\n * every column), exactly like the Timeline view's per-lane cards. A drag/drop within a lane's sub-list, or a\n * \"Move…\" menu action, only ever computes an index against that same sub-list — so \"the end of this section\"\n * (what the user sees) and \"the end of this lane's server-side group\" (what the index means) are always the\n * same place, with no cross-lane ambiguity. Dropping a card onto a *different* lane's section is rejected (the\n * move never changes `swimlane`); the lane header is only shown once a roadmap actually uses more than one\n * lane, so a single-lane (the common case) or lane-less roadmap renders exactly one unlabelled section per\n * column — visually identical to a flat list.\n */\n\nconst ITEM_FIELDS = `\n id\n title\n status\n startDate\n endDate\n horizon\n swimlane\n sortOrder\n confidence\n riskLevel\n riskNote\n featureRequestDemand {\n requests\n votes\n }\n`;\n\nconst NNL_ITEMS = gql`\n query RoadmapNowNextLaterItems(\n $workspaceId: ID!\n $filter: PlanmagnetRoadmapItemFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetRoadmapItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n ${ITEM_FIELDS}\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\nconst MOVE_ITEM = gql`\n mutation RoadmapNowNextLaterMove($id: ID!, $input: PlanmagnetMoveRoadmapItemInput!) {\n planmagnetMoveRoadmapItem(id: $id, input: $input) {\n ${ITEM_FIELDS}\n }\n }\n`;\n\ninterface NnlItem {\n id: string;\n title: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n horizon: 'NOW' | 'NEXT' | 'LATER' | null;\n swimlane: string | null;\n sortOrder: number;\n confidence: number | null;\n riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | null;\n riskNote: string | null;\n featureRequestDemand: { requests: number; votes: number } | null;\n}\n\ntype ColumnKey = 'UNPLACED' | 'NOW' | 'NEXT' | 'LATER';\ntype Horizon = 'NOW' | 'NEXT' | 'LATER' | null;\n\nconst COLUMNS: { key: ColumnKey; label: string; horizon: Horizon }[] = [\n { key: 'UNPLACED', label: 'Not placed', horizon: null },\n { key: 'NOW', label: 'Now', horizon: 'NOW' },\n { key: 'NEXT', label: 'Next', horizon: 'NEXT' },\n { key: 'LATER', label: 'Later', horizon: 'LATER' },\n];\nconst columnOf = (horizon: Horizon): ColumnKey => horizon ?? 'UNPLACED';\n\nconst PAGE_SIZE = 200;\n/** Items beyond this many are not loaded. Stated rather than silently dropped: a banner below the board says so\n * when the cap is actually hit, the same \"never silently stop\" convention as `controls/Pickers.tsx`. */\nconst MAX_ITEMS = 2000;\n\n/** Every item on the roadmap, paged through in full (never just the first page) up to `MAX_ITEMS`, ordered by\n * sortOrder then id — same pattern as `ProjectSelect` in `controls/Pickers.tsx`, generalized to a full board\n * rather than a picker list. */\nfunction useAllRoadmapItems(workspaceId: string | undefined, roadmapId: string) {\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetRoadmapItems: {\n items: NnlItem[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(NNL_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { roadmapId },\n pagination: { first: PAGE_SIZE },\n },\n skip: !workspaceId || !roadmapId,\n });\n const loaded = data?.planmagnetRoadmapItems.items ?? [];\n const hasNextPage = data?.planmagnetRoadmapItems.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetRoadmapItems.pageInfo.endCursor ?? null;\n const totalCount = data?.planmagnetRoadmapItems.totalCount ?? 0;\n\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_ITEMS) return;\n fetchMore({\n variables: { pagination: { first: PAGE_SIZE, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetRoadmapItems: {\n ...fetchMoreResult.planmagnetRoadmapItems,\n items: [\n ...prev.planmagnetRoadmapItems.items,\n ...fetchMoreResult.planmagnetRoadmapItems.items,\n ],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the items already loaded usable; `truncated` below still reports the gap.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n\n return {\n items: loaded,\n loading: loading && !data,\n error,\n totalCount,\n truncated: hasNextPage && loaded.length >= MAX_ITEMS,\n };\n}\n\n/** An item's lane key for grouping — `null`/unset normalizes to `''` so it compares equal to itself. */\nconst laneKey = (item: NnlItem) => item.swimlane ?? '';\n/** Display label for a lane key, matching the Timeline view's own term for the unassigned bucket\n * (`ProductsRoadmapDetailPagePage.tsx`'s `swimlaneGroups`). */\nconst laneLabel = (key: string) => key || 'Uncategorized';\n\n/**\n * The 0-based index to send `planmagnetMoveRoadmapItem` so `movedItem` lands where the drop indicates.\n * `laneItems` MUST already be scoped to a single lane's section (the same list the card was dropped in/onto —\n * see the module docstring), so there is no cross-lane ambiguity left to resolve here: `beforeId` is the id of\n * the card the drop should land in front of within that section, or `null` for the end of it, which is exactly\n * the end of the (roadmap, horizon, lane) group the server indexes into.\n */\nfunction destinationIndex(\n laneItems: NnlItem[],\n movedItemId: string,\n beforeId: string | null\n): number {\n const others = laneItems.filter((it) => it.id !== movedItemId);\n if (beforeId === null) return others.length;\n const cut = others.findIndex((it) => it.id === beforeId);\n return cut === -1 ? others.length : cut;\n}\n\nconst RISK_META: Record<\n 'LOW' | 'MEDIUM' | 'HIGH',\n { Icon: typeof ShieldCheck; label: string; className: string }\n> = {\n LOW: { Icon: ShieldCheck, label: 'Low risk', className: 'text-muted-foreground' },\n MEDIUM: { Icon: AlertTriangle, label: 'Medium risk', className: 'text-status-warning-text' },\n HIGH: { Icon: ShieldAlert, label: 'High risk', className: 'text-status-error-text' },\n};\n\n/** Icon + text label (never colour alone) for a risk level; the note (if any) shows on hover/focus via a\n * tooltip, which is keyboard-reachable because the trigger is a real, focusable button. Exported for\n * `roadmaps/RoadmapPresentation.tsx` (PRM-11), which reuses this exact chip rather than re-deriving risk styling. */\nexport function RiskIndicator({\n level,\n note,\n}: {\n level: NnlItem['riskLevel'];\n note: string | null;\n}) {\n if (!level) return null;\n const meta = RISK_META[level];\n const badge = (\n <span\n data-testid=\"nnl-risk\"\n className={`inline-flex items-center gap-1 text-xs ${meta.className}`}\n >\n <meta.Icon className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {meta.label}\n </span>\n );\n if (!note) return badge;\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {badge}\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">{note}</TooltipContent>\n </Tooltip>\n </TooltipProvider>\n );\n}\n\n/** Exported for `roadmaps/RoadmapPresentation.tsx` (PRM-11) — see `RiskIndicator` above. */\nexport function ConfidenceBadge({ confidence }: { confidence: number | null }) {\n if (confidence === null) return null;\n return (\n <span\n data-testid=\"nnl-confidence\"\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n >\n <Gauge className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {confidence}% confident\n </span>\n );\n}\n\n/** The keyboard/menu equivalent of dragging: every horizon this card isn't already in, plus reorder-in-place.\n * A native `<select>` with a constant `value=\"\"` — the same \"Move…\" pattern `board/ProjectBoard.tsx` uses — so\n * it always shows the placeholder after an action instead of needing to be reset by hand. */\nfunction CardMoveMenu({\n item,\n columnKey,\n laneItems,\n disabled,\n onMoveTo,\n onShift,\n}: {\n item: NnlItem;\n columnKey: ColumnKey;\n laneItems: NnlItem[];\n disabled: boolean;\n onMoveTo: (target: ColumnKey) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const pos = laneItems.findIndex((it) => it.id === item.id);\n const canUp = pos > 0;\n const canDown = pos !== -1 && pos < laneItems.length - 1;\n return (\n <select\n id={`nnl-move-${item.id}`}\n aria-label={`Move ${item.title}`}\n data-testid=\"nnl-card-move\"\n className=\"rounded border border-input bg-background px-1 py-0.5 text-[11px] disabled:opacity-60\"\n value=\"\"\n disabled={disabled}\n onChange={(e) => {\n const v = e.target.value;\n if (v === 'up') onShift(-1);\n else if (v === 'down') onShift(1);\n // Every other <option> value below is a literal ColumnKey from COLUMNS itself, so this is safe.\n else if (v) onMoveTo(v as ColumnKey);\n }}\n >\n <option value=\"\">Move…</option>\n {COLUMNS.filter((c) => c.key !== columnKey).map((c) => (\n <option key={c.key} value={c.key}>\n Move to {c.label}\n </option>\n ))}\n <option value=\"up\" disabled={!canUp}>\n Move up\n </option>\n <option value=\"down\" disabled={!canDown}>\n Move down\n </option>\n </select>\n );\n}\n\nfunction NnlCard({\n roadmapId,\n item,\n columnKey,\n laneItems,\n draggable,\n dragging,\n dropIndicator,\n moveDisabled,\n onDragStart,\n onDragEnd,\n onDragOverCard,\n onDropOnCard,\n onMoveTo,\n onShift,\n}: {\n roadmapId: string;\n item: NnlItem;\n columnKey: ColumnKey;\n laneItems: NnlItem[];\n draggable: boolean;\n dragging: boolean;\n dropIndicator: 'before' | 'after' | null;\n moveDisabled: boolean;\n onDragStart: (id: string) => void;\n onDragEnd: () => void;\n onDragOverCard: (e: DragEvent<HTMLLIElement>) => void;\n onDropOnCard: (e: DragEvent<HTMLLIElement>) => void;\n onMoveTo: (target: ColumnKey) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const demand = item.featureRequestDemand;\n return (\n <li\n draggable={draggable}\n onDragStart={(e) => {\n e.dataTransfer.setData('text/plain', item.id);\n e.dataTransfer.effectAllowed = 'move';\n onDragStart(item.id);\n }}\n onDragEnd={onDragEnd}\n onDragOver={onDragOverCard}\n onDrop={onDropOnCard}\n data-testid=\"nnl-card\"\n className={[\n 'space-y-1.5 rounded-md border bg-background p-2 text-sm',\n draggable ? 'cursor-grab' : '',\n dragging ? 'opacity-50' : '',\n dropIndicator === 'before' ? 'border-t-2 border-t-primary' : '',\n dropIndicator === 'after' ? 'border-b-2 border-b-primary' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <p className=\"font-medium leading-tight\">{item.title}</p>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n <span>{enumLabel(item.status)}</span>\n {(item.startDate || item.endDate) && (\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" aria-hidden=\"true\" />\n {item.startDate ? new Date(item.startDate).toLocaleDateString() : '?'}\n {' - '}\n {item.endDate ? new Date(item.endDate).toLocaleDateString() : '?'}\n </span>\n )}\n </div>\n {demand && demand.requests > 0 && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"roadmap-item-demand\">\n {demand.requests} feature request{demand.requests === 1 ? '' : 's'} · {demand.votes} vote\n {demand.votes === 1 ? '' : 's'}\n </p>\n )}\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <ConfidenceBadge confidence={item.confidence} />\n <RiskIndicator level={item.riskLevel} note={item.riskNote} />\n </div>\n <div className=\"flex items-center justify-between gap-1 pt-0.5\">\n <RoadmapItemCommentsButton roadmapId={roadmapId} itemId={item.id} itemTitle={item.title} />\n <CardMoveMenu\n item={item}\n columnKey={columnKey}\n laneItems={laneItems}\n disabled={moveDisabled}\n onMoveTo={onMoveTo}\n onShift={onShift}\n />\n </div>\n </li>\n );\n}\n\n/** Drop-target state: `laneKey` pins the drop to one column's one lane section, so hovering a different lane's\n * (rendered) section in the same column never gets confused with this one — see the module docstring. */\ninterface DropTarget {\n columnKey: ColumnKey;\n laneKey: string;\n beforeId: string | null;\n}\n\nexport function RoadmapNowNextLater({ roadmapId }: { roadmapId: string }) {\n const { workspaceId } = usePlanMagnet();\n const { items, loading, error, totalCount, truncated } = useAllRoadmapItems(\n workspaceId,\n roadmapId\n );\n const [move, moveState] = useMutation(MOVE_ITEM, {\n refetchQueries: [\n 'GetRoadmapDetailPage',\n 'GetRoadmapDetail',\n 'GetRoadmapAnalytics',\n 'RoadmapNowNextLaterItems',\n ],\n awaitRefetchQueries: true,\n });\n const [actionError, setActionError] = useState<string | null>(null);\n const [forbidden, setForbidden] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n const [draggingId, setDraggingId] = useState<string | null>(null);\n const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);\n const [pendingFocusId, setPendingFocusId] = useState<string | null>(null);\n\n // Lane keys in first-appearance order across the WHOLE roadmap (not per column), so every column groups its\n // cards into the same lane sections in the same order — same convention as the Timeline view's\n // `swimlaneGroups`, generalized so it stays consistent across all four columns.\n const laneOrder = useMemo(() => {\n const seen = new Set<string>();\n const order: string[] = [];\n for (const item of items) {\n const key = laneKey(item);\n if (!seen.has(key)) {\n seen.add(key);\n order.push(key);\n }\n }\n return order;\n }, [items]);\n // Only disambiguate visually when there's something to disambiguate: a roadmap that never uses more than one\n // lane renders exactly one unlabelled section per column, identical to a flat list.\n const hasMultipleLanes = laneOrder.length > 1;\n\n const byColumn = useMemo(() => {\n const groups: Record<ColumnKey, NnlItem[]> = { UNPLACED: [], NOW: [], NEXT: [], LATER: [] };\n for (const item of items) groups[columnOf(item.horizon)].push(item);\n return groups;\n }, [items]);\n\n // Every (column, lane) pair's items, in sortOrder — the unit both rendering and move math operate on.\n const byColumnLane = useMemo(() => {\n const groups: Record<ColumnKey, Record<string, NnlItem[]>> = {\n UNPLACED: {},\n NOW: {},\n NEXT: {},\n LATER: {},\n };\n for (const key of laneOrder) {\n groups.UNPLACED[key] = [];\n groups.NOW[key] = [];\n groups.NEXT[key] = [];\n groups.LATER[key] = [];\n }\n for (const item of items) {\n const col = columnOf(item.horizon);\n const lane = laneKey(item);\n (groups[col][lane] ??= []).push(item);\n }\n return groups;\n }, [items, laneOrder]);\n\n // Focus the moved card's own \"Move…\" control once the board has re-rendered in its new position — a\n // horizon-changing move remounts the control in a different column's DOM subtree, which would otherwise drop\n // focus back to the document body with no notice to a keyboard/screen-reader user.\n useEffect(() => {\n if (!pendingFocusId) return;\n const id = pendingFocusId;\n const raf = requestAnimationFrame(() => {\n document.getElementById(`nnl-move-${id}`)?.focus();\n setPendingFocusId(null);\n });\n return () => cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items]);\n\n const busy = moveState.loading;\n const movesAllowed = !forbidden;\n const draggingItem = draggingId ? (items.find((it) => it.id === draggingId) ?? null) : null;\n\n const runMove = async (\n item: NnlItem,\n horizon: Horizon,\n index: number,\n resultLabel: string,\n focusAfter: boolean\n ) => {\n setActionError(null);\n if (focusAfter) setPendingFocusId(item.id);\n try {\n await throwOnError(move({ variables: { id: item.id, input: { horizon, index } } }));\n setAnnouncement(`Moved \"${item.title}\" to ${resultLabel}.`);\n } catch (e) {\n if (isForbidden(e)) {\n setForbidden(true);\n setActionError('Your role cannot reorder this roadmap.');\n setAnnouncement(`Move refused: your role cannot reorder this roadmap.`);\n } else {\n const msg = mutationErrorMessage(e, 'The item could not be moved.');\n setActionError(msg);\n setAnnouncement(`Move failed: ${msg}`);\n }\n }\n };\n\n /** `laneItems` is one (column, lane) section's own list — see the module docstring on why the move never\n * needs to consider any other lane's items. */\n const dropOnLane = (\n columnKey: ColumnKey,\n lane: string,\n laneItems: NnlItem[],\n draggedId: string,\n beforeId: string | null\n ) => {\n if (!movesAllowed || busy) return;\n const movedItem = items.find((it) => it.id === draggedId);\n if (!movedItem) return;\n // Defensive: the section's own onDragOver already refuses to arm a drop target for a foreign lane (and a\n // dragover that never calls preventDefault never gets a drop event at all), but a drop is rejected here too\n // in case that ever changes — the move must never silently reassign `swimlane`.\n if (laneKey(movedItem) !== lane) return;\n const column = COLUMNS.find((c) => c.key === columnKey);\n if (!column) return;\n const index = destinationIndex(laneItems, movedItem.id, beforeId);\n // No-op guard: `destinationIndex` already excludes the moved item from its own count, so \"drop just before\n // itself\" and \"drop just after itself\" both resolve to its current lane-relative index — a single equality\n // check catches both without needing to special-case direction.\n if (columnOf(movedItem.horizon) === columnKey) {\n const currentIndex = laneItems.findIndex((it) => it.id === movedItem.id);\n if (currentIndex === index) return;\n }\n void runMove(movedItem, column.horizon, index, column.label, false);\n };\n\n const laneCardDragOver =\n (columnKey: ColumnKey, lane: string, laneItems: NnlItem[]) =>\n (e: DragEvent<HTMLLIElement>, item: NnlItem, index: number) => {\n if (!movesAllowed || busy || !draggingItem || laneKey(draggingItem) !== lane) return;\n e.preventDefault();\n // Stop this from also bubbling to the lane section's own onDragOver, which would otherwise overwrite the\n // precise before/after target computed here with \"append at the end\" on every mousemove over a card.\n e.stopPropagation();\n e.dataTransfer.dropEffect = 'move';\n const rect = e.currentTarget.getBoundingClientRect();\n const before = e.clientY < rect.top + rect.height / 2;\n const beforeId = before ? item.id : (laneItems[index + 1]?.id ?? null);\n setDropTarget({ columnKey, laneKey: lane, beforeId });\n };\n\n const laneDrop =\n (columnKey: ColumnKey, lane: string, laneItems: NnlItem[]) => (e: DragEvent<HTMLElement>) => {\n e.preventDefault();\n // Stop this from also bubbling to the section's own onDrop, which would otherwise run the same drop a\n // second time with the section-level (already-cleared) target.\n e.stopPropagation();\n const draggedId = e.dataTransfer.getData('text/plain');\n const beforeId =\n dropTarget?.columnKey === columnKey && dropTarget.laneKey === lane\n ? dropTarget.beforeId\n : null;\n setDropTarget(null);\n setDraggingId(null);\n if (draggedId) dropOnLane(columnKey, lane, laneItems, draggedId, beforeId);\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center p-12\" data-testid=\"nnl-board\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-destructive\" data-testid=\"nnl-board\">\n Failed to load roadmap items: {error.message}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\" data-testid=\"nnl-board\">\n <p className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announcement}\n </p>\n {forbidden && (\n <p\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 p-2 text-sm text-destructive\"\n >\n Your role can view this roadmap but not reorder it. Moves are disabled below.\n </p>\n )}\n {!forbidden && actionError && (\n <p\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 p-2 text-sm text-destructive\"\n >\n {actionError}\n </p>\n )}\n {truncated && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_ITEMS} of {totalCount} items.\n </p>\n )}\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-4\">\n {COLUMNS.map((column) => {\n const columnItems = byColumn[column.key];\n // Every lane's own list for this column, plus whether it should render at all: a roadmap using only\n // one lane always shows its (possibly empty) single section, exactly like the old flat list; once a\n // roadmap uses more than one lane, an EMPTY lane's section only appears while something from that\n // exact lane is being dragged (so it can serve as a drop target), keeping the common, non-dragging\n // view free of empty \"Lane — no items\" clutter.\n const laneSections = laneOrder\n .map((lane) => ({ lane, laneItems: byColumnLane[column.key][lane] ?? [] }))\n .filter(\n ({ lane, laneItems }) =>\n !hasMultipleLanes ||\n laneItems.length > 0 ||\n (draggingItem && laneKey(draggingItem) === lane)\n );\n return (\n <section\n key={column.key}\n aria-label={`${column.label} column`}\n data-testid={`nnl-column-${column.key}`}\n className=\"min-h-[120px] rounded-md border bg-muted/30 p-2\"\n // The whole column accepts a drop, not just its lane lists: a card dropped on the column's empty\n // space (below a short list, or on its header) goes to the END of its own lane in this column — the\n // lane never changes on a drop (see dropOnLane). The lists and cards handle their own, more precise\n // drops and stop them here.\n onDragOver={(e) => {\n if (!movesAllowed || busy || !draggingItem) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n setDropTarget({\n columnKey: column.key,\n laneKey: laneKey(draggingItem),\n beforeId: null,\n });\n }}\n onDrop={(e) => {\n if (!draggingItem) return;\n const lane = laneKey(draggingItem);\n laneDrop(column.key, lane, byColumnLane[column.key][lane] ?? [])(e);\n }}\n >\n <h3 className=\"mb-2 flex items-center gap-1.5 text-sm font-medium\">\n {column.key === 'UNPLACED' && (\n <Inbox className=\"h-3.5 w-3.5 text-muted-foreground\" aria-hidden=\"true\" />\n )}\n {column.label}\n <span className=\"text-xs text-muted-foreground\">({columnItems.length})</span>\n </h3>\n {laneSections.length === 0 && (\n <p className=\"py-4 text-center text-xs text-muted-foreground\">No items</p>\n )}\n <div className=\"space-y-3\">\n {laneSections.map(({ lane, laneItems }) => {\n const isLaneDropTarget =\n dropTarget?.columnKey === column.key && dropTarget.laneKey === lane;\n return (\n <div key={lane || '(none)'} className=\"space-y-1\">\n {hasMultipleLanes && (\n <p\n data-testid=\"nnl-lane-label\"\n className=\"text-[11px] font-medium text-muted-foreground\"\n >\n {laneLabel(lane)}{' '}\n <span className=\"text-muted-foreground/70\">({laneItems.length})</span>\n </p>\n )}\n <ul\n className={`min-h-[48px] space-y-2 rounded-sm p-1 ${\n isLaneDropTarget && !dropTarget.beforeId ? 'ring-2 ring-primary' : ''\n }`}\n onDragOver={(e) => {\n if (\n !movesAllowed ||\n busy ||\n !draggingItem ||\n laneKey(draggingItem) !== lane\n )\n return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n setDropTarget({ columnKey: column.key, laneKey: lane, beforeId: null });\n }}\n onDrop={laneDrop(column.key, lane, laneItems)}\n >\n {laneItems.length === 0 && (\n <li\n className=\"py-2 text-center text-[11px] text-muted-foreground\"\n aria-hidden=\"true\"\n >\n Drop here\n </li>\n )}\n {laneItems.map((item, index) => {\n // 'before': the drop would land this card right in front of `item`. 'after': the drop\n // targets \"end of this lane's section\" (beforeId null) and `item` is that section's\n // last card, so the line belongs below it.\n const dropIndicator: 'before' | 'after' | null = !isLaneDropTarget\n ? null\n : dropTarget.beforeId === item.id\n ? 'before'\n : dropTarget.beforeId === null && index === laneItems.length - 1\n ? 'after'\n : null;\n return (\n <NnlCard\n key={item.id}\n roadmapId={roadmapId}\n item={item}\n columnKey={column.key}\n laneItems={laneItems}\n draggable={movesAllowed && !busy}\n dragging={draggingId === item.id}\n dropIndicator={dropIndicator}\n moveDisabled={!movesAllowed || busy}\n onDragStart={setDraggingId}\n onDragEnd={() => {\n setDraggingId(null);\n setDropTarget(null);\n }}\n onDragOverCard={(e) =>\n laneCardDragOver(column.key, lane, laneItems)(e, item, index)\n }\n onDropOnCard={laneDrop(column.key, lane, laneItems)}\n onMoveTo={(target) => {\n const targetColumn = COLUMNS.find((c) => c.key === target);\n if (!targetColumn) return;\n const targetLaneItems = byColumnLane[target][laneKey(item)] ?? [];\n const index2 = destinationIndex(targetLaneItems, item.id, null);\n void runMove(\n item,\n targetColumn.horizon,\n index2,\n targetColumn.label,\n true\n );\n }}\n onShift={(delta) => {\n const pos = laneItems.findIndex((it) => it.id === item.id);\n const target = pos + delta;\n if (pos === -1 || target < 0 || target >= laneItems.length) return;\n void runMove(item, item.horizon, target, column.label, true);\n }}\n />\n );\n })}\n </ul>\n </div>\n );\n })}\n </div>\n </section>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RoadmapNowNextLater.js","names":[],"sources":["../../../src/planmagnet/roadmaps/RoadmapNowNextLater.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type DragEvent } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n Calendar,\n Gauge,\n Inbox,\n Loader2,\n ShieldAlert,\n ShieldCheck,\n} from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { RoadmapItemCommentsButton } from '../comments/RoadmapComments';\nimport { useLiveRefresh } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\n// PRM-11: this board's own query — re-read on any roadmap- or item-level event. Listed alongside\n// `ProductsRoadmapDetailPagePage`'s `GetRoadmapDetailPage` and `ProductsRoadmapAnalyticsPagePage`'s\n// `GetRoadmapAnalytics` in each of THEIR OWN `useLiveRefresh` calls — this board is its own self-contained\n// subscriber (own query, own stream), not fed through the detail page's.\nconst NNL_LIVE_QUERIES = ['RoadmapNowNextLaterItems'];\n\n/**\n * Now/Next/Later (PRM-06, PRM-09, PRM-10): a VIEW of a roadmap's items grouped by `horizon`, alongside the\n * existing Timeline view (`ProductsRoadmapDetailPagePage.tsx`) — not a separate roadmap type. Placement is\n * changed by dragging a card, or by the keyboard \"Move…\" select every card also carries (the same mutation\n * either way: `planmagnetMoveRoadmapItem`), never optimistically: the list shown is always what\n * `planmagnetRoadmapItems` last returned, so a refused move has nothing local to \"undo\" — the card simply never\n * left its column.\n *\n * `planmagnetMoveRoadmapItem`'s `index` is scoped to the (roadmap, horizon, **swimlane**) group, not to the\n * column as a whole — the same lane-scoping the Timeline view treats as first-class (`swimlaneGroups` there).\n * Each column therefore renders its cards **grouped by lane** (one sub-list per lane, in the same order in\n * every column), exactly like the Timeline view's per-lane cards. A drag/drop within a lane's sub-list, or a\n * \"Move…\" menu action, only ever computes an index against that same sub-list — so \"the end of this section\"\n * (what the user sees) and \"the end of this lane's server-side group\" (what the index means) are always the\n * same place, with no cross-lane ambiguity. Dropping a card onto a *different* lane's section is rejected (the\n * move never changes `swimlane`); the lane header is only shown once a roadmap actually uses more than one\n * lane, so a single-lane (the common case) or lane-less roadmap renders exactly one unlabelled section per\n * column — visually identical to a flat list.\n */\n\nconst ITEM_FIELDS = `\n id\n title\n status\n startDate\n endDate\n horizon\n swimlane\n sortOrder\n confidence\n riskLevel\n riskNote\n featureRequestDemand {\n requests\n votes\n }\n`;\n\nconst NNL_ITEMS = gql`\n query RoadmapNowNextLaterItems(\n $workspaceId: ID!\n $filter: PlanmagnetRoadmapItemFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetRoadmapItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n ${ITEM_FIELDS}\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\nconst MOVE_ITEM = gql`\n mutation RoadmapNowNextLaterMove($id: ID!, $input: PlanmagnetMoveRoadmapItemInput!) {\n planmagnetMoveRoadmapItem(id: $id, input: $input) {\n ${ITEM_FIELDS}\n }\n }\n`;\n\ninterface NnlItem {\n id: string;\n title: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n horizon: 'NOW' | 'NEXT' | 'LATER' | null;\n swimlane: string | null;\n sortOrder: number;\n confidence: number | null;\n riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | null;\n riskNote: string | null;\n featureRequestDemand: { requests: number; votes: number } | null;\n}\n\ntype ColumnKey = 'UNPLACED' | 'NOW' | 'NEXT' | 'LATER';\ntype Horizon = 'NOW' | 'NEXT' | 'LATER' | null;\n\nconst COLUMNS: { key: ColumnKey; label: string; horizon: Horizon }[] = [\n { key: 'UNPLACED', label: 'Not placed', horizon: null },\n { key: 'NOW', label: 'Now', horizon: 'NOW' },\n { key: 'NEXT', label: 'Next', horizon: 'NEXT' },\n { key: 'LATER', label: 'Later', horizon: 'LATER' },\n];\nconst columnOf = (horizon: Horizon): ColumnKey => horizon ?? 'UNPLACED';\n\nconst PAGE_SIZE = 200;\n/** Items beyond this many are not loaded. Stated rather than silently dropped: a banner below the board says so\n * when the cap is actually hit, the same \"never silently stop\" convention as `controls/Pickers.tsx`. */\nconst MAX_ITEMS = 2000;\n\n/** Every item on the roadmap, paged through in full (never just the first page) up to `MAX_ITEMS`, ordered by\n * sortOrder then id — same pattern as `ProjectSelect` in `controls/Pickers.tsx`, generalized to a full board\n * rather than a picker list. */\nfunction useAllRoadmapItems(workspaceId: string | undefined, roadmapId: string) {\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetRoadmapItems: {\n items: NnlItem[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(NNL_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { roadmapId },\n pagination: { first: PAGE_SIZE },\n },\n skip: !workspaceId || !roadmapId,\n });\n const loaded = data?.planmagnetRoadmapItems.items ?? [];\n const hasNextPage = data?.planmagnetRoadmapItems.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetRoadmapItems.pageInfo.endCursor ?? null;\n const totalCount = data?.planmagnetRoadmapItems.totalCount ?? 0;\n\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_ITEMS) return;\n fetchMore({\n variables: { pagination: { first: PAGE_SIZE, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetRoadmapItems: {\n ...fetchMoreResult.planmagnetRoadmapItems,\n items: [\n ...prev.planmagnetRoadmapItems.items,\n ...fetchMoreResult.planmagnetRoadmapItems.items,\n ],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the items already loaded usable; `truncated` below still reports the gap.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n\n // A bare refetch of this same query — MOVE_ITEM's own `refetchQueries` after every drop, and now also\n // `useLiveRefresh`'s event/poll-driven refetch on this same roadmap — re-issues the network request under\n // this hook's ORIGINAL (page-1) variables. That plain response overwrites whatever the `fetchMore` chain\n // above had already built up (there is no Apollo cache field policy merging pages back together for this\n // field), and the effect above re-climbs the pages correctly, but only after a couple of round trips.\n // Rendering `loaded` directly for that window would flash any roadmap with more than `PAGE_SIZE` items down\n // to its first page of cards on every refetch, then back up a moment later. `committed` instead only ever\n // replaces what this hook returns once a pagination pass has FULLY caught up (no more pages, or `MAX_ITEMS`\n // reached) — until then, the last fully-loaded snapshot keeps being returned. The very first load is\n // unaffected: nothing has been committed yet, so it falls through to `loaded` and grows page by page exactly\n // as before (no regression to the progressive initial reveal).\n //\n // Updated directly during render (the React-documented \"adjust state when a prop/value changes\" pattern —\n // same technique `useLiveRefresh.ts` uses for `mode`/`modeKey`), not inside a `useEffect`: each branch's own\n // guard goes false the instant it has acted, so this never cascades into an extra render loop, and there is\n // no one-render-late flash of stale data the way committing from an effect would add.\n const settled = !!data && (!hasNextPage || loaded.length >= MAX_ITEMS);\n const resetKey = `${workspaceId ?? ''}:${roadmapId}`;\n const [committed, setCommitted] = useState<{\n key: string;\n items: NnlItem[];\n totalCount: number;\n truncated: boolean;\n } | null>(null);\n\n if (committed && committed.key !== resetKey) {\n // A different roadmap (or workspace) invalidates whatever was committed for the last one — defends against\n // this hook ever showing a stale roadmap's cards if a future caller reuses the component instance across a\n // `roadmapId` prop change instead of remounting it.\n setCommitted(null);\n } else if (settled && committed?.items !== loaded) {\n setCommitted({\n key: resetKey,\n items: loaded,\n totalCount,\n truncated: hasNextPage && loaded.length >= MAX_ITEMS,\n });\n }\n\n return {\n items: committed && committed.key === resetKey ? committed.items : loaded,\n loading: loading && !data,\n error,\n totalCount: committed && committed.key === resetKey ? committed.totalCount : totalCount,\n truncated: committed && committed.key === resetKey ? committed.truncated : false,\n };\n}\n\n/** An item's lane key for grouping — `null`/unset normalizes to `''` so it compares equal to itself. */\nconst laneKey = (item: NnlItem) => item.swimlane ?? '';\n/** Display label for a lane key, matching the Timeline view's own term for the unassigned bucket\n * (`ProductsRoadmapDetailPagePage.tsx`'s `swimlaneGroups`). */\nconst laneLabel = (key: string) => key || 'Uncategorized';\n\n/**\n * The 0-based index to send `planmagnetMoveRoadmapItem` so `movedItem` lands where the drop indicates.\n * `laneItems` MUST already be scoped to a single lane's section (the same list the card was dropped in/onto —\n * see the module docstring), so there is no cross-lane ambiguity left to resolve here: `beforeId` is the id of\n * the card the drop should land in front of within that section, or `null` for the end of it, which is exactly\n * the end of the (roadmap, horizon, lane) group the server indexes into.\n */\nfunction destinationIndex(\n laneItems: NnlItem[],\n movedItemId: string,\n beforeId: string | null\n): number {\n const others = laneItems.filter((it) => it.id !== movedItemId);\n if (beforeId === null) return others.length;\n const cut = others.findIndex((it) => it.id === beforeId);\n return cut === -1 ? others.length : cut;\n}\n\nconst RISK_META: Record<\n 'LOW' | 'MEDIUM' | 'HIGH',\n { Icon: typeof ShieldCheck; label: string; className: string }\n> = {\n LOW: { Icon: ShieldCheck, label: 'Low risk', className: 'text-muted-foreground' },\n MEDIUM: { Icon: AlertTriangle, label: 'Medium risk', className: 'text-status-warning-text' },\n HIGH: { Icon: ShieldAlert, label: 'High risk', className: 'text-status-error-text' },\n};\n\n/** Icon + text label (never colour alone) for a risk level; the note (if any) shows on hover/focus via a\n * tooltip, which is keyboard-reachable because the trigger is a real, focusable button. Exported for\n * `roadmaps/RoadmapPresentation.tsx` (PRM-11), which reuses this exact chip rather than re-deriving risk styling. */\nexport function RiskIndicator({\n level,\n note,\n}: {\n level: NnlItem['riskLevel'];\n note: string | null;\n}) {\n if (!level) return null;\n const meta = RISK_META[level];\n const badge = (\n <span\n data-testid=\"nnl-risk\"\n className={`inline-flex items-center gap-1 text-xs ${meta.className}`}\n >\n <meta.Icon className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {meta.label}\n </span>\n );\n if (!note) return badge;\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {badge}\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">{note}</TooltipContent>\n </Tooltip>\n </TooltipProvider>\n );\n}\n\n/** Exported for `roadmaps/RoadmapPresentation.tsx` (PRM-11) — see `RiskIndicator` above. */\nexport function ConfidenceBadge({ confidence }: { confidence: number | null }) {\n if (confidence === null) return null;\n return (\n <span\n data-testid=\"nnl-confidence\"\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n >\n <Gauge className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {confidence}% confident\n </span>\n );\n}\n\n/** The keyboard/menu equivalent of dragging: every horizon this card isn't already in, plus reorder-in-place.\n * A native `<select>` with a constant `value=\"\"` — the same \"Move…\" pattern `board/ProjectBoard.tsx` uses — so\n * it always shows the placeholder after an action instead of needing to be reset by hand. */\nfunction CardMoveMenu({\n item,\n columnKey,\n laneItems,\n disabled,\n onMoveTo,\n onShift,\n}: {\n item: NnlItem;\n columnKey: ColumnKey;\n laneItems: NnlItem[];\n disabled: boolean;\n onMoveTo: (target: ColumnKey) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const pos = laneItems.findIndex((it) => it.id === item.id);\n const canUp = pos > 0;\n const canDown = pos !== -1 && pos < laneItems.length - 1;\n return (\n <select\n id={`nnl-move-${item.id}`}\n aria-label={`Move ${item.title}`}\n data-testid=\"nnl-card-move\"\n className=\"rounded border border-input bg-background px-1 py-0.5 text-[11px] disabled:opacity-60\"\n value=\"\"\n disabled={disabled}\n onChange={(e) => {\n const v = e.target.value;\n if (v === 'up') onShift(-1);\n else if (v === 'down') onShift(1);\n // Every other <option> value below is a literal ColumnKey from COLUMNS itself, so this is safe.\n else if (v) onMoveTo(v as ColumnKey);\n }}\n >\n <option value=\"\">Move…</option>\n {COLUMNS.filter((c) => c.key !== columnKey).map((c) => (\n <option key={c.key} value={c.key}>\n Move to {c.label}\n </option>\n ))}\n <option value=\"up\" disabled={!canUp}>\n Move up\n </option>\n <option value=\"down\" disabled={!canDown}>\n Move down\n </option>\n </select>\n );\n}\n\nfunction NnlCard({\n roadmapId,\n item,\n columnKey,\n laneItems,\n draggable,\n dragging,\n dropIndicator,\n moveDisabled,\n onDragStart,\n onDragEnd,\n onDragOverCard,\n onDropOnCard,\n onMoveTo,\n onShift,\n}: {\n roadmapId: string;\n item: NnlItem;\n columnKey: ColumnKey;\n laneItems: NnlItem[];\n draggable: boolean;\n dragging: boolean;\n dropIndicator: 'before' | 'after' | null;\n moveDisabled: boolean;\n onDragStart: (id: string) => void;\n onDragEnd: () => void;\n onDragOverCard: (e: DragEvent<HTMLLIElement>) => void;\n onDropOnCard: (e: DragEvent<HTMLLIElement>) => void;\n onMoveTo: (target: ColumnKey) => void;\n onShift: (delta: -1 | 1) => void;\n}) {\n const demand = item.featureRequestDemand;\n return (\n <li\n draggable={draggable}\n onDragStart={(e) => {\n e.dataTransfer.setData('text/plain', item.id);\n e.dataTransfer.effectAllowed = 'move';\n onDragStart(item.id);\n }}\n onDragEnd={onDragEnd}\n onDragOver={onDragOverCard}\n onDrop={onDropOnCard}\n data-testid=\"nnl-card\"\n className={[\n 'space-y-1.5 rounded-md border bg-background p-2 text-sm',\n draggable ? 'cursor-grab' : '',\n dragging ? 'opacity-50' : '',\n dropIndicator === 'before' ? 'border-t-2 border-t-primary' : '',\n dropIndicator === 'after' ? 'border-b-2 border-b-primary' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <p className=\"font-medium leading-tight\">{item.title}</p>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n <span>{enumLabel(item.status)}</span>\n {(item.startDate || item.endDate) && (\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" aria-hidden=\"true\" />\n {item.startDate ? new Date(item.startDate).toLocaleDateString() : '?'}\n {' - '}\n {item.endDate ? new Date(item.endDate).toLocaleDateString() : '?'}\n </span>\n )}\n </div>\n {demand && demand.requests > 0 && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"roadmap-item-demand\">\n {demand.requests} feature request{demand.requests === 1 ? '' : 's'} · {demand.votes} vote\n {demand.votes === 1 ? '' : 's'}\n </p>\n )}\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <ConfidenceBadge confidence={item.confidence} />\n <RiskIndicator level={item.riskLevel} note={item.riskNote} />\n </div>\n <div className=\"flex items-center justify-between gap-1 pt-0.5\">\n <RoadmapItemCommentsButton roadmapId={roadmapId} itemId={item.id} itemTitle={item.title} />\n <CardMoveMenu\n item={item}\n columnKey={columnKey}\n laneItems={laneItems}\n disabled={moveDisabled}\n onMoveTo={onMoveTo}\n onShift={onShift}\n />\n </div>\n </li>\n );\n}\n\n/** Drop-target state: `laneKey` pins the drop to one column's one lane section, so hovering a different lane's\n * (rendered) section in the same column never gets confused with this one — see the module docstring. */\ninterface DropTarget {\n columnKey: ColumnKey;\n laneKey: string;\n beforeId: string | null;\n}\n\nexport function RoadmapNowNextLater({ roadmapId }: { roadmapId: string }) {\n const { workspaceId } = usePlanMagnet();\n const { items, loading, error, totalCount, truncated } = useAllRoadmapItems(\n workspaceId,\n roadmapId\n );\n const [move, moveState] = useMutation(MOVE_ITEM, {\n refetchQueries: [\n 'GetRoadmapDetailPage',\n 'GetRoadmapDetail',\n 'GetRoadmapAnalytics',\n 'RoadmapNowNextLaterItems',\n ],\n awaitRefetchQueries: true,\n });\n const [actionError, setActionError] = useState<string | null>(null);\n const [forbidden, setForbidden] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n const [draggingId, setDraggingId] = useState<string | null>(null);\n const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);\n const [pendingFocusId, setPendingFocusId] = useState<string | null>(null);\n\n // PRM-11: `paused` while `draggingId` is set — a card is mid-drag (mouse down, not yet dropped). A live\n // refetch landing in that window would replace this list (and the native HTML5 drag source sitting in it) out\n // from under the gesture; `useLiveRefresh` defers it until `draggingId` goes back to null (the drop, in\n // `laneDrop`/`onDragEnd`, always clears it), then runs the deferred refetch right away. The move mutation's\n // OWN `refetchQueries` above already re-reads this same query right after a successful drop — this only\n // covers an event from somewhere ELSE (another collaborator) landing while this user is mid-drag.\n const live = useLiveRefresh({\n kind: 'roadmap',\n roadmapId,\n queries: NNL_LIVE_QUERIES,\n paused: draggingId !== null,\n });\n\n // Lane keys in first-appearance order across the WHOLE roadmap (not per column), so every column groups its\n // cards into the same lane sections in the same order — same convention as the Timeline view's\n // `swimlaneGroups`, generalized so it stays consistent across all four columns.\n const laneOrder = useMemo(() => {\n const seen = new Set<string>();\n const order: string[] = [];\n for (const item of items) {\n const key = laneKey(item);\n if (!seen.has(key)) {\n seen.add(key);\n order.push(key);\n }\n }\n return order;\n }, [items]);\n // Only disambiguate visually when there's something to disambiguate: a roadmap that never uses more than one\n // lane renders exactly one unlabelled section per column, identical to a flat list.\n const hasMultipleLanes = laneOrder.length > 1;\n\n const byColumn = useMemo(() => {\n const groups: Record<ColumnKey, NnlItem[]> = { UNPLACED: [], NOW: [], NEXT: [], LATER: [] };\n for (const item of items) groups[columnOf(item.horizon)].push(item);\n return groups;\n }, [items]);\n\n // Every (column, lane) pair's items, in sortOrder — the unit both rendering and move math operate on.\n const byColumnLane = useMemo(() => {\n const groups: Record<ColumnKey, Record<string, NnlItem[]>> = {\n UNPLACED: {},\n NOW: {},\n NEXT: {},\n LATER: {},\n };\n for (const key of laneOrder) {\n groups.UNPLACED[key] = [];\n groups.NOW[key] = [];\n groups.NEXT[key] = [];\n groups.LATER[key] = [];\n }\n for (const item of items) {\n const col = columnOf(item.horizon);\n const lane = laneKey(item);\n (groups[col][lane] ??= []).push(item);\n }\n return groups;\n }, [items, laneOrder]);\n\n // Focus the moved card's own \"Move…\" control once the board has re-rendered in its new position — a\n // horizon-changing move remounts the control in a different column's DOM subtree, which would otherwise drop\n // focus back to the document body with no notice to a keyboard/screen-reader user.\n useEffect(() => {\n if (!pendingFocusId) return;\n const id = pendingFocusId;\n const raf = requestAnimationFrame(() => {\n document.getElementById(`nnl-move-${id}`)?.focus();\n setPendingFocusId(null);\n });\n return () => cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items]);\n\n const busy = moveState.loading;\n const movesAllowed = !forbidden;\n const draggingItem = draggingId ? (items.find((it) => it.id === draggingId) ?? null) : null;\n\n const runMove = async (\n item: NnlItem,\n horizon: Horizon,\n index: number,\n resultLabel: string,\n focusAfter: boolean\n ) => {\n setActionError(null);\n if (focusAfter) setPendingFocusId(item.id);\n try {\n await throwOnError(move({ variables: { id: item.id, input: { horizon, index } } }));\n setAnnouncement(`Moved \"${item.title}\" to ${resultLabel}.`);\n } catch (e) {\n if (isForbidden(e)) {\n setForbidden(true);\n setActionError('Your role cannot reorder this roadmap.');\n setAnnouncement(`Move refused: your role cannot reorder this roadmap.`);\n } else {\n const msg = mutationErrorMessage(e, 'The item could not be moved.');\n setActionError(msg);\n setAnnouncement(`Move failed: ${msg}`);\n }\n }\n };\n\n /** `laneItems` is one (column, lane) section's own list — see the module docstring on why the move never\n * needs to consider any other lane's items. */\n const dropOnLane = (\n columnKey: ColumnKey,\n lane: string,\n laneItems: NnlItem[],\n draggedId: string,\n beforeId: string | null\n ) => {\n if (!movesAllowed || busy) return;\n const movedItem = items.find((it) => it.id === draggedId);\n if (!movedItem) return;\n // Defensive: the section's own onDragOver already refuses to arm a drop target for a foreign lane (and a\n // dragover that never calls preventDefault never gets a drop event at all), but a drop is rejected here too\n // in case that ever changes — the move must never silently reassign `swimlane`.\n if (laneKey(movedItem) !== lane) return;\n const column = COLUMNS.find((c) => c.key === columnKey);\n if (!column) return;\n const index = destinationIndex(laneItems, movedItem.id, beforeId);\n // No-op guard: `destinationIndex` already excludes the moved item from its own count, so \"drop just before\n // itself\" and \"drop just after itself\" both resolve to its current lane-relative index — a single equality\n // check catches both without needing to special-case direction.\n if (columnOf(movedItem.horizon) === columnKey) {\n const currentIndex = laneItems.findIndex((it) => it.id === movedItem.id);\n if (currentIndex === index) return;\n }\n void runMove(movedItem, column.horizon, index, column.label, false);\n };\n\n const laneCardDragOver =\n (columnKey: ColumnKey, lane: string, laneItems: NnlItem[]) =>\n (e: DragEvent<HTMLLIElement>, item: NnlItem, index: number) => {\n if (!movesAllowed || busy || !draggingItem || laneKey(draggingItem) !== lane) return;\n e.preventDefault();\n // Stop this from also bubbling to the lane section's own onDragOver, which would otherwise overwrite the\n // precise before/after target computed here with \"append at the end\" on every mousemove over a card.\n e.stopPropagation();\n e.dataTransfer.dropEffect = 'move';\n const rect = e.currentTarget.getBoundingClientRect();\n const before = e.clientY < rect.top + rect.height / 2;\n const beforeId = before ? item.id : (laneItems[index + 1]?.id ?? null);\n setDropTarget({ columnKey, laneKey: lane, beforeId });\n };\n\n const laneDrop =\n (columnKey: ColumnKey, lane: string, laneItems: NnlItem[]) => (e: DragEvent<HTMLElement>) => {\n e.preventDefault();\n // Stop this from also bubbling to the section's own onDrop, which would otherwise run the same drop a\n // second time with the section-level (already-cleared) target.\n e.stopPropagation();\n const draggedId = e.dataTransfer.getData('text/plain');\n const beforeId =\n dropTarget?.columnKey === columnKey && dropTarget.laneKey === lane\n ? dropTarget.beforeId\n : null;\n setDropTarget(null);\n setDraggingId(null);\n if (draggedId) dropOnLane(columnKey, lane, laneItems, draggedId, beforeId);\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center p-12\" data-testid=\"nnl-board\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-destructive\" data-testid=\"nnl-board\">\n Failed to load roadmap items: {error.message}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\" data-testid=\"nnl-board\">\n <p className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announcement}\n </p>\n <div className=\"flex justify-end\">\n <LiveStatus mode={live.mode} />\n </div>\n {forbidden && (\n <p\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 p-2 text-sm text-destructive\"\n >\n Your role can view this roadmap but not reorder it. Moves are disabled below.\n </p>\n )}\n {!forbidden && actionError && (\n <p\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 p-2 text-sm text-destructive\"\n >\n {actionError}\n </p>\n )}\n {truncated && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_ITEMS} of {totalCount} items.\n </p>\n )}\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-4\">\n {COLUMNS.map((column) => {\n const columnItems = byColumn[column.key];\n // Every lane's own list for this column, plus whether it should render at all: a roadmap using only\n // one lane always shows its (possibly empty) single section, exactly like the old flat list; once a\n // roadmap uses more than one lane, an EMPTY lane's section only appears while something from that\n // exact lane is being dragged (so it can serve as a drop target), keeping the common, non-dragging\n // view free of empty \"Lane — no items\" clutter.\n const laneSections = laneOrder\n .map((lane) => ({ lane, laneItems: byColumnLane[column.key][lane] ?? [] }))\n .filter(\n ({ lane, laneItems }) =>\n !hasMultipleLanes ||\n laneItems.length > 0 ||\n (draggingItem && laneKey(draggingItem) === lane)\n );\n return (\n <section\n key={column.key}\n aria-label={`${column.label} column`}\n data-testid={`nnl-column-${column.key}`}\n className=\"min-h-[120px] rounded-md border bg-muted/30 p-2\"\n // The whole column accepts a drop, not just its lane lists: a card dropped on the column's empty\n // space (below a short list, or on its header) goes to the END of its own lane in this column — the\n // lane never changes on a drop (see dropOnLane). The lists and cards handle their own, more precise\n // drops and stop them here.\n onDragOver={(e) => {\n if (!movesAllowed || busy || !draggingItem) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n setDropTarget({\n columnKey: column.key,\n laneKey: laneKey(draggingItem),\n beforeId: null,\n });\n }}\n onDrop={(e) => {\n if (!draggingItem) return;\n const lane = laneKey(draggingItem);\n laneDrop(column.key, lane, byColumnLane[column.key][lane] ?? [])(e);\n }}\n >\n <h3 className=\"mb-2 flex items-center gap-1.5 text-sm font-medium\">\n {column.key === 'UNPLACED' && (\n <Inbox className=\"h-3.5 w-3.5 text-muted-foreground\" aria-hidden=\"true\" />\n )}\n {column.label}\n <span className=\"text-xs text-muted-foreground\">({columnItems.length})</span>\n </h3>\n {laneSections.length === 0 && (\n <p className=\"py-4 text-center text-xs text-muted-foreground\">No items</p>\n )}\n <div className=\"space-y-3\">\n {laneSections.map(({ lane, laneItems }) => {\n const isLaneDropTarget =\n dropTarget?.columnKey === column.key && dropTarget.laneKey === lane;\n return (\n <div key={lane || '(none)'} className=\"space-y-1\">\n {hasMultipleLanes && (\n <p\n data-testid=\"nnl-lane-label\"\n className=\"text-[11px] font-medium text-muted-foreground\"\n >\n {laneLabel(lane)}{' '}\n <span className=\"text-muted-foreground/70\">({laneItems.length})</span>\n </p>\n )}\n <ul\n className={`min-h-[48px] space-y-2 rounded-sm p-1 ${\n isLaneDropTarget && !dropTarget.beforeId ? 'ring-2 ring-primary' : ''\n }`}\n onDragOver={(e) => {\n if (\n !movesAllowed ||\n busy ||\n !draggingItem ||\n laneKey(draggingItem) !== lane\n )\n return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n setDropTarget({ columnKey: column.key, laneKey: lane, beforeId: null });\n }}\n onDrop={laneDrop(column.key, lane, laneItems)}\n >\n {laneItems.length === 0 && (\n <li\n className=\"py-2 text-center text-[11px] text-muted-foreground\"\n aria-hidden=\"true\"\n >\n Drop here\n </li>\n )}\n {laneItems.map((item, index) => {\n // 'before': the drop would land this card right in front of `item`. 'after': the drop\n // targets \"end of this lane's section\" (beforeId null) and `item` is that section's\n // last card, so the line belongs below it.\n const dropIndicator: 'before' | 'after' | null = !isLaneDropTarget\n ? null\n : dropTarget.beforeId === item.id\n ? 'before'\n : dropTarget.beforeId === null && index === laneItems.length - 1\n ? 'after'\n : null;\n return (\n <NnlCard\n key={item.id}\n roadmapId={roadmapId}\n item={item}\n columnKey={column.key}\n laneItems={laneItems}\n draggable={movesAllowed && !busy}\n dragging={draggingId === item.id}\n dropIndicator={dropIndicator}\n moveDisabled={!movesAllowed || busy}\n onDragStart={setDraggingId}\n onDragEnd={() => {\n setDraggingId(null);\n setDropTarget(null);\n }}\n onDragOverCard={(e) =>\n laneCardDragOver(column.key, lane, laneItems)(e, item, index)\n }\n onDropOnCard={laneDrop(column.key, lane, laneItems)}\n onMoveTo={(target) => {\n const targetColumn = COLUMNS.find((c) => c.key === target);\n if (!targetColumn) return;\n const targetLaneItems = byColumnLane[target][laneKey(item)] ?? [];\n const index2 = destinationIndex(targetLaneItems, item.id, null);\n void runMove(\n item,\n targetColumn.horizon,\n index2,\n targetColumn.label,\n true\n );\n }}\n onShift={(delta) => {\n const pos = laneItems.findIndex((it) => it.id === item.id);\n const target = pos + delta;\n if (pos === -1 || target < 0 || target >= laneItems.length) return;\n void runMove(item, item.horizon, target, column.label, true);\n }}\n />\n );\n })}\n </ul>\n </div>\n );\n })}\n </div>\n </section>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|