u
This commit is contained in:
@@ -30,7 +30,8 @@ In `dashfront`, state is managed outside the React component lifecycle using **A
|
||||
| **`artikelAutomat`** | [`src/automats/artikelAutomat.js`](file:///home/seb/src/dashfront/src/automats/artikelAutomat.js) | [artikelAutomat.md](./artikelAutomat.md) | Cache and reactive distributor for single article detail payloads (`GET /api/article/:sku`) with inflight request deduplication. |
|
||||
| **`authAutomat`** | [`src/automats/authAutomat.js`](file:///home/seb/src/dashfront/src/automats/authAutomat.js) | [authAutomat.md](./authAutomat.md) | Manages authentication lifecycle, session bootstrap, Google One Tap login, and multi-tenant company selection. |
|
||||
| **`brandAutomat`** | [`src/automats/brandAutomat.js`](file:///home/seb/src/dashfront/src/automats/brandAutomat.js) | [brandAutomat.md](./brandAutomat.md) | Company-scoped catalog brands / manufacturers list (`GET /api/brand`, upsert). Auto-loads on company auth. |
|
||||
| **`categoryAutomat`** | [`src/automats/categoryAutomat.js`](file:///home/seb/src/dashfront/src/automats/categoryAutomat.js) | [categoryAutomat.md](./categoryAutomat.md) | Hierarchical category tree (id → node map), route view article automats, tree ordering, drag-and-drop moves, and image uploads. |
|
||||
| **`categoryAutomat`** | [`src/automats/categoryAutomat.js`](file:///home/seb/src/dashfront/src/automats/categoryAutomat.js) | [categoryAutomat.md](./categoryAutomat.md) | Hierarchical category tree (id → node map), tree ordering, drag-and-drop moves, and image uploads. Triggers the bulk category-view preload once the tree succeeds. |
|
||||
| **`categoryViewAutomat`** | [`src/automats/categoryViewAutomat.js`](file:///home/seb/src/dashfront/src/automats/categoryViewAutomat.js) | [categoryViewAutomat.md](./categoryViewAutomat.md) | Per-category article views (`KeyedAutomat`) keyed by slug path; all views are warmed right after the tree loads. |
|
||||
| **`companyAutomat`** | [`src/automats/companyAutomat.js`](file:///home/seb/src/dashfront/src/automats/companyAutomat.js) | [companyAutomat.md](./companyAutomat.md) | Active company configuration, tax rates (standard, reduced), root category ID, and company profile info. |
|
||||
| **`deliveryEditorAutomat`** | [`src/automats/deliveryEditorAutomat.js`](file:///home/seb/src/dashfront/src/automats/deliveryEditorAutomat.js) | [deliveryEditorAutomat.md](./deliveryEditorAutomat.md) | Multi-line inbound delivery editor, OCR/PDF receipt reconciliation, supplier article auto-matching, and draft badge tracking. |
|
||||
| **`navigationAutomat`** | [`src/automats/navigationAutomat.js`](file:///home/seb/src/dashfront/src/automats/navigationAutomat.js) | [navigationAutomat.md](./navigationAutomat.md) | Tracks and restores last visited article SKU and delivery paths across top-level section navigation. |
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# `categoryAutomat`
|
||||
|
||||
> **Source File**: [`src/automats/categoryAutomat.js`](file:///home/seb/src/dashfront/src/automats/categoryAutomat.js)
|
||||
> **Registration Key**: `'category'` (`Automat.get('category')` / `window.__AUTOMATS__.get('category')`)
|
||||
> **Registration Key**: `'category'` (`Automat.get('category')` / `window.__AUTOMATS__.get('category')`)
|
||||
> **Related**: [`categoryViewAutomat`](./categoryViewAutomat.md) — the keyed per-category article views, extracted into their own module.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview & Purpose
|
||||
|
||||
`categoryAutomat` manages the hierarchical category catalog tree for the active company (`/kategorien/*`), category breadcrumbs, category images, POS category visibility flags, and dynamic per-route category article view automats.
|
||||
`categoryAutomat` manages the hierarchical category catalog tree for the active company (`/kategorien/*`), category breadcrumbs, category images and POS category visibility flags. It owns the tree and all category/article **mutations**; the per-category *article views* live in [`categoryViewAutomat.js`](../src/automats/categoryViewAutomat.js) and are attached to this automat via `attachCategoryTree()`.
|
||||
|
||||
### Core Responsibilities
|
||||
- **Hierarchical Category Tree**: Normalized `id → node` dictionary (`categories`) representing the parent-child relational tree.
|
||||
- **Dynamic Per-Route Category View Automats**: Instantiates lightweight route-scoped automats (`category-view:<slug>`) that load category articles on-demand with automatic cache cleanup and disposal.
|
||||
- **Bulk View Preload**: after `GET /api/category/tree` succeeds, it triggers `loadAllCategoryViews()` so every category's articles are already cached when a route mounts.
|
||||
- **Mutations & Tree Reordering**: Category creation, batch creation, sibling reordering (`sort`), drag-and-drop hierarchy moves, and in-place renaming.
|
||||
- **Article Insertion & Synchronous UI Reflection**: When articles are created within a category, `categoryAutomat` immediately patches the active route automat's article list.
|
||||
- **Article Insertion & Synchronous UI Reflection**: When articles are created within a category, `categoryAutomat` immediately patches the matching category views' article lists.
|
||||
- **Media Management**: Uploading and deleting category cover images and updating AVIF full and thumbnail hashes (`fullHash`, `thumbHash`).
|
||||
- **POS Integration**: Toggling POS visibility and hardware terminal IDs per category.
|
||||
|
||||
@@ -50,6 +51,8 @@ interface CategoryState {
|
||||
treeStatus: 'idle' | 'loading' | 'succeeded' | 'failed';
|
||||
/** Last tree error message. */
|
||||
error: string | null;
|
||||
/** True while the post-tree preload of all category views is running. */
|
||||
viewsLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,29 +66,35 @@ const initialState = {
|
||||
renameMode: false,
|
||||
treeStatus: 'idle',
|
||||
error: null,
|
||||
viewsLoading: false,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Dynamic Category View Automats (`KeyedAutomat`)
|
||||
## 3. Category View Automats (delegated)
|
||||
|
||||
Category views are powered by a `KeyedAutomat` instance (`categoryViews`) indexed by route **slug path** (e.g. `''` for root, `'getraenke'`, `'getraenke/softdrinks'`).
|
||||
The keyed per-category article views are **no longer defined in this file**. They are implemented in [`categoryViewAutomat.js`](../src/automats/categoryViewAutomat.js) and re-exported here for backward compatibility:
|
||||
|
||||
| Symbol | Source |
|
||||
|---|---|
|
||||
| `categoryViews` | `categoryViewAutomat.js` |
|
||||
| `getOrCreateCategoryAutomat({ slugPath })` | `categoryViewAutomat.js` |
|
||||
| `invalidateAllCategoryViews()` | `categoryViewAutomat.js` |
|
||||
| `disposeAllCategoryViewAutomats()` | `categoryViewAutomat.js` |
|
||||
|
||||
The following facades remain on the tree automat and delegate to `categoryViews`:
|
||||
|
||||
### `categoryAutomat.key(slugPath): KeyHandle`
|
||||
Returns a scoped `KeyHandle` proxy for the given route slug.
|
||||
- **Loader**: Resolves `slugPath` → `categoryId` from the tree, fetches articles via `GET /api/category/:id/articles`, and derives `childCategories` (all child categories having the current as their root/parent).
|
||||
- **Loader**: Resolves `slugPath` → `categoryId` from the tree, fetches articles via `GET /api/category/:id/articles`, and derives `childCategories`.
|
||||
- **Handle State**: `{ data: { slug, categoryId, articles, childCategories }, loaded: boolean, loading: boolean, error: string | null }`.
|
||||
- **`handle.load(options?)`**: Deduplicated background fetch across concurrent mounts.
|
||||
- **`handle.read()`**: React Suspense-compatible read (throws in-flight promise if unready).
|
||||
- **`handle.reload()`**: Re-fetches articles and child categories.
|
||||
- **`handle.subscribe(target, selector)`**: Fine-grained subscription to this slug view with zero cross-talk between different category routes.
|
||||
|
||||
### `invalidateAllCategoryViews(): void`
|
||||
Triggers `reload()` across all active slug view handles.
|
||||
|
||||
### `disposeAllCategoryViewAutomats(): void`
|
||||
Clears all active view handles and frees memory on company switch or logout.
|
||||
See [categoryViewAutomat.md](./categoryViewAutomat.md) for the full view API, including `loadAllCategoryViews()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +103,8 @@ Clears all active view handles and frees memory on company switch or logout.
|
||||
Access actions via `categoryAutomat.actions.<method>()`.
|
||||
|
||||
### Tree Operations
|
||||
- **`loadTree(): Promise<void>`**: Reads `rootCategoryId` from `companyAutomat`, queries `GET /api/category/tree`, parses the response into a normalized dictionary, and updates state.
|
||||
- **`loadTree(): Promise<void>`**: Reads `rootCategoryId` from `companyAutomat`, queries `GET /api/category/tree`, parses the response into a normalized dictionary, updates state, and then **awaits `loadAllCategoryViews()`** with `viewsLoading: true → false`. Individual category failures are logged as a warning and do not fail the tree load.
|
||||
- **`loadAllCategoryViews(): Promise<{ loaded: number, failed: Array<{ slug, error }> }>`**: Re-runs the bulk view preload on demand (idempotent — already cached views resolve immediately).
|
||||
- **`setRenameMode(enabled: boolean): void`**: Toggles inline renaming mode for the category explorer.
|
||||
- **`reset(): void`**: Disposes all route automats and resets tree state to `initialState`.
|
||||
|
||||
|
||||
136
docs/categoryViewAutomat.md
Normal file
136
docs/categoryViewAutomat.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# `categoryViewAutomat`
|
||||
|
||||
> **Source File**: [`src/automats/categoryViewAutomat.js`](file:///home/seb/src/dashfront/src/automats/categoryViewAutomat.js)
|
||||
> **Registration Key**: `'category-views'` (`Automat.get('category-views')` / `window.__AUTOMATS__.get('category-views')`)
|
||||
> **Related**: [`categoryAutomat`](./categoryAutomat.md) — owns the category **tree** this module resolves against.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview & Purpose
|
||||
|
||||
`categoryViewAutomat` owns the **per-category article views**: a `KeyedAutomat` (`categoryViews`) indexed by route **slug path** (`''` for root, `'getraenke'`, `'getraenke/softdrinks'`). Each entry holds one category's articles plus its child categories.
|
||||
|
||||
This module was extracted from [`categoryAutomat.js`](../src/automats/categoryAutomat.js) so the tree and the views can evolve independently. The dependency stays **one-directional**:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CA[categoryAutomat<br/>tree + mutations] -->|attachCategoryTree| CV[categoryViewAutomat<br/>KeyedAutomat views]
|
||||
CV -->|loadAllCategoryViews| API[/api/category/:id/articles]
|
||||
CV -->|collectLoadedCategoryViewData| AI[articleIndexAutomat<br/>derived search index]
|
||||
```
|
||||
|
||||
`categoryAutomat` injects itself via `attachCategoryTree(automat)`; `categoryViewAutomat` never imports the tree, which avoids a circular import.
|
||||
|
||||
### Core Responsibilities
|
||||
- **Slug → Category Resolution**: Resolves a slug path against the loaded tree to a `categoryId`.
|
||||
- **Per-Category Article Payloads**: `GET /api/category/:id/articles` for the resolved category.
|
||||
- **Bulk Warm-Up**: `loadAllCategoryViews()` loads **every** category in the tree (concurrency-bounded), triggered by `categoryAutomat.loadTree()` the moment the tree succeeds.
|
||||
- **Child Category Derivation**: Recomputes `childCategories` for all loaded views whenever the tree changes.
|
||||
- **Incremental Patches**: Appends created articles to, and removes deleted articles from, the affected views.
|
||||
|
||||
---
|
||||
|
||||
## 2. View Data Structure
|
||||
|
||||
```ts
|
||||
interface CategoryViewData {
|
||||
/** Cleaned slug path this view is keyed by. */
|
||||
slug: string;
|
||||
/** Resolved category id. */
|
||||
categoryId: number | string;
|
||||
/** Articles in that category. */
|
||||
articles: Array<ArticleRow>;
|
||||
/** Direct child category nodes, sorted by `sort`. */
|
||||
childCategories: Array<CategoryNode>;
|
||||
}
|
||||
```
|
||||
|
||||
Each keyed entry wraps this as `ItemState<CategoryViewData>`:
|
||||
`{ data, loading, loaded, error }`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Loading Strategy
|
||||
|
||||
### `loadAllCategoryViews({ concurrency }): Promise<{ loaded, failed }>`
|
||||
Builds the slug path of **every** category node from its breadcrumb trail
|
||||
(`categoryViewSlugPath()`), de-duplicates them, then loads each with
|
||||
`categoryViews.load(slug)`.
|
||||
|
||||
- **Already cached views resolve immediately** (`load()` short-circuits when `loaded && !error`), so the call is idempotent.
|
||||
- Concurrency defaults to **6**; failures are captured per category and returned in `failed` rather than rejecting the batch.
|
||||
- **Triggered by**: `categoryAutomat.actions.loadTree()` right after `treeStatus === 'succeeded'`, and available on demand via `categoryAutomat.actions.loadAllCategoryViews()`.
|
||||
|
||||
### `reloadLoadedCategoryViews(): Promise<void>`
|
||||
Re-fetches **only the already cached** views (`setDirty(undefined, false, true)`), keeping the cached set small for SSE-driven refreshes.
|
||||
|
||||
### `invalidateAllCategoryViews(): Promise<unknown[]>`
|
||||
Background-invalidates all loaded views. Called by `sseAutomat` on `'category'` and `'articles'` events.
|
||||
|
||||
### `disposeAllCategoryViewAutomats(): void`
|
||||
Clears every view and frees memory on company switch or logout.
|
||||
|
||||
### `waitForCategoryTree(): Promise<CategoryState>`
|
||||
Resolves once the attached tree reports `treeStatus === 'succeeded'`. Rejects on tree failure or when `authAutomat` turns `'anon'`, so a pending view never hangs forever.
|
||||
|
||||
---
|
||||
|
||||
## 4. Exported API
|
||||
|
||||
| Export | Description |
|
||||
|---|---|
|
||||
| `categoryViews` (default) | The `KeyedAutomat` keyed by slug path. |
|
||||
| `attachCategoryTree(automat)` | Injects the tree automat (called once by `categoryAutomat`). |
|
||||
| `categoryViewSlugPath(categories, rootCategoryId, categoryId)` | Breadcrumb-derived slug path usable as a key. |
|
||||
| `collectLoadedCategoryViewData()` | All loaded view payloads — the source for the derived article index. |
|
||||
| `loadAllCategoryViews({ concurrency })` | Bulk warm-up of every category in the tree. |
|
||||
| `reloadLoadedCategoryViews()` | Re-fetch only the cached views. |
|
||||
| `invalidateAllCategoryViews()` | Background-invalidate all loaded views. |
|
||||
| `disposeAllCategoryViewAutomats()` | Clear all views. |
|
||||
| `getOrCreateCategoryAutomat({ slugPath })` | Returns the `KeyHandle` for a slug path. |
|
||||
| `refreshCategoryViewChildren(categories)` | Re-derive `childCategories` for every loaded view. |
|
||||
| `appendArticlesToCategoryViews({ categoryId, articles })` | Append newly created articles. |
|
||||
| `removeArticleBySku(sku)` | Strip a deleted article from every view. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Key Handle Usage
|
||||
|
||||
```js
|
||||
import categoryAutomat from '../automats/categoryAutomat';
|
||||
|
||||
const handle = categoryAutomat.key('getraenke/softdrinks');
|
||||
|
||||
handle.subscribe(this, (item) => ({
|
||||
articles: item.data?.articles ?? [],
|
||||
childCategories: item.data?.childCategories ?? [],
|
||||
}));
|
||||
|
||||
const data = await handle.load(); // deduplicated across concurrent mounts
|
||||
const data2 = handle.read(); // Suspense: throws the in-flight promise when unready
|
||||
handle.reload();
|
||||
handle.setDirty();
|
||||
```
|
||||
|
||||
All four facades (`key`, `read`, `load`, `views`) are attached to `categoryAutomat`
|
||||
as thin delegates to `categoryViews`, so existing import sites keep working.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cross-Automat Coordination
|
||||
|
||||
| Automat | Interaction |
|
||||
|---|---|
|
||||
| `categoryAutomat` | `attachCategoryTree()`, `loadAllCategoryViews()` after the tree succeeds, `refreshCategoryViewChildren()` on every tree mutation, `disposeAllCategoryViewAutomats()` on reset. |
|
||||
| `authAutomat` | `waitForCategoryTree()` rejects pending views on `'anon'`. |
|
||||
| `sseAutomat` | `invalidateAllCategoryViews()` on `'category'` / `'articles'` events. |
|
||||
| `articleIndexAutomat` | Subscribes to `categoryViews` and re-derives the search index from `collectLoadedCategoryViewData()`. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Gotchas
|
||||
|
||||
- **Bulk warm-up cost**: `loadTree()` now issues one request per category. On very large trees this is many requests — `concurrency` (default 6) is the throttle to tune. Individual failures are non-fatal.
|
||||
- **`viewsLoading`**: While it is `true`, the tree is ready but the views are still warming. Use it to decide between "render a spinner" and "render an empty grid".
|
||||
- **Keys are slug paths, not ids**: A rename or move changes the slug, so the view key changes and the old entry should be evicted; `refreshCategoryViewChildren()` handles the child lists, but stale keys may linger until the next `disposeAllCategoryViewAutomats()`.
|
||||
- **Do not import `categoryAutomat` from this file** — that would create the circular dependency the `attachCategoryTree()` injection exists to avoid.
|
||||
Reference in New Issue
Block a user