11 KiB
categoryAutomat
Source File:
src/automats/categoryAutomat.js
Registration Key:'category'(Automat.get('category')/window.__AUTOMATS__.get('category'))
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.
Core Responsibilities
- Hierarchical Category Tree: Normalized
id → nodedictionary (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. - 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 active route automat's article list. - 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;
}
Initial State
const initialState = {
companyId: null,
languageId: null,
categories: null,
rootCategoryId: null,
renameMode: false,
treeStatus: 'idle',
error: null,
};
3. Dynamic Category View Automats (KeyedAutomat)
Category views are powered by a KeyedAutomat instance (categoryViews) indexed by route slug path (e.g. '' for root, 'getraenke', 'getraenke/softdrinks').
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(all child categories having the current as their root/parent). - 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.
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, and updates state.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.