12 KiB
categoryAutomat
Source File:
src/automats/categoryAutomat.js
Registration Key:'category'(Automat.get('category')/window.__AUTOMATS__.get('category'))
Related:categoryViewAutomat— 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 and POS category visibility flags. It owns the tree and all category/article mutations; the per-category article views live in categoryViewAutomat.js and are attached to this automat via attachCategoryTree().
Core Responsibilities
- Hierarchical Category Tree: Normalized
id → nodedictionary (categories) representing the parent-child relational tree. - Bulk View Preload: after
GET /api/category/treesucceeds, it triggersloadAllCategoryViews()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,
categoryAutomatimmediately 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.
2. State Structure
interface CategoryNode {
id: number | string;
name: string;
parentId: number | string | null;
sort: number;
pos?: boolean;
fullHash?: string | null;
thumbHash?: string | null;
thumbW?: number | null;
thumbH?: number | null;
posterminalIds?: Array<number>;
childCount?: number;
[key: string]: any;
}
interface CategoryState {
companyId: number | string | null;
languageId: string | null;
/** Normalized category map keyed by string ID. */
categories: Record<string, CategoryNode> | null;
/** Root (home) category ID for the current company. */
rootCategoryId: number | string | null;
/** Whether category tiles are in rename/edit mode. */
renameMode: boolean;
/** Category tree loading status. */
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;
}
Initial State
const initialState = {
companyId: null,
languageId: null,
categories: null,
rootCategoryId: null,
renameMode: false,
treeStatus: 'idle',
error: null,
viewsLoading: false,
};
3. Category View Automats (delegated)
The keyed per-category article views are no longer defined in this file. They are implemented in 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→categoryIdfrom the tree, fetches articles viaGET /api/category/:id/articles, and deriveschildCategories. - 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.
See categoryViewAutomat.md for the full view API, including loadAllCategoryViews().
4. Actions & Methods
Access actions via categoryAutomat.actions.<method>().
Tree Operations
loadTree(): Promise<void>: ReadsrootCategoryIdfromcompanyAutomat, queriesGET /api/category/tree, parses the response into a normalized dictionary, updates state, and then awaitsloadAllCategoryViews()withviewsLoading: 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 toinitialState.
Category Mutations
createCategory({ parentId, name }): Promise<CategoryNode>: Creates a category viaPOST /api/categoryand inserts it intocategories.createCategories({ parentId, names }): Promise<CategoryNode[]>: Batch-creates multiple categories underparentId.renameCategory({ categoryId, name }): Promise<CategoryNode>: Updates a category name viaPATCH /api/category/:id/name. Protects root category from being renamed.reorderCategories({ parentId, categoryIds }): Promise<number[]>: Updates sibling display ordering viaPOST /api/category/:parentId/reorder.moveCategory({ categoryId, parentId }): Promise<object>: Moves a category under a new parent viaPATCH /api/category/:id/move.deleteCategory(categoryId): Promise<number|string>: Deletes a category viaDELETE /api/category/:idand removes it from state.toggleCategoryPos({ categoryId, pos }): Promise<CategoryNode>: Toggles POS visibility viaPATCH /api/category/:id/pos.
Article Mutations within Categories
createArticle({ parentId, name }): Promise<object>: DispatchesPOST /api/articleand appends the new article to the active category view automat'sarticleslist immediately.createArticles({ parentId, names }): Promise<object[]>: Batch creates articles and updates the active route view.removeArticleBySku(sku: string): void: Removes an article from all live category view article lists when deleted.
Category Media
uploadCategoryImage({ categoryId, file }): Promise<object>: Uploads an image blob toPOST /api/category/:id/imageand patchesfullHash,thumbHash,thumbW, andthumbHin the category node.deleteCategoryImage(categoryId): Promise<object>: Deletes the category image viaDELETE /api/category/:id/image.
Hardware POS Terminals
patchCategoryPosterminalIds({ categoryId, terminalId, enabled }): void: Updates theposterminalIdsarray on a category node locally.
5. Exported Tree Utilities
Pure helper functions re-exported from ../utils/categoryUtils:
ROOT_CATEGORY_NAME: Constant for the root category.getCategoryBreadcrumbs(categories, categoryId): Array<CategoryNode>: Traverses parent pointers from node to root.getCategoryChildren(categories, parentId): Array<CategoryNode>: Returns all immediate child nodes sorted bysort.categoryDisplayName(category): string: Formats category display label.categorySlug(name): string: Generates a clean URL slug.normalizeCategoryNode(raw): CategoryNode: Normalizes API category payload.
6. Cross-Automat Coordination
categoryAutomat automatically stays in sync with companyAutomat and authAutomat:
// Sync with companyAutomat:
companyAutomat.subscribe((company) => {
const id = company.current?.id ?? null;
const rootCategoryId = company.current?.rootCategoryId ?? null;
if (id && rootCategoryId && id !== _lastCompanyId) {
_lastCompanyId = id;
disposeAllCategoryViewAutomats();
void categoryAutomat.actions.loadTree();
}
if (!id && _lastCompanyId) {
_lastCompanyId = null;
categoryAutomat.actions.reset();
}
});
// Reset on logout:
authAutomat.subscribe((auth) => {
if (auth.status === 'anon') {
_lastCompanyId = null;
categoryAutomat.actions.reset();
}
});
7. Usage Examples
A. React PureComponent Category Tree Browser
import { PureComponent } from 'react';
import categoryAutomat, { getCategoryChildren, getCategoryBreadcrumbs } from '../automats/categoryAutomat';
export class CategoryBrowser extends PureComponent {
constructor(props) {
super(props);
this.state = {
categories: categoryAutomat.state.categories,
rootCategoryId: categoryAutomat.state.rootCategoryId,
treeStatus: categoryAutomat.state.treeStatus,
};
}
componentDidMount() {
this.unsub = categoryAutomat.subscribe(this, ['categories', 'rootCategoryId', 'treeStatus']);
}
componentWillUnmount() {
this.unsub?.();
}
render() {
const { categories, rootCategoryId, treeStatus } = this.state;
if (treeStatus === 'loading' || !categories) return <div>Lade Kategorien...</div>;
const currentParentId = this.props.categoryId ?? rootCategoryId;
const subcategories = getCategoryChildren(categories, currentParentId);
const breadcrumbs = getCategoryBreadcrumbs(categories, currentParentId);
return (
<div>
<nav>
{breadcrumbs.map((b) => (
<span key={b.id}>{b.name} / </span>
))}
</nav>
<ul>
{subcategories.map((cat) => (
<li key={cat.id}>{cat.name}</li>
))}
</ul>
</div>
);
}
}
B. Consuming a Per-Route Category View with categoryAutomat.key()
import { PureComponent } from 'react';
import categoryAutomat from '../automats/categoryAutomat';
export class CategoryArticlesList extends PureComponent {
constructor(props) {
super(props);
this.handle = categoryAutomat.key(props.slugPath);
this.state = {
articles: this.handle.state.data?.articles ?? [],
childCategories: this.handle.state.data?.childCategories ?? [],
loading: this.handle.state.loading,
};
}
componentDidMount() {
this.unsub = this.handle.subscribe(this, (item) => ({
articles: item.data?.articles ?? [],
childCategories: item.data?.childCategories ?? [],
loading: item.loading,
}));
this.handle.load();
}
componentWillUnmount() {
this.unsub?.();
}
render() {
const { articles, childCategories, loading } = this.state;
if (loading) return <div>Lade Artikel in dieser Kategorie...</div>;
return (
<div>
<h3>Unterkategorien ({childCategories.length})</h3>
<ul>
{childCategories.map((cat) => (
<li key={cat.id}>{cat.name}</li>
))}
</ul>
<h3>Artikel ({articles.length})</h3>
<ul>
{articles.map((art) => (
<li key={art.sku}>{art.name} ({art.sku})</li>
))}
</ul>
</div>
);
}
}
8. Best Practices & Gotchas
- Root category cannot be renamed or deleted: The backend returns HTTP 400 if you attempt to rename or delete the company root category.
- Reverse index updates: When creating articles with
categoryAutomat.actions.createArticle, any active route automat matching the parent ID is updated immediately. - Use normalized helpers: Always use
getCategoryChildren(categories, parentId)rather than manual filtering; it ensures correct sort order.