# LibrariesSidebar

Knowledge Libraries sidebar: renders a tree of articles (root + nested
subarticles), lets you navigate, create, and reorder via drag & drop.

It's built for large lists (hundreds/thousands of nodes), so it has several
performance decisions worth understanding before you touch it.

---

## TL;DR (mental model in 30 seconds)

1. The consumer (backoffice/web) fetches data from its API and turns it into a
   **`SidebarModel`** using the **adapters**. The component never talks to the API.
2. You pass the component `model` + `capabilities` + callbacks. That's it.
3. Inside, a single **`SidebarProvider`** (context) owns all the state (sort mode,
   pending order, which rows are expanded). Rows only read from the context.
4. The **root** list is virtualized (only renders what's visible). Drag & drop
   (`dnd-kit`) is only mounted when you enter **sort mode**.

```
consumer API ──► adapters ──► SidebarModel ──► <LibrariesSidebar/> ──► rows (UI)
  (each repo)   (material-hu)   (contract)       (this component)
```

---

## Key concepts

### `SidebarModel` — the contract

This is the **only way** data enters the component. Defined in
[`types/model.ts`](./types/model.ts):

```ts
type SidebarModel = {
  headerTitle: string;           // header title (empty at root)
  nodes: readonly SidebarNode[]; // FLAT list (adjacency list), not a tree
};
```

Key point: `nodes` is a **flat list**, not a tree. Each node knows its parent
(`parentId`) and its depth (`depth`). The tree is rebuilt in memory inside the
context (`childrenByParent`).

```ts
type SidebarNode = {
  id: number;
  parentId: number | null;  // null = lives at the root
  position: number;         // order among siblings
  title: string;
  icon: IconInterface;
  status: 'enabled' | 'disabled';
  childCount: number;       // how many direct children (for the chevron)
  depth: number;            // 1 = root, 2 = child, ...
  isEditor?: boolean | null;
};
```

### `SidebarCapabilities` — the permissions/flags

Presentation flags decided by the consumer (typically from user permissions):

```ts
type SidebarCapabilities = {
  canCreate: boolean;       // shows/hides the "+" buttons
  showStatusBadge: boolean; // shows the status badge on the avatar
};
```

### `LibrariesTreeItem` — the row model

It's the `SidebarNode` already "cooked" for the UI (node + capabilities merged).
Produced by `toLibrariesTreeItem` in [`utils/index.ts`](./utils/index.ts). Rows
work with this, **not** with `SidebarNode` directly.

---

## How to consume it (real example)

The consumer does 3 things: (1) adapt data into a `SidebarModel`, (2) build
`capabilities`, (3) render `<LibrariesSidebar/>` with the callbacks.

```tsx
// 1) Adapt API → SidebarModel (in a consumer hook)
const model = resolveSidebarModel(isRoot, rootArticles, childrenModel);

// 2) Capabilities from permissions
const capabilities = { canCreate: canCreateLibraries, showStatusBadge: true };

// 3) Render
<LibrariesSidebar
  model={model}
  capabilities={capabilities}
  parentId={rootId}            // undefined => root view, number => children view
  loading={isLoading}
  onBack={handleBack}
  onItemClick={onItemClick}    // navigate to the article
  onAdd={handleCreate}         // open the create drawer
  onSort={handleSort}          // persist the new order
  onAddMouseEnter={prefetch}   // (optional) prefetch on hover
/>
```

### The adapters (your entry point as a consumer)

They live in [`utils/adapters.ts`](./utils/adapters.ts). They turn the API
response into a `SidebarModel`:

| Function | Purpose |
|---|---|
| `adaptFromRootArticles(articles)` | Root list (`GET /knowledge-libraries`) |
| `adaptFromChildrenTree(tree, viewParentId)` | Nested tree (`GET /:id/children-tree`) |
| `resolveSidebarModel(isRoot, root, children)` | Picks root vs nested per the view |
| `recalculateChildCounts(model)` | Recomputes `childCount` after filtering nodes |
| `applySidebarNodeFilter(model, predicate, ctx)` | Filters nodes (e.g. search) + recomputes counts |

> `onSort` hands you back `SortedItem[]`. Use `getNewPositionOrders(payload)` from
> [`utils/index.ts`](./utils/index.ts) to convert it into the `{ parentId, orderedIds }`
> shape the backend usually expects.

---

## How it works inside

### File structure

```
LibrariesSidebar/
├── index.tsx              # Entry point. Wires up Provider + header + list.
├── context.tsx            # ⭐ SidebarProvider: owner of ALL state.
├── types/
│   ├── model.ts           # Data contract (SidebarModel, SidebarNode, ...).
│   └── index.ts           # Internal component types (props, context, row).
├── utils/
│   ├── adapters.ts        # API → SidebarModel (used by the consumer).
│   ├── tree.ts            # sortByPosition, flattenTree, buildDepthMap.
│   └── index.ts           # toLibrariesTreeItem, getReorderedItems, etc.
├── hooks/
│   ├── useSortableList.ts # Connects a list (root or children) to drag & drop.
│   └── useGetSidebarColors.ts
└── components/
    ├── headers/           # Header, actions, sort buttons.
    └── list/
        ├── SortableRows.tsx          # dnd-kit wrapper (sort mode only).
        ├── root/                     # Root list (virtualized).
        └── children/                 # Nested lists (recursive accordion).
```

### The context owns the state ([`context.tsx`](./context.tsx))

All the logic lives in `SidebarProvider`. Rows and headers only **read** from the
context via `useSidebarContext()`. What it manages:

- **Item index**: builds `itemsById` (O(1) lookup) and `childrenByParent`
  (rebuilt tree) from the flat `model.nodes` list.
- **Sort mode**: the `isSortMode` toggle and whether the sort button shows
  (`showSortButton`, on when `onSort` is provided).
- **Pending (optimistic) order**: while sorting, drag results are staged per
  parent in `pendingSortByParent`. On save, they're emitted via `onSort` and kept
  applied until the refetched `model` reflects them.
- **Row expansion**: tracked in a `Set` inside a `ref` (`expandedIds`), so
  opening/closing a row does **not** re-render the tree.

### Root vs Children (two different lists)

- **Root** ([`list/root/SidebarRootList.tsx`](./components/list/root/SidebarRootList.tsx)):
  flat, **virtualized** list (`useVirtualizer`, backed by
  `@tanstack/react-virtual`). Only renders visible rows → scales to thousands of
  items.
- **Children** ([`list/children/SidebarChildrenList.tsx`](./components/list/children/SidebarChildrenList.tsx)):
  **recursive** accordion. Each `SidebarChildRow` can expand and render another
  `SidebarChildrenList` inside (`<Collapse mountOnEnter unmountOnExit>`).
  Only the **top level (`depth === 0`)** is virtualized with `useVirtualizer`,
  using **dynamic measurement** (`rowVirtualizer.measureElement`) because rows
  grow when expanded. Nested levels (`depth > 0`) render inline so the parent's
  measured row reflects their full expanded height (no nested scroll containers).

### The sort mode flow

1. User taps "sort" → `handleToggleSortMode()` sets `isSortMode = true`.
2. The list switches from its normal render to `SortableRows` (which mounts `dnd-kit`).
3. Each drag calls `handleDragEnd` → `getReorderedItems` → `setPendingSort`
   (stages the optimistic order per parent).
4. "Save" → `handleSaveSort()` emits `onSort(sortedItems)` and exits sort mode.
   The optimistic order stays until the refetch confirms it.
5. "Cancel" → `handleRestoreSortableItems()` discards the pending order.

---

## Performance notes (why it's built this way)

> If you're going to touch the list rendering, read this first.

1. **Root is virtualized in read mode.** Rendering thousands of MUI rows at once
   is expensive. `useVirtualizer` (`@tanstack/react-virtual`) only mounts what's
   visible, absolutely positioned inside a sized spacer.

2. **`dnd-kit` only in sort mode.** `dnd-kit` adds per-row cost (listeners,
   per-item context, re-renders on every move). That's why `SortableRows` is
   **only** mounted when `isSortMode` is true. In read mode rows are flat.
   - Expected side effect: **entering** sort mode on a large list has a small
     delay because all sortable items mount at once (the list is no longer
     virtualized). It's a one-time mounting cost and expected.

3. **Optimistic order keyed by content, not array identity.** The pending-order
   reset is triggered by a stable `id:position` signature of the nodes
   (`nodesSignature`), not by the `items` array identity. This keeps the
   optimistic order alive until the real refetch, regardless of whether the
   consumer memoizes its `model`/`capabilities`.

4. **Expansion in a `ref`, not state.** Opening/closing rows doesn't re-render
   the tree and the open/closed state survives remounts (important with
   virtualization).

5. **Memoized rows** (`SortableRootRow`, `SidebarChildRow` with `React.memo`):
   avoid re-renders when the context changes but the row doesn't.

---

## "I want to..." (quick recipes)

- **...add a field to each row** → add it to `SidebarNode`
  ([`types/model.ts`](./types/model.ts)), propagate it in the adapters
  ([`utils/adapters.ts`](./utils/adapters.ts)) and in `toLibrariesTreeItem`
  ([`utils/index.ts`](./utils/index.ts)) if the UI needs it.
- **...add a permission/flag** → add it to `SidebarCapabilities`
  ([`types/model.ts`](./types/model.ts)) and consume it where needed.
- **...change how a row looks** → `SortableRootRow` (root) or `SidebarChildRow`
  (children), in `components/list/`.
- **...change the reorder logic** → `getReorderedItems`
  ([`utils/index.ts`](./utils/index.ts)) and the sort flow in
  [`context.tsx`](./context.tsx).
- **...filter nodes (e.g. search)** → use `applySidebarNodeFilter` in the
  consumer; don't put filtering inside the component.
- **...support a new API** → write a new `adaptFrom...` in
  [`utils/adapters.ts`](./utils/adapters.ts) that returns a `SidebarModel`. Don't
  touch the lists.

---

## Golden rules

- The component does **not** talk to the API. Everything comes in as a `SidebarModel`.
- **State lives in the context**, not in rows. Rows read, they don't own.
- `model.nodes` is **flat**. The tree is built in memory.
- Don't put `dnd-kit` in the read-mode render.
- If you touch `lib/` (artifacts): run the full `bun run tsc` before committing,
  or the push fails on the `prepush:tsc-guard`.
