Files
dashfront/docs/categoryAutomat.md
2026-10-04 23:38:46 +02:00

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 → node dictionary (categories) representing the parent-child relational tree.
  • 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 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 → 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.

See categoryViewAutomat.md for the full view API, including loadAllCategoryViews().


4. Actions & Methods

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, 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.

Category Mutations

  • createCategory({ parentId, name }): Promise<CategoryNode>: Creates a category via POST /api/category and inserts it into categories.
  • createCategories({ parentId, names }): Promise<CategoryNode[]>: Batch-creates multiple categories under parentId.
  • renameCategory({ categoryId, name }): Promise<CategoryNode>: Updates a category name via PATCH /api/category/:id/name. Protects root category from being renamed.
  • reorderCategories({ parentId, categoryIds }): Promise<number[]>: Updates sibling display ordering via POST /api/category/:parentId/reorder.
  • moveCategory({ categoryId, parentId }): Promise<object>: Moves a category under a new parent via PATCH /api/category/:id/move.
  • deleteCategory(categoryId): Promise<number|string>: Deletes a category via DELETE /api/category/:id and removes it from state.
  • toggleCategoryPos({ categoryId, pos }): Promise<CategoryNode>: Toggles POS visibility via PATCH /api/category/:id/pos.

Article Mutations within Categories

  • createArticle({ parentId, name }): Promise<object>: Dispatches POST /api/article and appends the new article to the active category view automat's articles list 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 to POST /api/category/:id/image and patches fullHash, thumbHash, thumbW, and thumbH in the category node.
  • deleteCategoryImage(categoryId): Promise<object>: Deletes the category image via DELETE /api/category/:id/image.

Hardware POS Terminals

  • patchCategoryPosterminalIds({ categoryId, terminalId, enabled }): void: Updates the posterminalIds array 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 by sort.
  • 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

  1. Root category cannot be renamed or deleted: The backend returns HTTP 400 if you attempt to rename or delete the company root category.
  2. Reverse index updates: When creating articles with categoryAutomat.actions.createArticle, any active route automat matching the parent ID is updated immediately.
  3. Use normalized helpers: Always use getCategoryChildren(categories, parentId) rather than manual filtering; it ensures correct sort order.