@webx-ui/module-catalog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webx-ui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @webx-ui/module-catalog
2
+
3
+ The front end of the catalogue of the [WebX UI](https://github.com/webx-ui/webx-ui) admin panel:
4
+ products with their gallery, a tree of categories with the filters each shows, and «Deleted».
5
+
6
+ The other half is the Composer package `webx-ui/module-catalog`, which owns the products, the
7
+ categories, their addresses, the search engine and the API. The section appears in the panel when
8
+ both halves are installed.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @webx-ui/module-catalog
14
+ ```
15
+
16
+ ```ts
17
+ import { createAdmin } from '@webx-ui/module-admin'
18
+ import { catalog } from '@webx-ui/module-catalog'
19
+ import '@webx-ui/module-catalog/style.css'
20
+
21
+ createAdmin({
22
+ basePath: '/cms',
23
+ modules: [catalog()],
24
+ }).mount()
25
+ ```
26
+
27
+ `catalog()` is one section — the server registers one module, `catalog` — which opens on the
28
+ products; the tree of categories and «Deleted» are reached from its head. The satellites
29
+ (`module-catalog-*`) stand beside it in the `catalog` group of the navigation.
30
+
31
+ Keep the default path (`/catalog`): the server's refusal of a taken article number links to
32
+ `{panel}/catalog/products/{id}`.
33
+
34
+ ## Screens
35
+
36
+ | Route | Screen |
37
+ | -------------------------- | -------------------------------------------------------------------------- |
38
+ | `/catalog/products` | the list: search, views, facets from `/facets`, satellites' columns, picks |
39
+ | `/catalog/products/{id}` | `catalog.product-form` — a described screen, patched by satellites |
40
+ | `/catalog/categories` | the tree with the number of products in each branch, dragged into place |
41
+ | `/catalog/categories/{id}` | `catalog.category-form`, with the «Filters» tab |
42
+ | `/catalog/deleted` | «Deleted», products and categories, each with «Restore» |
43
+
44
+ The list keeps what it is looking at in the address — `view`, `q`, `sort`, `page`, and a
45
+ parameter per facet: `f.category=2,5`, `f.price=100-500`, `f.in-stock=1`.
46
+
47
+ ## Screen node types
48
+
49
+ | Type | What it is |
50
+ | --------------------- | ----------------------------------------------------------------------------- |
51
+ | `wx-catalog-category` | a category from the tree; `props.multiple` for several |
52
+ | `wx-catalog-facets` | a category's own facet setting, or where it inherits one from |
53
+ | `wx-catalog-gallery` | the product's pictures: added, ordered and captioned by requests of their own |
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,20 @@
1
+ import { BulkSelection } from './types';
2
+ /**
3
+ * The actions over what is picked in the list (§11.4), and the run they start.
4
+ *
5
+ * Which actions there are is the server's answer — the core's and every satellite's, only the
6
+ * ones this administrator may start. A small selection is done inside the request and answered at
7
+ * once; a large one is a run in the background, polled here with its progress, and its refusals
8
+ * are listed by product when it ends. The list reloads either way, and `finished` carries the ids
9
+ * that refused — what the list leaves picked, so they can be put right and tried again.
10
+ */
11
+ type __VLS_Props = {
12
+ count: number;
13
+ selection: BulkSelection;
14
+ };
15
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
16
+ finished: (refused: number[]) => any;
17
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
18
+ onFinished?: ((refused: number[]) => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
20
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { BulkActionInfo } from './types';
2
+ /**
3
+ * What a bulk action asks for before it runs — a category, a label — and the last word before it
4
+ * does: how many products it is about to touch. The fields are drawn from what the server said
5
+ * about the action, so a satellite's action gets its dialog without this file knowing it.
6
+ */
7
+ type __VLS_Props = {
8
+ action: BulkActionInfo;
9
+ count: number;
10
+ };
11
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
12
+ export default _default;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The shelves of the catalogue: the whole tree at once, with how many products each holds
3
+ * (§6.1), dragged into place.
4
+ *
5
+ * Whole, not a level at a time as the pages are: a catalogue has dozens of categories, not
6
+ * thousands, and the counts are one query for all of them on the server. A drag moves a category
7
+ * and everything under it; the addresses do not move with it — they are flat (decision 23) — so
8
+ * no drop is ever questioned, unlike a page's.
9
+ *
10
+ * Publication is here as well as on the form, because taking a branch off the site is a thing
11
+ * done to the tree: an unpublished category hides everything below it (§6.3), and the rows under
12
+ * it say so.
13
+ *
14
+ * A search puts the tree away and lists the matches flat — a branch drawn for one match deep
15
+ * inside it tells the reader nothing they asked about.
16
+ */
17
+ type __VLS_Props = {
18
+ base?: string;
19
+ };
20
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
+ base: string;
22
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
23
+ export default _default;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A new category: its name and where it hangs. Created unpublished — a shelf nobody filled is
3
+ * not one to show visitors — at the end of its parent's children; the tree is where it is dragged
4
+ * into place. The address is made of the name, and a taken one is refused under the name.
5
+ */
6
+ type __VLS_Props = {
7
+ parent?: number | null;
8
+ };
9
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
10
+ parent: number | null;
11
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
12
+ field: HTMLDivElement;
13
+ }, any>;
14
+ export default _default;
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ base?: string;
3
+ };
4
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
5
+ base: string;
6
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
7
+ export default _default;
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ multiple?: boolean;
3
+ };
4
+ type __VLS_PublicProps = {
5
+ modelValue?: number | number[] | null;
6
+ } & __VLS_Props;
7
+ declare const _default: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
8
+ "update:modelValue": (value: number | number[] | null) => any;
9
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
10
+ "onUpdate:modelValue"?: ((value: number | number[] | null) => any) | undefined;
11
+ }>, {
12
+ multiple: boolean;
13
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
14
+ export default _default;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * «Deleted» (§5, §6.3): what was deleted, and the one thing that can be done to it — bring it
3
+ * back. There is no "delete for ever": orders will point at products, and their links have to
4
+ * keep answering. No filters either; a search finds what somebody is looking for, and the list
5
+ * is newest first, which is where a regret usually is.
6
+ *
7
+ * Products and categories are two tabs of one screen rather than two screens, because the
8
+ * question a reader comes with is "where did it go", not which kind of thing it was.
9
+ *
10
+ * A row says what a restore will do to it before it is done: a product whose category is
11
+ * deleted too comes back without one and unpublished, and that is worth knowing first.
12
+ */
13
+ type __VLS_Props = {
14
+ base?: string;
15
+ };
16
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
+ base: string;
18
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
19
+ export default _default;
@@ -0,0 +1,10 @@
1
+ import { FacetSetting } from './types';
2
+ type __VLS_PublicProps = {
3
+ modelValue?: FacetSetting[] | null;
4
+ };
5
+ declare const _default: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
6
+ "update:modelValue": (value: FacetSetting[] | null) => any;
7
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
8
+ "onUpdate:modelValue"?: ((value: FacetSetting[] | null) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
10
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, HTMLDivElement>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {
2
+ field: HTMLDivElement;
3
+ }, any>;
4
+ export default _default;
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ base?: string;
3
+ };
4
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
5
+ base: string;
6
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
7
+ export default _default;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The products of the catalogue (§11.1): a page of rows, the views of decision 3 over it, the
3
+ * facets as filters behind the funnel, and a checkbox per row.
4
+ *
5
+ * The checkboxes are for the bulk actions (§11.4): a selection outlives the page it was made on,
6
+ * which is what "select these twelve across three pages" needs, and "everything found" picks the
7
+ * filter itself rather than its ids. The actions and their progress are `BulkBar`'s.
8
+ *
9
+ * What the list shows beyond the core's columns is the satellites' (`ProductColumns`, §7.4): the
10
+ * server says which, and each row carries its values under their keys.
11
+ */
12
+ type __VLS_Props = {
13
+ base?: string;
14
+ };
15
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
16
+ base: string;
17
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
18
+ root: HTMLDivElement;
19
+ }, HTMLDivElement>;
20
+ export default _default;
package/dist/api.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { AdminContext } from '@webx-ui/module-admin';
2
+ import { Paginated } from '@webx-ui/core';
3
+ import { ScreenModel } from '@webx-ui/schema';
4
+ import { BulkActionInfo, BulkRun, BulkSelection, CategoryDetail, CategoryNode, CategoryRow, DeletedCategory, DeletedKind, DeletedProduct, FacetRegistryAnswer, ProductDetail, ProductImage, ProductQuery, ProductRow, ProductsPage } from './types';
5
+ export interface CatalogApi {
6
+ products(query?: ProductQuery): Promise<ProductsPage>;
7
+ product(id: number): Promise<ProductDetail>;
8
+ /** The values of `catalog.product-form`; a refused field comes back as a 422 under its name. */
9
+ createProduct(values: ScreenModel): Promise<ProductDetail>;
10
+ saveProduct(id: number, values: ScreenModel): Promise<ProductDetail>;
11
+ removeProduct(id: number): Promise<void>;
12
+ restoreProduct(id: number): Promise<ProductRow>;
13
+ /** A file off the disk, with how far it has got; or an address the server fetches itself. */
14
+ addImage(id: number, source: File | string, onProgress?: (percent: number) => void): Promise<ProductImage>;
15
+ /** The whole gallery in its order, with every picture's words (§11.2). */
16
+ saveImages(id: number, images: Pick<ProductImage, 'id' | 'alt' | 'title'>[]): Promise<ProductImage[]>;
17
+ removeImage(id: number, image: number): Promise<void>;
18
+ categories(): Promise<CategoryNode[]>;
19
+ category(id: number): Promise<CategoryDetail>;
20
+ createCategory(values: ScreenModel, parent: number | null): Promise<CategoryDetail>;
21
+ saveCategory(id: number, values: ScreenModel): Promise<CategoryDetail>;
22
+ /** Under `parent` (null — the top), before `before` (null — last). Answers with the whole tree. */
23
+ moveCategory(id: number, parent: number | null, before: number | null): Promise<CategoryNode[]>;
24
+ removeCategory(id: number): Promise<void>;
25
+ restoreCategory(id: number): Promise<CategoryRow>;
26
+ /** The registry of facets, with the sorts the list is offered. */
27
+ facets(): Promise<FacetRegistryAnswer>;
28
+ /** The bulk actions this administrator may start, with what each asks for (§11.4). */
29
+ bulkActions(): Promise<BulkActionInfo[]>;
30
+ /** Done at once when small (`id` null in the answer), queued otherwise. */
31
+ startBulk(action: string, params: Record<string, unknown>, selection: BulkSelection): Promise<BulkRun>;
32
+ bulkRun(id: number): Promise<BulkRun>;
33
+ deleted(kind: 'products', query?: {
34
+ q?: string;
35
+ page?: number;
36
+ per_page?: number;
37
+ }): Promise<Paginated<DeletedProduct>>;
38
+ deleted(kind: 'categories', query?: {
39
+ q?: string;
40
+ page?: number;
41
+ per_page?: number;
42
+ }): Promise<Paginated<DeletedCategory>>;
43
+ deleted(kind: DeletedKind, query?: {
44
+ q?: string;
45
+ page?: number;
46
+ per_page?: number;
47
+ }): Promise<Paginated<DeletedProduct | DeletedCategory>>;
48
+ }
49
+ /** The path of the catalogue under the panel's API. */
50
+ export declare const CATALOG_API = "catalog";
51
+ /**
52
+ * The list's query as Laravel reads nested parameters: `facets[brand][]=3`,
53
+ * `facets[price][min]=100`. Every facet's choice is a list, a toggle's too — on is
54
+ * `facets[in-stock][]=1`, the value the storefront's own links send; a bare `facets[in-stock]=1`
55
+ * is refused. Empty choices are left out, so a filter that was opened and closed again asks for
56
+ * nothing.
57
+ */
58
+ export declare function productSearch(query: ProductQuery): URLSearchParams;
59
+ export declare function createCatalogApi(admin: AdminContext): CatalogApi;
@@ -0,0 +1,41 @@
1
+ import { InjectionKey, Ref } from 'vue';
2
+ import { CategoryRow, ProductImage, ProductRow } from './types';
3
+ /**
4
+ * What the product editor knows and the nodes of `catalog.product-form` do not.
5
+ *
6
+ * The gallery needs it most: its pictures are not a value of the form but records of their own,
7
+ * added, ordered and captioned by requests of their own (§11.2), so the node has to know which
8
+ * product it is attached to and whether this administrator may write.
9
+ */
10
+ export interface ProductEditorContext {
11
+ /** The product as the server last answered it, or `null` while the first request is out. */
12
+ product: Ref<ProductRow | null>;
13
+ /** The gallery in its order. The editor reads it with the product; the node keeps it. */
14
+ images: Ref<ProductImage[]>;
15
+ /** Closed for writing: no permission, or a save in flight. */
16
+ locked: Ref<boolean>;
17
+ }
18
+ export declare const productEditorKey: InjectionKey<ProductEditorContext>;
19
+ export declare function provideProductEditor(editor: ProductEditorContext): void;
20
+ /** The editor above this node, or `null` outside one — a demo or a test draws a placeholder. */
21
+ export declare function useProductEditor(): ProductEditorContext | null;
22
+ /**
23
+ * What the category editor knows and `wx-catalog-facets` needs: which category it is, so that the
24
+ * field can say whose setting it inherits — the parent's, the grandparent's, or nobody's (§6.2).
25
+ */
26
+ export interface CategoryEditorContext {
27
+ category: Ref<CategoryRow | null>;
28
+ /** Closed for writing — what the screen's form says to its own controls, for the ones it cannot. */
29
+ locked: Ref<boolean>;
30
+ /**
31
+ * The server's answer to "whose facet setting does it show" (`facets_from`): the ancestor, `null`
32
+ * for its own or the default, `undefined` when the server did not say.
33
+ */
34
+ facetsFrom: Ref<{
35
+ id: number;
36
+ name: string;
37
+ } | null | undefined>;
38
+ }
39
+ export declare const categoryEditorKey: InjectionKey<CategoryEditorContext>;
40
+ export declare function provideCatalogCategoryEditor(editor: CategoryEditorContext): void;
41
+ export declare function useCatalogCategoryEditor(): CategoryEditorContext | null;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The first refusal of a field, whether the server named the field (`name`) or one language of it
3
+ * (`name.en`). Matched on the name and a dot, not on the first dot of the key: names with a dot in
4
+ * them exist (`stock.status`), and `name.en` must not be read as the field `name.en`.
5
+ */
6
+ export declare function firstError(errors: Record<string, string[]>, field: string): string | undefined;
@@ -0,0 +1,18 @@
1
+ import { LocationQuery } from 'vue-router';
2
+ import { FacetChoice, FacetInfo } from './types';
3
+ /**
4
+ * The list's facet filters live in the address, one parameter per facet — `f.<key>` — so that
5
+ * coming back from a product lands on the same filter, and a filtered list can be linked to
6
+ * (the tree's "Show its products" does exactly that):
7
+ *
8
+ * f.category=12,14 values of a terms or tree facet
9
+ * f.price=100-500 a range; either end may be empty
10
+ * f.in-stock=1 a toggle
11
+ *
12
+ * The server's own format is another (`facets[key][]=…`, see `productSearch()`); this one is for
13
+ * a person reading the address bar.
14
+ */
15
+ export declare const FACET_PREFIX = "f.";
16
+ export declare function readFacets(query: LocationQuery, facets: FacetInfo[]): Record<string, FacetChoice>;
17
+ /** One facet's choice as its parameter; `undefined` takes the parameter away. */
18
+ export declare function writeFacet(choice: FacetChoice | null | undefined): string | undefined;
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Puts this package's English under the panel's dictionary, once per panel. */
2
+ export declare function useCatalogMessages(): void;
@@ -0,0 +1,16 @@
1
+ export { catalog, type CatalogOptions } from './module';
2
+ export { createCatalogApi, productSearch, CATALOG_API, type CatalogApi } from './api';
3
+ export { catalogMessages } from './messages';
4
+ export { provideProductEditor, productEditorKey, useProductEditor, provideCatalogCategoryEditor, categoryEditorKey, useCatalogCategoryEditor, type ProductEditorContext, type CategoryEditorContext, } from './editor';
5
+ export { useCategoryTree, useFacetRegistry, type CategoryTree, type FacetRegistry } from './store';
6
+ export { default as WxCatalogProductsPage } from './ProductsPage';
7
+ export { default as WxCatalogProductEditor } from './ProductEditorPage';
8
+ export { default as WxCatalogProductCreateDialog } from './ProductCreateDialog';
9
+ export { default as WxCatalogCategoriesPage } from './CategoriesPage';
10
+ export { default as WxCatalogCategoryEditor } from './CategoryEditorPage';
11
+ export { default as WxCatalogCategoryCreateDialog } from './CategoryCreateDialog';
12
+ export { default as WxCatalogDeletedPage } from './DeletedPage';
13
+ export { default as WxCatalogCategoryField } from './CategoryField';
14
+ export { default as WxCatalogFacetsField } from './FacetsField';
15
+ export { default as WxCatalogGallery } from './GalleryField';
16
+ export type { CategoryDetail, CategoryNode, CategoryRef, CategoryRow, DeletedCategory, DeletedKind, DeletedProduct, FacetChoice, FacetInfo, FacetKind, FacetSetting, ProductColumnInfo, ProductDetail, ProductImage, ProductQuery, ProductRow, ProductSort, ProductState, ProductsPage, SkuHolder, } from './types';