docs: document bulk tech tree position writes
This commit is contained in:
parent
80ad24580a
commit
98cc860299
1 changed files with 2 additions and 2 deletions
|
|
@ -312,9 +312,9 @@ Leaflet world map at `/map` (top-level sidebar entry, whole unit views it). Full
|
|||
- **View wiring**: `payload.config.ts` `admin.components.views.techTree` (path `/tech-tree`, component `src/components/admin/tech-tree/TechTreeView.tsx`) and `admin.components.Nav` → `TechTreeNav.tsx`. Custom root views are public by default, so the view gates itself (logged-in + `technologies:read` OR Intelligence qualification). The nav replica injects the entry into the group containing `technologies` and reuses public exports (`NavWrapper`/`NavHamburger`/`DefaultNavClient` from `@payloadcms/next/client`, `groupNavItems` from `@payloadcms/ui/shared`); registered admin components must be **default exports** (importMap convention in this repo).
|
||||
- **Layout** (`layout.ts`, pure): a **propagating left-to-right tree**. A technology WITH prerequisites is placed one column to the right of its parents, at their average row, so linking A -> B drops B immediately beside A and B's dependents continue further right; nothing is teleported to a distant column when its depth changes. Technologies in a category stay pinned to that category's column (the category contract); uncategorized roots fill a catalog grid whose slots come from each tech's rank among ALL uncategorized technologies (`MAX_ROWS_PER_COLUMN` = 10 rows, spilling into more columns) so promoting a tech into the tree leaves an empty slot rather than re-shuffling the grid; uncategorized dependents follow their parents inside the uncategorized zone. Collisions search rows outward from the desired row (dependents may also grow upward, and their columns are not height-capped, so a link from a full column still lands beside its parent rather than far right). Category bands are the bounding box of their members' final positions. Prerequisites placed to the RIGHT of their dependent surface as amber warnings (toolbar chip + node icon + preview panel section).
|
||||
- **Edges**: AND prerequisites solid slate, OR prerequisites dashed cyan with an OR label. Connections are editable on the canvas: drag from a node's right handle onto another node (the target card OR its handle) to add it as an AND prerequisite; the drop-on-card case is resolved by an `onConnectEnd` fallback because React Flow only resolves drops onto a handle. Select an edge and press Backspace to remove the prerequisite. Duplicates, self-references, and cycles (direct or transitive) are rejected by the pure `prereqEdits.ts` helpers with an inline error in the toolbar. Lines are also manually routable: selecting an edge reveals a "+" handle at each segment's midpoint (click to add a bend), bends drag to move, and double-clicking a bend removes it (`RoutedEdge.tsx` + `edgeRoutes.ts`). Routes are stored per browser in `localStorage`; making them shared would need a persisted field on the target technology. Edges also route around technologies they are not connected to: `edgeRouting.ts` detours each segment around the node rectangles it would cross (inflated by a clearance margin) and `buildRoutePath` rounds every corner. An unrouted edge keeps its automatic out/across/in shape unless that path would cut through a node, in which case it falls back to a direct line the router can bend around.
|
||||
- **Interactions**: nodes are draggable; dropping one PATCHes its `treePosition` (persisted, survives reload) and the toolbar shows an "Auto-arrange" button (technologies:update gated) that clears every manual position and restores the ordered layout. A **Save positions** button (technologies:update gated), next to Auto-arrange, commits the current on-screen position of every node to `treePosition` in one pass (matching what React Flow rendered, not just dragged/pinned cards) so the exact arrangement survives a reload; Auto-arrange then clears them. Dragging one card of a multi-selection moves and persists the whole selection (React Flow hands the full dragged set to `onNodeDragStop`), and a category band is draggable by its header (`dragHandle`), which moves every technology in that band. A **Snap: On/Off** toggle (default on, remembered per browser, 46px grid that divides `COLUMN_STEP`/`ROW_STEP`) snaps dragged cards to the grid. **Connecting does not move anything**: if the ordered layout would relocate the connected technology, the canvas pins it to its current spot (`treePosition` written in the same PATCH as the prerequisite) so the edge appears without the card jumping; the catalog grid also uses stable slots (each tech's rank among all uncategorized technologies, so a tech promoted into the tree leaves its slot empty instead of re-shuffling every other root). Pressing "Auto-arrange" clears those pins and moves everything, including the connected node, to its ordered position. Node click opens the side panel, which is a **lite editor** over the fields it shows (name, type, approval status, summary, category with inline create, prerequisites with AND/OR grouping and add/remove, research costs with resource rows and min/max durations): dirty tracking, Save/Reset, and an "Edit in full view" jump to the standard admin edit route. Final approval states stay superuser-only (`canSetFinalApproval` = `system:admin-access`), mirroring the field access. Quick-create dialog (name, type, summary, category, research durations) creates via REST; the category picker is a native input + suggestion list (existing values selectable, new values creatable inline) because Radix Popover inside the admin Dialog proved layer-unstable. New categories get `position = max + 1`. The canvas uses React Flow's `<Background variant="lines">` so the grid scales with zoom (a CSS-background grid does not, and looked broken at low zoom), and `fitView` is capped at `maxZoom: 0.85`. Panel and toolbar controls carry dark tactical chrome (the admin panel's light shadcn defaults would render as white slabs on the dark surface; bare `<button>` elements in the admin additionally inherit a global light background AND border, so list rows need explicit `bg-transparent` + `border-0`), and the panel's dropdowns use the in-house `PanelSelect` listbox rather than native `<select>`: a native popup is browser/OS chrome that ignores dark styling (only `color-scheme` influences it) and cannot be screenshotted, whereas `PanelSelect` renders its list through a portal as a fixed-position overlay above the panel (so it never pushes the panel's content down), flips upward when there is more room above, closes on scroll/resize so it cannot detach from its trigger, and offers a search field for long lists. Both it and the panel's category picker use a mono header strip, hairline row separators, a cyan left accent on the selected row, and the `.tech-tree-scroll` dark scrollbar rules from `custom.scss`.
|
||||
- **Interactions**: nodes are draggable; dropping one PATCHes its `treePosition` (persisted, survives reload) and the toolbar shows an "Auto-arrange" button (technologies:update gated) that clears every manual position and restores the ordered layout. A **Save positions** button (technologies:update gated), next to Auto-arrange, commits the current on-screen position of every node to `treePosition` in one pass (matching what React Flow rendered, not just dragged/pinned cards) so the exact arrangement survives a reload; Auto-arrange then clears them. Dragging one card of a multi-selection moves and persists the whole selection (React Flow hands the full dragged set to `onNodeDragStop`), and a category band is draggable by its header (`dragHandle`), which moves every technology in that band. A **Snap: On/Off** toggle (default on, remembered per browser, 46px grid that divides `COLUMN_STEP`/`ROW_STEP`) snaps dragged cards to the grid. **Every canvas position write is one bulk request**: drags, Save positions, Auto-arrange, and freeze pins all POST `{ positions: [{ id, x, y }] }` to `/api/tech-tree/positions` (null x/y clears a pin), which `writeTreePositions` (`src/lib/tech-tree/positions.ts`) applies as a single `UPDATE ... FROM (VALUES ...)` on the `tree_position_x/y` columns (access = `technologies:update` or Intelligence membership, the collection's own update rule; batches capped at 2000). Never persist positions with a PATCH per node: firing dozens of concurrent document updates exhausted the database pool and could crash the server on Auto-arrange. Auto-arrange is optimistic (the canvas re-lays out on click, pins are restored if the save fails) and a spinner status chip plus disabled layout controls and node dragging cover the save. **Connecting does not move anything**: if the ordered layout would relocate the connected technology, the canvas pins it to its current spot (`treePosition` written in the same PATCH as the prerequisite) so the edge appears without the card jumping; the catalog grid also uses stable slots (each tech's rank among all uncategorized technologies, so a tech promoted into the tree leaves its slot empty instead of re-shuffling every other root). Pressing "Auto-arrange" clears those pins and moves everything, including the connected node, to its ordered position. Node click opens the side panel, which is a **lite editor** over the fields it shows (name, type, approval status, summary, category with inline create, prerequisites with AND/OR grouping and add/remove, research costs with resource rows and min/max durations): dirty tracking, Save/Reset, and an "Edit in full view" jump to the standard admin edit route. Final approval states stay superuser-only (`canSetFinalApproval` = `system:admin-access`), mirroring the field access. Quick-create dialog (name, type, summary, category, research durations) creates via REST; the category picker is a native input + suggestion list (existing values selectable, new values creatable inline) because Radix Popover inside the admin Dialog proved layer-unstable. New categories get `position = max + 1`. The canvas uses React Flow's `<Background variant="lines">` so the grid scales with zoom (a CSS-background grid does not, and looked broken at low zoom), and `fitView` is capped at `maxZoom: 0.85`. Panel and toolbar controls carry dark tactical chrome (the admin panel's light shadcn defaults would render as white slabs on the dark surface; bare `<button>` elements in the admin additionally inherit a global light background AND border, so list rows need explicit `bg-transparent` + `border-0`), and the panel's dropdowns use the in-house `PanelSelect` listbox rather than native `<select>`: a native popup is browser/OS chrome that ignores dark styling (only `color-scheme` influences it) and cannot be screenshotted, whereas `PanelSelect` renders its list through a portal as a fixed-position overlay above the panel (so it never pushes the panel's content down), flips upward when there is more room above, closes on scroll/resize so it cannot detach from its trigger, and offers a search field for long lists. Both it and the panel's category picker use a mono header strip, hairline row separators, a cyan left accent on the selected row, and the `.tech-tree-scroll` dark scrollbar rules from `custom.scss`.
|
||||
- **Styling**: tactical dossier (dark `#05070a` canvas + 28px grid via inline styles, squared corners, classification strips, mono readouts); shadcn form controls in the dialog stay conventional. React Flow controls/attribution are darkened via `.tech-tree-canvas` rules in `src/app/(payload)/custom.scss`.
|
||||
- **Gotchas**: React Flow in controlled mode needs `useNodesState` AND `useEdgesState` with change handlers that forward everything except `remove` (a no-op edge handler silently breaks edge selection, and a passed-through remove lets Backspace delete nodes that only exist in the data layer); category band nodes must be `pointerEvents: "none"` or they swallow edge/pane clicks; node handles must live OUTSIDE the card's `overflow-hidden` box or they become un-draggable; a second `next dev` for the same directory is impossible (Next 16 lock) so e2e verification either frees port 3000 or uses a copied repo (whose file watcher is unreliable: restart the copied server after every source change). technologies create requires `approvalStatus` + `researchCosts.minimumResearchDuration` (the field's `defaultValue: 0` violates its own `min: 1`, so creates must pass it explicitly); clearing `treePosition` means sending `{ x: null, y: null }` (a group cannot be nulled wholesale). The e2e seeds via cookie-authed REST (`page.request`) after frontend login, so it MUST run against the test DB: a dev server left on port 3000 is silently reused (`reuseExistingServer: true`) and its DEV database 403s every seed (the dev user has no create permission). Free the port first. Tests: `tests/int/tech-tree-layout.int.spec.ts` (pure layout: wrapping, overrides, band bboxes, warnings, cycles), `tests/int/tech-tree-prereq-edits.int.spec.ts` (pure add/remove/switch with cycle rejection), `tests/int/tech-categories.int.spec.ts` (CRUD, RBAC, division gating, FK set-null, position round-trip), `tests/int/tech-tree-edge-routes.int.spec.ts` (rounded route path + obstacle avoidance) + `tests/e2e/tech-tree.e2e.spec.ts` (nav, render, panel, edit nav, both quick-create paths, drag persistence + Auto-arrange, save positions + reload survival, multi-select drag, category-band drag, snap-to-grid, edge routing, connect/disconnect + lite-editor saves, wrapping at scale). Vivaldi crashes under automation in this suite; run with `E2E_BROWSER_PATH=<playwright chromium>`.
|
||||
- **Gotchas**: React Flow in controlled mode needs `useNodesState` AND `useEdgesState` with change handlers that forward everything except `remove` (a no-op edge handler silently breaks edge selection, and a passed-through remove lets Backspace delete nodes that only exist in the data layer); category band nodes must be `pointerEvents: "none"` or they swallow edge/pane clicks; node handles must live OUTSIDE the card's `overflow-hidden` box or they become un-draggable; a second `next dev` for the same directory is impossible (Next 16 lock) so e2e verification either frees port 3000 or uses a copied repo (whose file watcher is unreliable: restart the copied server after every source change). technologies create requires `approvalStatus` + `researchCosts.minimumResearchDuration` (the field's `defaultValue: 0` violates its own `min: 1`, so creates must pass it explicitly); clearing `treePosition` means sending `{ x: null, y: null }` (a group cannot be nulled wholesale). The e2e seeds via cookie-authed REST (`page.request`) after frontend login, so it MUST run against the test DB: a dev server left on port 3000 is silently reused (`reuseExistingServer: true`) and its DEV database 403s every seed (the dev user has no create permission). Free the port first. Tests: `tests/int/tech-tree-layout.int.spec.ts` (pure layout: wrapping, overrides, band bboxes, warnings, cycles), `tests/int/tech-tree-prereq-edits.int.spec.ts` (pure add/remove/switch with cycle rejection), `tests/int/tech-categories.int.spec.ts` (CRUD, RBAC, division gating, FK set-null, position round-trip), `tests/int/tech-tree-edge-routes.int.spec.ts` (rounded route path + obstacle avoidance), `tests/int/tech-tree-positions.int.spec.ts` (bulk position validation + single-statement write/clear) + `tests/e2e/tech-tree.e2e.spec.ts` (nav, render, panel, edit nav, both quick-create paths, drag persistence + Auto-arrange, save positions + reload survival, multi-select drag, category-band drag, snap-to-grid, edge routing, connect/disconnect + lite-editor saves, wrapping at scale). Vivaldi crashes under automation in this suite; run with `E2E_BROWSER_PATH=<playwright chromium>`.
|
||||
|
||||
## Wiki
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue