@stapel/categories-react 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/CHANGELOG.md +1 -0
- package/MODULE.md +194 -0
- package/README.md +191 -0
- package/dist/api/categoriesApi.d.ts +121 -0
- package/dist/api/categoriesApi.d.ts.map +1 -0
- package/dist/api/categoriesApi.js +48 -0
- package/dist/api/categoriesApi.js.map +1 -0
- package/dist/api/generated/schema.d.ts +2321 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +92 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/catalog/labels.d.ts +103 -0
- package/dist/catalog/labels.d.ts.map +1 -0
- package/dist/catalog/labels.js +56 -0
- package/dist/catalog/labels.js.map +1 -0
- package/dist/catalog/pks.d.ts +25 -0
- package/dist/catalog/pks.d.ts.map +1 -0
- package/dist/catalog/pks.js +38 -0
- package/dist/catalog/pks.js.map +1 -0
- package/dist/catalog/sync.d.ts +103 -0
- package/dist/catalog/sync.d.ts.map +1 -0
- package/dist/catalog/sync.js +114 -0
- package/dist/catalog/sync.js.map +1 -0
- package/dist/catalog/tree.d.ts +119 -0
- package/dist/catalog/tree.d.ts.map +1 -0
- package/dist/catalog/tree.js +166 -0
- package/dist/catalog/tree.js.map +1 -0
- package/dist/default/CatalogPage.d.ts +16 -0
- package/dist/default/CatalogPage.d.ts.map +1 -0
- package/dist/default/CatalogPage.js +13 -0
- package/dist/default/CatalogPage.js.map +1 -0
- package/dist/default/CategoryBreadcrumbsBar.d.ts +20 -0
- package/dist/default/CategoryBreadcrumbsBar.d.ts.map +1 -0
- package/dist/default/CategoryBreadcrumbsBar.js +35 -0
- package/dist/default/CategoryBreadcrumbsBar.js.map +1 -0
- package/dist/default/CategoryCarouselStrip.d.ts +22 -0
- package/dist/default/CategoryCarouselStrip.d.ts.map +1 -0
- package/dist/default/CategoryCarouselStrip.js +24 -0
- package/dist/default/CategoryCarouselStrip.js.map +1 -0
- package/dist/default/CategoryFeatureList.d.ts +23 -0
- package/dist/default/CategoryFeatureList.d.ts.map +1 -0
- package/dist/default/CategoryFeatureList.js +28 -0
- package/dist/default/CategoryFeatureList.js.map +1 -0
- package/dist/default/CategoryPage.d.ts +43 -0
- package/dist/default/CategoryPage.d.ts.map +1 -0
- package/dist/default/CategoryPage.js +21 -0
- package/dist/default/CategoryPage.js.map +1 -0
- package/dist/default/CategoryPickerField.d.ts +26 -0
- package/dist/default/CategoryPickerField.d.ts.map +1 -0
- package/dist/default/CategoryPickerField.js +40 -0
- package/dist/default/CategoryPickerField.js.map +1 -0
- package/dist/default/CategoryTreePane.d.ts +28 -0
- package/dist/default/CategoryTreePane.d.ts.map +1 -0
- package/dist/default/CategoryTreePane.js +25 -0
- package/dist/default/CategoryTreePane.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +9 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +27 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/index.d.ts +37 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +28 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +25 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +40 -0
- package/dist/default/theme.js.map +1 -0
- package/dist/default/types.d.ts +12 -0
- package/dist/default/types.d.ts.map +1 -0
- package/dist/default/types.js +2 -0
- package/dist/default/types.js.map +1 -0
- package/dist/headless/CategoriesProvider.d.ts +19 -0
- package/dist/headless/CategoriesProvider.d.ts.map +1 -0
- package/dist/headless/CategoriesProvider.js +15 -0
- package/dist/headless/CategoriesProvider.js.map +1 -0
- package/dist/headless/CategoryBreadcrumbs.d.ts +40 -0
- package/dist/headless/CategoryBreadcrumbs.d.ts.map +1 -0
- package/dist/headless/CategoryBreadcrumbs.js +42 -0
- package/dist/headless/CategoryBreadcrumbs.js.map +1 -0
- package/dist/headless/CategoryCarousel.d.ts +48 -0
- package/dist/headless/CategoryCarousel.d.ts.map +1 -0
- package/dist/headless/CategoryCarousel.js +34 -0
- package/dist/headless/CategoryCarousel.js.map +1 -0
- package/dist/headless/CategoryFeatures.d.ts +67 -0
- package/dist/headless/CategoryFeatures.d.ts.map +1 -0
- package/dist/headless/CategoryFeatures.js +46 -0
- package/dist/headless/CategoryFeatures.js.map +1 -0
- package/dist/headless/CategoryPicker.d.ts +69 -0
- package/dist/headless/CategoryPicker.d.ts.map +1 -0
- package/dist/headless/CategoryPicker.js +86 -0
- package/dist/headless/CategoryPicker.js.map +1 -0
- package/dist/headless/CategoryTree.d.ts +55 -0
- package/dist/headless/CategoryTree.d.ts.map +1 -0
- package/dist/headless/CategoryTree.js +51 -0
- package/dist/headless/CategoryTree.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +22 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +31 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +25 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +74 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +407 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +207 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +89 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +134 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +41 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +90 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/model/catalogStore.d.ts +23 -0
- package/dist/model/catalogStore.d.ts.map +1 -0
- package/dist/model/catalogStore.js +85 -0
- package/dist/model/catalogStore.js.map +1 -0
- package/dist/model/catalogSync.d.ts +49 -0
- package/dist/model/catalogSync.d.ts.map +1 -0
- package/dist/model/catalogSync.js +64 -0
- package/dist/model/catalogSync.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/queries.d.ts +124 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +147 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +30 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +16 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +31 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +6 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/nav/manifest.d.ts +31 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +37 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +125 -0
- package/manifest.json +918 -0
- package/nav-manifest.json +44 -0
- package/package.json +119 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/categoriesApi.ts +179 -0
- package/src/api/generated/schema.ts +2323 -0
- package/src/api/types.ts +99 -0
- package/src/catalog/labels.ts +129 -0
- package/src/catalog/pks.ts +35 -0
- package/src/catalog/sync.ts +169 -0
- package/src/catalog/tree.ts +242 -0
- package/src/default/CatalogPage.tsx +40 -0
- package/src/default/CategoryBreadcrumbsBar.tsx +93 -0
- package/src/default/CategoryCarouselStrip.tsx +102 -0
- package/src/default/CategoryFeatureList.tsx +125 -0
- package/src/default/CategoryPage.tsx +114 -0
- package/src/default/CategoryPickerField.tsx +173 -0
- package/src/default/CategoryTreePane.tsx +134 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/index.ts +36 -0
- package/src/default/theme.tsx +67 -0
- package/src/default/types.ts +12 -0
- package/src/headless/CategoriesProvider.tsx +20 -0
- package/src/headless/CategoryBreadcrumbs.tsx +78 -0
- package/src/headless/CategoryCarousel.tsx +80 -0
- package/src/headless/CategoryFeatures.tsx +100 -0
- package/src/headless/CategoryPicker.tsx +173 -0
- package/src/headless/CategoryTree.tsx +115 -0
- package/src/i18n/errorsMap.ts +46 -0
- package/src/i18n/es.ts +92 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +238 -0
- package/src/i18n/generated/errors.json +495 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +156 -0
- package/src/i18n/ru.ts +110 -0
- package/src/index.ts +191 -0
- package/src/model/catalogStore.ts +102 -0
- package/src/model/catalogSync.ts +115 -0
- package/src/model/context.tsx +28 -0
- package/src/model/queries.ts +226 -0
- package/src/model/queryKeys.ts +50 -0
- package/src/model/runtime.ts +39 -0
- package/src/nav/manifest.ts +66 -0
- package/tsconfig.json +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @stapel/categories-react
|
package/MODULE.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @stapel/categories-react — module guide
|
|
2
|
+
|
|
3
|
+
Pairs with **stapel-categories 0.6.0** (`>=0.6 <0.7`), 23 paths under
|
|
4
|
+
`/categories/api/v1/`. Contract sources: the module's own
|
|
5
|
+
`docs/{schema,errors,flows}.json`, pinned in `contract-pins.json` and
|
|
6
|
+
regenerated under `pnpm gen:check`.
|
|
7
|
+
|
|
8
|
+
## Layers
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/api/ categoriesApi.ts the five public reads; the one home of path strings
|
|
12
|
+
types.ts wire aliases, and the one hand-mirrored field
|
|
13
|
+
generated/ openapi-typescript, drift-gated
|
|
14
|
+
src/catalog/ pks.ts treenode's comma-joined pk columns
|
|
15
|
+
tree.ts flat rows → tree, slug → node, breadcrumbs
|
|
16
|
+
sync.ts the delta protocol as pure transitions
|
|
17
|
+
labels.ts key-or-literal, per row
|
|
18
|
+
src/model/ runtime · context · queryKeys · catalogStore · catalogSync · queries
|
|
19
|
+
src/headless/ CategoriesProvider · CategoryTree · CategoryBreadcrumbs
|
|
20
|
+
CategoryCarousel · CategoryPicker · CategoryFeatures
|
|
21
|
+
src/default/ the antd skin, `./default` subpath
|
|
22
|
+
src/i18n/ keys · ru · es · errorsMap + generated/
|
|
23
|
+
src/nav/ manifest.ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`catalog/` is pure: no React, no fetch, no storage. That is what lets the tree
|
|
27
|
+
assembly and the whole delta protocol be tested without a DOM, and what lets an
|
|
28
|
+
SSR render call them directly.
|
|
29
|
+
|
|
30
|
+
There is **no `flows/` layer**: `stapel-categories` annotates no `@flow_step`
|
|
31
|
+
and its `docs/flows.json` is `[]`, so the pair is not wired into `gen:flows`
|
|
32
|
+
(spec §8.1 — a flows layer only where a multi-step flow exists).
|
|
33
|
+
|
|
34
|
+
## The three server facts this package exists to absorb
|
|
35
|
+
|
|
36
|
+
### 1. There is no tree endpoint
|
|
37
|
+
|
|
38
|
+
Three shapes could serve a catalogue, and the right one for a storefront is the
|
|
39
|
+
paginated list: `GET /categories/` with `RevisionPagination`, envelope
|
|
40
|
+
`{pagination, revisions, results}`, rows ordered by `revision`. Each row
|
|
41
|
+
carries `tn_parent`, `tn_priority`, `tn_ancestors_pks`, `tn_children_pks`.
|
|
42
|
+
|
|
43
|
+
The ancestry columns are django-treenode `TextField`s holding **comma-joined
|
|
44
|
+
pks** (`treenode/utils.py`: `PKS_SEPARATOR = ","`), typed `string` in the
|
|
45
|
+
schema, `""` for a root. `parseTreenodePks` exists because the storefront spec
|
|
46
|
+
described them as arrays; a `JSON.parse` there fails, and a naive
|
|
47
|
+
`Number(column)` yields `NaN`, which fails every `===` against a real id and
|
|
48
|
+
therefore reads as "the parent is missing".
|
|
49
|
+
|
|
50
|
+
`buildCategoryTree` filters two flags the endpoint does not:
|
|
51
|
+
|
|
52
|
+
- `deleted` — the list's `include_deleted` defaults to **true**, and the delta
|
|
53
|
+
protocol needs it that way.
|
|
54
|
+
- `active` — filtered server-side only by `/carousel/`. It is the storefront's
|
|
55
|
+
visibility switch, so the public tree drops inactive rows by default and
|
|
56
|
+
keeping them is an explicit option.
|
|
57
|
+
|
|
58
|
+
An unreachable row (a parent cycle in authored data) is promoted to a root
|
|
59
|
+
rather than dropped: a broken branch must be visible, not absent.
|
|
60
|
+
|
|
61
|
+
### 2. There is no slug lookup
|
|
62
|
+
|
|
63
|
+
`lookup_field` is never overridden and the list has no slug filter. `/c/:slug`
|
|
64
|
+
resolves against the synced tree. `Category.slug` is `unique=True` on the
|
|
65
|
+
model, so a collision cannot come from the database — it can only come from a
|
|
66
|
+
stale row in the client's own snapshot, and first-in-display-order wins
|
|
67
|
+
deterministically.
|
|
68
|
+
|
|
69
|
+
### 3. Names are translation keys
|
|
70
|
+
|
|
71
|
+
Covered in the README and in `catalog/labels.ts` at length. The short version:
|
|
72
|
+
`translate()` runs the `DISPLAY_TRANSLATOR` seam, its default is the identity
|
|
73
|
+
function, and **no serializer calls it** — the only two call sites are
|
|
74
|
+
`Category.__str__` and the admin's memoized feature label. `translatable`
|
|
75
|
+
(category) and `translate` (feature) say per row whether a string is a key.
|
|
76
|
+
|
|
77
|
+
`featureOptionsAreKeys` carries the third case, which is the one that bites
|
|
78
|
+
both ways: option labels are keys only under `translate: "all"` and only while
|
|
79
|
+
`config.translatable_options` is not false. Translating an opted-out option
|
|
80
|
+
shows the raw key; not translating an opted-in one shows the raw key too.
|
|
81
|
+
|
|
82
|
+
## The delta protocol, and the two rules the docs omit
|
|
83
|
+
|
|
84
|
+
The module's viewset docstring gives four steps: full GET, store
|
|
85
|
+
`revisions.global_max`, `?min_revision=`, drop `deleted: true`. Implemented
|
|
86
|
+
literally, that protocol has two holes.
|
|
87
|
+
|
|
88
|
+
**`deleted_ids` beats scanning rows.** `revisions.deleted_ids` is computed as
|
|
89
|
+
`filter(deleted=True, revision__gt=min_revision)` — unpaginated, over the whole
|
|
90
|
+
table, and reflecting the CURRENT flag (so a category deleted and then restored
|
|
91
|
+
is simply absent from it). The `deleted: true` rows, by contrast, are
|
|
92
|
+
paginated: a walk that stops early misses one. Both channels are applied;
|
|
93
|
+
`deleted_ids` is the complete one. On a full sync it is `[]` by construction —
|
|
94
|
+
the server only computes it when `min_revision` was sent — which is why
|
|
95
|
+
dropping `deleted: true` rows still matters.
|
|
96
|
+
|
|
97
|
+
**A multi-page walk must pin its window.** `paginate_queryset` filters at
|
|
98
|
+
request time and orders by `revision`; `global_max` is `Max(revision)` over the
|
|
99
|
+
table at response time. A write landing between two pages shifts every later
|
|
100
|
+
boundary. `max_revision` exists for exactly this and the documented flow never
|
|
101
|
+
mentions it: `nextPageRequest` pins the window to the first page's
|
|
102
|
+
`global_max`, and `applyCategoryPage`'s `cursorLimit` stops the cursor
|
|
103
|
+
recording anything past it. Whatever was written during the walk is picked up
|
|
104
|
+
by the next delta.
|
|
105
|
+
|
|
106
|
+
Beyond that, two loop guards:
|
|
107
|
+
|
|
108
|
+
- a **page budget** (`MAX_SYNC_PAGES`, 1000 — a million categories at the
|
|
109
|
+
server's max page size), because a server-reported `has_next` bug is an
|
|
110
|
+
infinite request loop against production, not a slow render; and
|
|
111
|
+
- **publish-on-completion**: a truncated walk rewinds its cursor to the stored
|
|
112
|
+
one, so the worst case is one repeated full walk rather than a permanently
|
|
113
|
+
incomplete tree.
|
|
114
|
+
|
|
115
|
+
The FULL/DELTA distinction is expressed as *what the accumulator starts from*
|
|
116
|
+
(`EMPTY_SNAPSHOT` vs the stored snapshot), not as a flag on the merge. A flag
|
|
117
|
+
invites both silent failures: a delta treated as full empties the catalogue, a
|
|
118
|
+
full treated as delta resurrects rows deleted while the client was away.
|
|
119
|
+
|
|
120
|
+
## Why the catalogue is app-scoped, not user-scoped
|
|
121
|
+
|
|
122
|
+
`createRepository` is the only sanctioned client-side persistence primitive,
|
|
123
|
+
and `scope` is not a size decision here. A user-scoped repository is encrypted
|
|
124
|
+
with the per-session key and wiped at logout with no opt-out — both wrong for a
|
|
125
|
+
category tree, which is public content identical for every visitor and for a
|
|
126
|
+
visitor with no session at all. App scope also means the store never asks for a
|
|
127
|
+
`SessionManager`, so it works on a page with no auth wired.
|
|
128
|
+
|
|
129
|
+
Every read and write is wrapped: a private window, cleared site data or a
|
|
130
|
+
snapshot written by an older shape all degrade to "no snapshot". A cold sync
|
|
131
|
+
costs one request; a cache that throws costs the page.
|
|
132
|
+
|
|
133
|
+
## The bridge to @stapel/attributes-react
|
|
134
|
+
|
|
135
|
+
`GET /categories/{id}/features/` is where the two packages meet.
|
|
136
|
+
`CategoryFeature` **is** attributes-react's `FeatureDef` — not a parallel type
|
|
137
|
+
— so `bag.features` feeds `<FeatureFields>`, `unsupportedTypeGate`,
|
|
138
|
+
`mirrorValidate` and `formatFeatureValue` unmodified. `test/features.test.tsx`
|
|
139
|
+
asserts that end to end: the rows this pair fetches go through
|
|
140
|
+
`unsupportedTypes` and it names the one type no builtin editor covers.
|
|
141
|
+
|
|
142
|
+
This pair adds exactly three decisions on top and no more: whether the name is
|
|
143
|
+
a key, whether the options are keys, and which rows are `show_as_badge` /
|
|
144
|
+
`show_at_title` projections. Value semantics — defaults, validation, formatting
|
|
145
|
+
— stay with the package that owns them.
|
|
146
|
+
|
|
147
|
+
## Notes on the contract, recorded rather than worked around
|
|
148
|
+
|
|
149
|
+
1. **`FeatureConfig`'s discriminator is malformed, and the generated types are
|
|
150
|
+
wrong because of it.** The schema declares
|
|
151
|
+
`discriminator: {propertyName: "type", mapping: {"null": ConvertibleUnitConfig}}`
|
|
152
|
+
— one bogus entry instead of the ten type slugs. openapi-typescript
|
|
153
|
+
therefore (a) strips the discriminator from every use site, emitting
|
|
154
|
+
`Omit<FeatureConfig, "type">`, and (b) re-adds a synthetic one per member,
|
|
155
|
+
so the generated `IntConfig` declares `type: "IntConfig"` where the wire
|
|
156
|
+
sends `"int"`. Feature configs are consequently typed through
|
|
157
|
+
attributes-react's hand-mirrored `FeatureConfig` (itself pinned against the
|
|
158
|
+
engine's generated golden corpus). **Upstream ask**: emit the ten slugs in
|
|
159
|
+
`discriminator.mapping`, and both halves disappear.
|
|
160
|
+
2. **`FeatureCompact` carries no `revision`**, so the `/features/` collection's
|
|
161
|
+
`RevisionPagination` envelope always reports `revisions.min/max: null`. Not
|
|
162
|
+
on this pair's surface (a storefront reads features through a category), but
|
|
163
|
+
it makes the parallel feature tree un-syncable by the same protocol.
|
|
164
|
+
3. **`POST {id}/validate-dto/` reads like a public helper and is not one.** The
|
|
165
|
+
viewset is `ReadOnlyOrStaff`, which allows safe methods to anyone and
|
|
166
|
+
everything else to staff — so this answers 403 to a visitor. It is absent
|
|
167
|
+
from `CategoriesApi` for that reason, not by oversight.
|
|
168
|
+
4. **`include_deleted` is honoured** (`RevisionViewSetMixin.get_queryset`),
|
|
169
|
+
despite being documented on the paginator, which ignores it. Its default is
|
|
170
|
+
`true`.
|
|
171
|
+
5. **No `translations/` directory.** 20 of the 62 registry codes can never come
|
|
172
|
+
from an upstream catalogue. They split by owner: the 42 cross-cutting
|
|
173
|
+
`stapel_core` codes are generated from core's catalogue, the 8
|
|
174
|
+
`stapel_categories` codes are authored in this pair's `./i18n/{ru,es}`, and
|
|
175
|
+
the 12 `stapel_attributes` codes stay with `@stapel/attributes-react`, which
|
|
176
|
+
already translates them. `test/i18n.test.ts` asserts over the UNION of the
|
|
177
|
+
two bundles a host registers, so nobody can "fix" a red test by copying
|
|
178
|
+
another package's twelve keys in and giving one refusal two sentences.
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
|
|
182
|
+
119 in 9 files (115 in `test`, 4 in `test:pack`).
|
|
183
|
+
|
|
184
|
+
| File | What it holds down |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `tree.test.ts` | flat rows → tree; priority order and its tie-break; both filters; orphan promotion; parent cycles; slug resolution incl. collisions; breadcrumbs cross-checked against the server's own `tn_ancestors_pks` |
|
|
187
|
+
| `sync.test.ts` | cold vs warm request; the `max_revision` pin; `deleted_ids` applied with no tombstone row present; cursor monotonicity and the window cap; snapshot parsing rejecting junk; the page budget and its cursor rewind |
|
|
188
|
+
| `labels.test.ts` | key vs literal for categories, features and option labels; a missing translation showing the key |
|
|
189
|
+
| `catalog.test.tsx` | loading / ready-empty / failed as three different sentences; "unknown slug" only after the catalogue loaded; the second mount asking for a delta |
|
|
190
|
+
| `features.test.tsx` | the attributes-react bridge, including `unsupportedTypes` over the un-reshaped payload |
|
|
191
|
+
| `skin.test.tsx` | the four `matchList` arms on screen; both blocked reasons named; searching without a request; ru copy |
|
|
192
|
+
| `i18n.test.ts` | every registry code resolving in en/ru/es over the union of the two bundles; ownership of the twenty un-catalogued keys; interpolation slots preserved |
|
|
193
|
+
| `pair.test.ts` | query-key namespace; the API surface being exactly the five public reads; nav ids, surfaces, routes and components |
|
|
194
|
+
| `prodBundlePurity.test.ts` | no demo/showcase code in the tarball |
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @stapel/categories-react
|
|
2
|
+
|
|
3
|
+
The frontend pair for **stapel-categories**: the catalogue tree a storefront
|
|
4
|
+
navigates by, kept fresh with the module's own revision-delta protocol instead
|
|
5
|
+
of being refetched on every page.
|
|
6
|
+
|
|
7
|
+
The backend has **no tree endpoint** and **no slug lookup**, and it serves
|
|
8
|
+
category names as **translation keys**. This package is where each of those
|
|
9
|
+
three facts is handled once, in the open, rather than rediscovered by every
|
|
10
|
+
host.
|
|
11
|
+
|
|
12
|
+
Business + state in the main entry, zero visual opinion; the antd skin lives
|
|
13
|
+
behind `./default`. Built on `@stapel/core` (typed client + `StapelApiError`
|
|
14
|
+
envelope, `LoadState`, `createRepository`, i18n engine, TanStack Query) and
|
|
15
|
+
`@stapel/attributes-react` (the feature schema's value types).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pnpm add @stapel/categories-react @stapel/core @stapel/attributes-react @tanstack/react-query react
|
|
21
|
+
# for the default skin:
|
|
22
|
+
pnpm add antd @stapel/tokens-antd
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## A catalogue, in six lines
|
|
26
|
+
|
|
27
|
+
**No session, no workspace id, no auth client** — every endpoint this pair
|
|
28
|
+
calls is a safe method under `ReadOnlyOrStaff`, so the catalogue renders for a
|
|
29
|
+
visitor who will never sign in:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { createCategoriesRuntime, CategoriesProvider } from "@stapel/categories-react";
|
|
33
|
+
import { CatalogPage } from "@stapel/categories-react/default";
|
|
34
|
+
|
|
35
|
+
const runtime = createCategoriesRuntime({ baseUrl: "/categories/api/v1/" });
|
|
36
|
+
|
|
37
|
+
export function CatalogRoute() {
|
|
38
|
+
return (
|
|
39
|
+
<CategoriesProvider runtime={runtime}>
|
|
40
|
+
<CatalogPage />
|
|
41
|
+
</CategoriesProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The client builds the tree, because the server has none
|
|
47
|
+
|
|
48
|
+
`GET /categories/` returns **flat rows ordered by `revision`** — a sync feed,
|
|
49
|
+
not a menu. Ancestry rides along in django-treenode's columns, which are
|
|
50
|
+
**comma-joined primary-key strings**, not arrays:
|
|
51
|
+
|
|
52
|
+
```jsonc
|
|
53
|
+
{ "id": 4, "slug": "used-phones", "name": "category.used_phones",
|
|
54
|
+
"tn_parent": 2, "tn_ancestors_pks": "1,2", "tn_children_pks": "" }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`buildCategoryTree(rows)` assembles the hierarchy, orders siblings by
|
|
58
|
+
`tn_priority` descending (ties by id, so a catalogue that never set priority
|
|
59
|
+
does not reshuffle between renders), and drops two kinds of row the endpoint
|
|
60
|
+
happily serves: soft-deleted ones, and **inactive** ones — `active` is filtered
|
|
61
|
+
server-side only by `/carousel/`.
|
|
62
|
+
|
|
63
|
+
A row whose parent is missing becomes a root rather than disappearing. Dropping
|
|
64
|
+
it would delete a live branch of the catalogue from the menu and give nobody a
|
|
65
|
+
reason.
|
|
66
|
+
|
|
67
|
+
## The catalogue is synced by delta, not refetched
|
|
68
|
+
|
|
69
|
+
The module documents the protocol on its own viewset: full GET → store
|
|
70
|
+
`revisions.global_max` → later `?min_revision=<stored>` → drop what comes back
|
|
71
|
+
`deleted: true`. This package implements it into an **app-scoped repository**
|
|
72
|
+
(`createRepository(..., { scope: "app" })` — the one sanctioned persistence
|
|
73
|
+
primitive), so a second page costs one small delta request instead of the whole
|
|
74
|
+
catalogue.
|
|
75
|
+
|
|
76
|
+
Two rules the documentation does not state are implemented anyway:
|
|
77
|
+
|
|
78
|
+
- **`revisions.deleted_ids` is the complete tombstone channel.** The
|
|
79
|
+
`deleted: true` rows are paginated, so one can sit on a page a short walk
|
|
80
|
+
never reached; `deleted_ids` is computed unpaginated over the whole table.
|
|
81
|
+
Both are applied; the list is the one that is complete.
|
|
82
|
+
- **A multi-page walk pins `max_revision`.** Pages are ordered by revision and
|
|
83
|
+
filtered at request time, so a write landing between page 1 and page 2 shifts
|
|
84
|
+
every later boundary and the walk skips a row. The window is pinned to the
|
|
85
|
+
`global_max` the first page reported, and the cursor never records reading
|
|
86
|
+
past it.
|
|
87
|
+
|
|
88
|
+
A walk stopped by its page budget reports `truncated` and **rewinds its
|
|
89
|
+
cursor**: the rows are still shown, but the next sync redoes the walk rather
|
|
90
|
+
than sitting a delta on top of a catalogue that was never fully read.
|
|
91
|
+
|
|
92
|
+
## `/c/:slug` is resolved on the client
|
|
93
|
+
|
|
94
|
+
`CategoryViewSet` never overrides `lookup_field` and the list endpoint has no
|
|
95
|
+
slug filter, so `GET /categories/<slug>/` is a 404 and `?slug=` is ignored. The
|
|
96
|
+
storefront's `/c/:slug` resolves against the synced tree —
|
|
97
|
+
`resolveCategorySlug(index, slug)` — which is a second, independent reason the
|
|
98
|
+
tree is cached.
|
|
99
|
+
|
|
100
|
+
A slug is not "unknown" until the catalogue has actually loaded. Rendering
|
|
101
|
+
"there is no category here" during a sync shows a 404 for a page that exists.
|
|
102
|
+
|
|
103
|
+
## Names are translation KEYS
|
|
104
|
+
|
|
105
|
+
This is the fact most likely to be got wrong, so the package states it in the
|
|
106
|
+
types instead of in a comment.
|
|
107
|
+
|
|
108
|
+
`stapel-categories` stores keys and never owns a catalogue. Its
|
|
109
|
+
`DISPLAY_TRANSLATOR` seam is called from `Category.__str__` and the admin's
|
|
110
|
+
label cache — **no serializer calls it**. So a category arrives as
|
|
111
|
+
`{"name": "category.electronics"}` even on a deployment with a real translator
|
|
112
|
+
configured, there is no `?lang=`, and `GET /translation-keys/` is a
|
|
113
|
+
service-only extraction feed for translators, not a resolver.
|
|
114
|
+
|
|
115
|
+
Which strings are keys is declared per row, and the pair reads the flag rather
|
|
116
|
+
than guessing:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
categoryLabel(category) // { kind: "key" | "literal", value } ← `translatable`
|
|
120
|
+
featureLabel(feature) // `translate: "none"` makes the name a literal
|
|
121
|
+
featureOptionsAreKeys(f) // `translate: "all"` AND config.translatable_options
|
|
122
|
+
renderCategoryLabel(label, t)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The resolution goes through the **host's** i18n engine, and this package ships
|
|
126
|
+
no category names: a catalogue is a deployment's content, not a library's
|
|
127
|
+
chrome. When a key does not resolve, the key is shown — deliberately. A visible
|
|
128
|
+
`category.electronics` gets fixed; a prettified "Electronics" invented by the
|
|
129
|
+
library ships for a year in the wrong language.
|
|
130
|
+
|
|
131
|
+
## The feature schema, and where attributes-react takes over
|
|
132
|
+
|
|
133
|
+
`GET /categories/{id}/features/` resolves inheritance and order server-side and
|
|
134
|
+
returns the polymorphic attributes config **verbatim** — not through
|
|
135
|
+
`get_config_with_defaults()`, so an absent key means "the type's default", and
|
|
136
|
+
`@stapel/attributes-react` owns those defaults. `<CategoryFeatures>` hands the
|
|
137
|
+
rows over unmodified:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<CategoryFeatures categoryId={id}>
|
|
141
|
+
{(bag) => (
|
|
142
|
+
<FeatureFields features={bag.features} values={values} onChange={set} />
|
|
143
|
+
)}
|
|
144
|
+
</CategoryFeatures>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The same `bag.features` is what `@stapel/search-react`'s facet panel takes as
|
|
148
|
+
`categoryFeatures` to caption facet values.
|
|
149
|
+
|
|
150
|
+
## Surface
|
|
151
|
+
|
|
152
|
+
| Export | What it is |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `createCategoriesRuntime` / `CategoriesProvider` | wiring |
|
|
155
|
+
| `useCategoryCatalog` | the delta-synced tree, one hook, mounted once |
|
|
156
|
+
| `useCategoryChildren` / `useCategoryCarousel` / `useCategoryFeatures` / `useCategoriesRevision` | the four direct reads |
|
|
157
|
+
| `buildCategoryTree` / `resolveCategorySlug` / `categoryBreadcrumbs` / `parseTreenodePks` | pure tree assembly |
|
|
158
|
+
| `applyCategoryPage` / `firstPageRequest` / `nextPageRequest` / `syncCatalog` | the delta protocol, testable without React |
|
|
159
|
+
| `categoryLabel` / `featureLabel` / `renderCategoryLabel` | the translation-key answer |
|
|
160
|
+
| `<CategoryTree>` `<CategoryBreadcrumbs>` `<CategoryCarousel>` `<CategoryPicker>` `<CategoryFeatures>` | headless bags |
|
|
161
|
+
| `/default`: `CatalogPage` `CategoryPage` `CategoryTreePane` `CategoryBreadcrumbsBar` `CategoryCarouselStrip` `CategoryPickerField` `CategoryFeatureList` | the antd skin |
|
|
162
|
+
|
|
163
|
+
Nav entries: `categories.catalog` → `/c`, `categories.category` → `/c/:slug`,
|
|
164
|
+
both `surface: "public"`. `/` is **not** claimed — the storefront's landing is
|
|
165
|
+
categories *plus* search, and a composed route belongs to the container that
|
|
166
|
+
composes it.
|
|
167
|
+
|
|
168
|
+
`<CategoryPage>` takes `renderListings` for the same reason: the results half of
|
|
169
|
+
`/c/:slug` belongs to another pair, handed in rather than imported across the
|
|
170
|
+
L2 layer.
|
|
171
|
+
|
|
172
|
+
## Not in this version
|
|
173
|
+
|
|
174
|
+
The catalogue **admin** — create/update/delete, `bulk_add`, `bulk-commands`,
|
|
175
|
+
`undelete`, `convert-type` and the four feature-editor operations — is not on
|
|
176
|
+
this pair's surface. Every one of them is `IsStaffUser`, and the storefront
|
|
177
|
+
wave leaves them to Django admin. `POST {id}/validate-dto/` is absent for the
|
|
178
|
+
same reason: DRF treats it as a write, so it answers 403 to exactly the
|
|
179
|
+
visitors who would want it; the compose form uses attributes-react's
|
|
180
|
+
client-side mirror plus the server's verdict on publish. `GET
|
|
181
|
+
/translation-keys/` is `IsServiceRequest`. `GET /data.json` is a second sync
|
|
182
|
+
protocol and one tested protocol beats two half-tested ones.
|
|
183
|
+
|
|
184
|
+
Nothing is hidden: `manifest.json` lists the whole contract.
|
|
185
|
+
|
|
186
|
+
## More
|
|
187
|
+
|
|
188
|
+
- [`MODULE.md`](./MODULE.md) — the module guide: layers, and every contract
|
|
189
|
+
note recorded rather than worked around.
|
|
190
|
+
- [`llms.txt`](./llms.txt) — the generated surface slice for a coding harness.
|
|
191
|
+
- [`manifest.json`](./manifest.json) — the machine-readable self-description.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { StapelClient } from "@stapel/core";
|
|
2
|
+
import type { Category, CategoryFeature, CategoryListParams, CategoryPage, MaxRevision } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The pair's typed operation surface — one method per stapel-categories
|
|
5
|
+
* endpoint a storefront may call, bound to the injected {@link StapelClient}
|
|
6
|
+
* (the per-module override seam of frontend-standard §7.2). Paths are relative
|
|
7
|
+
* to the runtime's `baseUrl` (`/categories/api/v1/`).
|
|
8
|
+
*
|
|
9
|
+
* ── Five reads, and the eighteen operations that are NOT here ──────────────
|
|
10
|
+
*
|
|
11
|
+
* The contract has 23 paths. Every one of them is on `ReadOnlyOrStaff`, which
|
|
12
|
+
* means SAFE methods are open to anonymous callers and everything else needs
|
|
13
|
+
* `is_staff` — so the split below is not a taste call, it is the permission
|
|
14
|
+
* boundary drawn on the surface:
|
|
15
|
+
*
|
|
16
|
+
* - **Public reads, and this pair's whole surface**: the category list (the
|
|
17
|
+
* delta-sync source), `children`, `carousel`, `{id}/features`, and
|
|
18
|
+
* `revision`.
|
|
19
|
+
* - **Staff writes**: create / update / delete, `bulk_add`,
|
|
20
|
+
* `bulk-commands`, `undelete`, `convert-type`, and the four
|
|
21
|
+
* `feature-editor*` operations. These are the catalogue admin, which the
|
|
22
|
+
* storefront wave leaves to Django admin (spec §4.3) — a pair that exposed
|
|
23
|
+
* them would invite a public screen to call something only staff may.
|
|
24
|
+
* - **`POST {id}/validate-dto/` is a WRITE in DRF's eyes**, so despite
|
|
25
|
+
* reading like a public helper it answers 403 to a visitor. The listing
|
|
26
|
+
* compose flow validates against the same rules with
|
|
27
|
+
* `@stapel/attributes-react`'s client-side mirror plus the server's verdict
|
|
28
|
+
* on publish; asking this endpoint would fail for exactly the people who
|
|
29
|
+
* need it.
|
|
30
|
+
* - **`GET {id}/validate-configs/`** is a catalogue-health read for an
|
|
31
|
+
* operator, not a storefront read.
|
|
32
|
+
* - **`GET translation-keys/`** is `IsServiceRequest` (service-to-service):
|
|
33
|
+
* it is the extraction feed for translators, and it returns KEYS with
|
|
34
|
+
* admin refs — never resolved labels. See `catalog/labels.ts`.
|
|
35
|
+
* - **`GET data.json/`** requires a `?revision=` cache-buster and hands back
|
|
36
|
+
* the whole table in one response. It is a legitimate alternative to the
|
|
37
|
+
* paged walk, but it is a *second* sync protocol, and the module documents
|
|
38
|
+
* the paged one. One protocol, tested, beats two, half-tested.
|
|
39
|
+
* - The parallel `/features/` collection (list, retrieve, create, …) is the
|
|
40
|
+
* feature tree in its own right — an admin axis. A storefront reads
|
|
41
|
+
* features THROUGH a category, which is the only place inheritance and
|
|
42
|
+
* order are resolved.
|
|
43
|
+
*
|
|
44
|
+
* Nothing is hidden: `manifest.json` lists the whole contract.
|
|
45
|
+
*
|
|
46
|
+
* These operations will be GENERATED from schema.json operationIds by gen-api
|
|
47
|
+
* v2 (task `core-typed-ops`); until then they are hand-authored here (the ONE
|
|
48
|
+
* legal home of path strings — `stapel/no-string-paths` §2.3 carve-out).
|
|
49
|
+
*/
|
|
50
|
+
export interface CategoriesApi {
|
|
51
|
+
readonly client: StapelClient;
|
|
52
|
+
/**
|
|
53
|
+
* One page of the revision-paginated category list — the source the whole
|
|
54
|
+
* tree is assembled from, and the only endpoint that reports deletions.
|
|
55
|
+
*
|
|
56
|
+
* Rows are ordered by `revision`, NOT by tree position: this is a sync feed,
|
|
57
|
+
* not a menu. `revisions.global_max` is the cursor to store;
|
|
58
|
+
* `revisions.deleted_ids` is the authoritative tombstone list for a delta
|
|
59
|
+
* (see `catalog/sync.ts` for why it beats scanning `deleted: true` rows).
|
|
60
|
+
*/
|
|
61
|
+
list(params?: CategoryListParams, options?: {
|
|
62
|
+
readonly signal?: AbortSignal;
|
|
63
|
+
}): Promise<CategoryPage>;
|
|
64
|
+
/**
|
|
65
|
+
* Non-deleted direct children of one category, `tn_priority` descending.
|
|
66
|
+
*
|
|
67
|
+
* Redundant with the synced tree by construction, and deliberately kept: a
|
|
68
|
+
* host that wants one branch without paying for the catalogue (an SSR
|
|
69
|
+
* category page, a lazily-expanded admin-ish picker) has an endpoint for it,
|
|
70
|
+
* and it is the one place the server does the `deleted` filtering.
|
|
71
|
+
*/
|
|
72
|
+
children(id: number, options?: {
|
|
73
|
+
readonly signal?: AbortSignal;
|
|
74
|
+
}): Promise<readonly Category[]>;
|
|
75
|
+
/**
|
|
76
|
+
* The carousel strip: `active` AND `carousel_enabled`, `tn_priority`
|
|
77
|
+
* descending, cached server-side and sent with
|
|
78
|
+
* `Cache-Control: public, max-age` — so this one is safe to call on every
|
|
79
|
+
* landing render.
|
|
80
|
+
*/
|
|
81
|
+
carousel(options?: {
|
|
82
|
+
readonly signal?: AbortSignal;
|
|
83
|
+
}): Promise<readonly Category[]>;
|
|
84
|
+
/**
|
|
85
|
+
* A category's resolved feature schema: its own features plus every ancestor's,
|
|
86
|
+
* deduplicated by SLUG (an `inherit` override is a new row sharing the
|
|
87
|
+
* parent's slug, and the version nearest this category wins), ordered by the
|
|
88
|
+
* category's own `CategoryFeature.order` first.
|
|
89
|
+
*
|
|
90
|
+
* This is what the listing compose form draws with
|
|
91
|
+
* `@stapel/attributes-react` and what labels a search facet. `config`
|
|
92
|
+
* arrives verbatim — defaults are the reader's job, and attributes-react
|
|
93
|
+
* owns them.
|
|
94
|
+
*/
|
|
95
|
+
features(id: number, options?: {
|
|
96
|
+
readonly signal?: AbortSignal;
|
|
97
|
+
}): Promise<readonly CategoryFeature[]>;
|
|
98
|
+
/**
|
|
99
|
+
* The table's current maximum revision, without transferring any rows.
|
|
100
|
+
*
|
|
101
|
+
* The cheap freshness probe: a stored snapshot whose cursor already equals
|
|
102
|
+
* this needs no delta request at all.
|
|
103
|
+
*/
|
|
104
|
+
revision(options?: {
|
|
105
|
+
readonly signal?: AbortSignal;
|
|
106
|
+
}): Promise<MaxRevision>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The wire query object for one list request.
|
|
110
|
+
*
|
|
111
|
+
* Exported because the query KEY is built from it, so the key and the request
|
|
112
|
+
* are the same value by construction.
|
|
113
|
+
*
|
|
114
|
+
* `include_deleted` is sent ONLY when the caller asked for a value, and the
|
|
115
|
+
* server's own default is `true`. Sending it always would be harmless; leaving
|
|
116
|
+
* it out when unasked keeps the cache key of a plain read from depending on
|
|
117
|
+
* this pair's opinion about a server default.
|
|
118
|
+
*/
|
|
119
|
+
export declare function categoryListParams(params?: CategoryListParams): Record<string, string | number | boolean | undefined>;
|
|
120
|
+
export declare function createCategoriesApi(client: StapelClient): CategoriesApi;
|
|
121
|
+
//# sourceMappingURL=categoriesApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"categoriesApi.d.ts","sourceRoot":"","sources":["../../src/api/categoriesApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B;;;;;;;;OAQG;IACH,IAAI,CACF,MAAM,CAAC,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB;;;;;;;OAOG;IACH,QAAQ,CACN,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAEhC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;KAC/B,GAAG,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAEjC;;;;;;;;;;OAUG;IACH,QAAQ,CACN,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;KAC/B,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAUvD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CA8BvE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire query object for one list request.
|
|
3
|
+
*
|
|
4
|
+
* Exported because the query KEY is built from it, so the key and the request
|
|
5
|
+
* are the same value by construction.
|
|
6
|
+
*
|
|
7
|
+
* `include_deleted` is sent ONLY when the caller asked for a value, and the
|
|
8
|
+
* server's own default is `true`. Sending it always would be harmless; leaving
|
|
9
|
+
* it out when unasked keeps the cache key of a plain read from depending on
|
|
10
|
+
* this pair's opinion about a server default.
|
|
11
|
+
*/
|
|
12
|
+
export function categoryListParams(params = {}) {
|
|
13
|
+
const query = {};
|
|
14
|
+
if (params.minRevision !== undefined)
|
|
15
|
+
query["min_revision"] = params.minRevision;
|
|
16
|
+
if (params.maxRevision !== undefined)
|
|
17
|
+
query["max_revision"] = params.maxRevision;
|
|
18
|
+
if (params.includeDeleted !== undefined) {
|
|
19
|
+
query["include_deleted"] = params.includeDeleted;
|
|
20
|
+
}
|
|
21
|
+
if (params.page !== undefined)
|
|
22
|
+
query["page"] = params.page;
|
|
23
|
+
if (params.pageSize !== undefined)
|
|
24
|
+
query["page_size"] = params.pageSize;
|
|
25
|
+
return query;
|
|
26
|
+
}
|
|
27
|
+
export function createCategoriesApi(client) {
|
|
28
|
+
return {
|
|
29
|
+
client,
|
|
30
|
+
list: (params, options) => client.get("/categories/", {
|
|
31
|
+
query: categoryListParams(params),
|
|
32
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
33
|
+
}),
|
|
34
|
+
children: (id, options) => client.get(`/categories/${String(id)}/children/`, {
|
|
35
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
36
|
+
}),
|
|
37
|
+
carousel: (options) => client.get("/categories/carousel/", {
|
|
38
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
39
|
+
}),
|
|
40
|
+
features: (id, options) => client.get(`/categories/${String(id)}/features/`, {
|
|
41
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
42
|
+
}),
|
|
43
|
+
revision: (options) => client.get("/categories/revision/", {
|
|
44
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=categoriesApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"categoriesApi.js","sourceRoot":"","sources":["../../src/api/categoriesApi.ts"],"names":[],"mappings":"AA2HA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAA6B,EAAE;IAE/B,MAAM,KAAK,GAA0D,EAAE,CAAC;IACxE,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;IACjF,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;IACjF,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACxC,KAAK,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;IACnD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IAC3D,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;IACxE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAoB;IACtD,OAAO;QACL,MAAM;QAEN,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE;YACzB,KAAK,EAAE,kBAAkB,CAAC,MAAM,CAAC;YACjC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE;YAChD,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CACpB,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE;YAClC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE;YAChD,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CACpB,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE;YAClC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;KACL,CAAC;AACJ,CAAC"}
|