Skip to content

feat(categories): nest product and catalog categories up to three levels - #156

Merged
roncodes merged 5 commits into
release/v0.4.26from
feat/nested-categories
Oct 16, 2026
Merged

roncodes merged 5 commits into
release/v0.4.26from
feat/nested-categories

Conversation

@roncodes

@roncodes roncodes commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Product categories and catalog (menu) categories nest up to three levels, for example Drinks › Coffee › Cold brew. Opening a parent shows its own products plus those of every category under it. The customer app change is fleetbase/storefront-app#82.

Requires fleetbase/core-api#316 (core-api 1.6.71): the internal Category resource sends parent_uuid, which the console builds its tree from. composer.json now requires ^1.6.71.

Server

  • Rules: a category's parent must be in the same store or catalog, and of the same kind. No loops, and at most three levels.
    • New Support/CategoryTree (descendants, depth, height, whole-tree validation, top-down ordering).
    • New CategoryTreeObserver on the core Category and on CatalogCategory: it refuses a bad parent with a 422 and moves a removed category's subcategories up a level.
  • Public categories (v1/categories):
    • each category carries parent (public id) and depth, from one query per store;
    • with_subcategories now really lists the categories under each one (it wrapped toArray() rows before);
    • with_products&rollup=1 brings every product under a category.
  • Product rollup: public products?category=, plus the console's category and category_slug filters, include subcategories' products. category_direct=1 keeps an exact match. Product's API cache now depends on category, so moving a category invalidates the rolled-up lists.
  • Public products carry category: the category's public id, so the app can group a parent's products by subcategory.
  • Catalogs:
    • setCategories() takes each category's parent by parent_uuid, or by parent_key for a category created in the same save;
    • the whole tree is validated first, then parents are attached top-down;
    • the public catalog payload gives each category's parent.

Console

  • Product categories:
    • the category form gains a parent picker: the store's tree, minus the category itself, its subcategories, and anything past three levels;
    • sidebar rows offer "New subcategory" up to the second level;
    • the category page's header shows the path and the rolled-up product count;
    • the product form names each category option by its path;
    • a refused parent shows the server's error and the form stays open.
  • Bulk move to category: now lists the store's categories in tree order. The modal ignored the categories it was given and showed the network picker instead, so moving products to a category didn't work.
  • Catalog editor:
    • categories show as a tree, with indent, outdent, add subcategory, and move within siblings;
    • removing a category moves its subcategories up;
    • new categories carry a key until saved;
    • a duplicated catalog keeps its tree.

Tests

  • PHP:
    • new server/tests/Unit/Support/NestedCategoriesTest.php covers the tree rules, the observer, public categories (parent, depth, subcategories, rollup) and nested catalog saves;
    • updated the product filter, product API and cache-dependency tests for the rollup.
    • Full suite: 1130 tests, 100% line coverage (12903/12903).
    • One failure: CustomerControllerContractsTest, which needs core-api 1.6.70 for the phone fix. My local vendor copy is 1.6.69; CI installs the current version.
  • Ember:
    • new tests/unit/utils/category-tree-test.js;
    • the sidebar nesting and subcategory action;
    • the category modal's parent picker;
    • the bulk-move tree select;
    • the catalog editor's tree actions, removal and duplicate.
    • All of those modules pass (30 tests). The test browser hit its intermittent 120-second boot timeout on two runs; reruns passed.
  • eslint, ember-template-lint, stylelint, fleetbase-intl-lint and php-cs-fixer: clean.

Not checked in the browser: the console dev server wasn't running locally.

API and docs

  • postman:
    • Categories: parent, depth, with_subcategories, rollup;
    • Products: category= includes subcategories, category_direct, and the product's category;
    • Catalogs: each category's parent.
  • fleetbase.io: storefront/catalog/categories.mdx (nesting, the three-level limit, rollup) and the food-truck menu docs.

Labelled needs-api-spec and needs-docs.

Merge order

This branch includes #155 (the catalog-category edit modal), merged in with the conflicts resolved: a new subcategory opens in #155's modal to be named, and a duplicated catalog keeps both its tree and each category's description and icon. Merge #155 first, then this PR; its diff then shrinks to the nesting changes.

…els (server)

Product categories and catalog (menu) categories nest: Drinks › Coffee ›
Cold brew. A parent is in the same store or catalog and of the same kind, and
a tree is at most three levels deep; CategoryTreeObserver refuses anything
else with a 422 and moves a removed category's subcategories up a level.

- Public categories carry parent (public id) and depth; with_subcategories
  lists the categories under each one; with_products&rollup=1 brings every
  product under a category.
- Products filtered by a category (public category=, console category and
  category_slug) include its subcategories' products; category_direct=1 keeps
  an exact match. Public products carry their category's public id.
- A catalog saves its categories as a tree (parent_uuid, or parent_key for a
  category created in the same save), validated whole and attached top-down;
  the public catalog payload gives each category's parent.
…n a modal

The catalog editor's pencil button only renamed a category inline, so a menu
category could not get an icon. It now opens the product category modal on the
catalog category (name, description, icon upload). Changes stay on the page
until the catalog is saved, like every other editor change; cancel restores
them. A set icon shows in the category row, and Duplicate copies each
category's description and icon.

Catalog::setCategories() now writes description, icon, icon_color and
icon_file_uuid when the payload carries them (left alone when it does not),
and the catalog category resource returns icon and icon_color, plus
icon_file_uuid on internal requests, so the editor's save round trip keeps them.
- Product categories: the category form has a parent picker (the store's
  tree, minus the category itself, its subcategories and anything past three
  levels); the sidebar's rows offer New subcategory up to the second level;
  the category page's header shows its path and rolled-up product count; the
  product form names each category option by its path.
- Bulk move to category now shows the store's categories in tree order (the
  modal ignored the categories it was given and showed the network picker).
- Catalog editor: categories show as a tree with indent, outdent, add
  subcategory and move within siblings; removing one moves its subcategories
  up; new categories carry a key so a subcategory can sit under one before it
  is saved; a duplicated catalog keeps its tree.
- A refused parent (the server's 422) is shown and the form stays open.
The console builds the category tree and the parent picker from parent_uuid,
which core's internal Category resource sends from 1.6.71 (core-api#316).
Without it a nested category would read as top-level and be saved as one.
@roncodes roncodes added needs-docs Requires documentation updates needs-api-spec Requires API specification updates type:feature Feature or enhancement labels Oct 15, 2026
The catalog editor's category edit modal (#155) replaces the inline rename;
a new subcategory now opens in it to be named, and duplicating a catalog keeps
both the tree and each category's description and icon.
@roncodes
roncodes merged commit 0583f0f into release/v0.4.26 Oct 16, 2026
@roncodes
roncodes deleted the feat/nested-categories branch October 16, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-api-spec Requires API specification updates needs-docs Requires documentation updates type:feature Feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant