@stapel/listings-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 +191 -0
- package/README.md +169 -0
- package/dist/api/generated/schema.d.ts +1253 -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/listingsApi.d.ts +145 -0
- package/dist/api/listingsApi.d.ts.map +1 -0
- package/dist/api/listingsApi.js +38 -0
- package/dist/api/listingsApi.js.map +1 -0
- package/dist/api/types.d.ts +156 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +37 -0
- package/dist/api/types.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/FavoritesPane.d.ts +18 -0
- package/dist/default/FavoritesPane.d.ts.map +1 -0
- package/dist/default/FavoritesPane.js +28 -0
- package/dist/default/FavoritesPane.js.map +1 -0
- package/dist/default/ListingCard.d.ts +42 -0
- package/dist/default/ListingCard.d.ts.map +1 -0
- package/dist/default/ListingCard.js +37 -0
- package/dist/default/ListingCard.js.map +1 -0
- package/dist/default/ListingComposerPage.d.ts +46 -0
- package/dist/default/ListingComposerPage.d.ts.map +1 -0
- package/dist/default/ListingComposerPage.js +99 -0
- package/dist/default/ListingComposerPage.js.map +1 -0
- package/dist/default/ListingDetailPane.d.ts +33 -0
- package/dist/default/ListingDetailPane.d.ts.map +1 -0
- package/dist/default/ListingDetailPane.js +48 -0
- package/dist/default/ListingDetailPane.js.map +1 -0
- package/dist/default/ListingPhoto.d.ts +25 -0
- package/dist/default/ListingPhoto.d.ts.map +1 -0
- package/dist/default/ListingPhoto.js +20 -0
- package/dist/default/ListingPhoto.js.map +1 -0
- package/dist/default/MyListingsPane.d.ts +25 -0
- package/dist/default/MyListingsPane.d.ts.map +1 -0
- package/dist/default/MyListingsPane.js +84 -0
- package/dist/default/MyListingsPane.js.map +1 -0
- package/dist/default/StatusTags.d.ts +31 -0
- package/dist/default/StatusTags.d.ts.map +1 -0
- package/dist/default/StatusTags.js +40 -0
- package/dist/default/StatusTags.js.map +1 -0
- package/dist/default/icons.d.ts +16 -0
- package/dist/default/icons.d.ts.map +1 -0
- package/dist/default/icons.js +5 -0
- package/dist/default/icons.js.map +1 -0
- package/dist/default/index.d.ts +35 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +26 -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/flows/registry.d.ts +35 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +33 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/Favorites.d.ts +52 -0
- package/dist/headless/Favorites.d.ts.map +1 -0
- package/dist/headless/Favorites.js +78 -0
- package/dist/headless/Favorites.js.map +1 -0
- package/dist/headless/ListingActions.d.ts +30 -0
- package/dist/headless/ListingActions.d.ts.map +1 -0
- package/dist/headless/ListingActions.js +54 -0
- package/dist/headless/ListingActions.js.map +1 -0
- package/dist/headless/ListingComposer.d.ts +133 -0
- package/dist/headless/ListingComposer.d.ts.map +1 -0
- package/dist/headless/ListingComposer.js +246 -0
- package/dist/headless/ListingComposer.js.map +1 -0
- package/dist/headless/ListingDetail.d.ts +81 -0
- package/dist/headless/ListingDetail.d.ts.map +1 -0
- package/dist/headless/ListingDetail.js +101 -0
- package/dist/headless/ListingDetail.js.map +1 -0
- package/dist/headless/ListingsProvider.d.ts +17 -0
- package/dist/headless/ListingsProvider.d.ts.map +1 -0
- package/dist/headless/ListingsProvider.js +16 -0
- package/dist/headless/ListingsProvider.js.map +1 -0
- package/dist/headless/MyListings.d.ts +44 -0
- package/dist/headless/MyListings.d.ts.map +1 -0
- package/dist/headless/MyListings.js +82 -0
- package/dist/headless/MyListings.js.map +1 -0
- package/dist/headless/useMandateGate.d.ts +28 -0
- package/dist/headless/useMandateGate.d.ts.map +1 -0
- package/dist/headless/useMandateGate.js +39 -0
- package/dist/headless/useMandateGate.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +34 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +45 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +21 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +146 -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 +413 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +210 -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 +170 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +295 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +37 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +162 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.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/draft.d.ts +138 -0
- package/dist/model/draft.d.ts.map +1 -0
- package/dist/model/draft.js +192 -0
- package/dist/model/draft.js.map +1 -0
- package/dist/model/features.d.ts +69 -0
- package/dist/model/features.d.ts.map +1 -0
- package/dist/model/features.js +73 -0
- package/dist/model/features.js.map +1 -0
- package/dist/model/mineSource.d.ts +47 -0
- package/dist/model/mineSource.d.ts.map +1 -0
- package/dist/model/mineSource.js +45 -0
- package/dist/model/mineSource.js.map +1 -0
- package/dist/model/mutations.d.ts +60 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +165 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +75 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +117 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +50 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +24 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +69 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +14 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/status.d.ts +117 -0
- package/dist/model/status.d.ts.map +1 -0
- package/dist/model/status.js +164 -0
- package/dist/model/status.js.map +1 -0
- package/dist/model/transitions.d.ts +41 -0
- package/dist/model/transitions.d.ts.map +1 -0
- package/dist/model/transitions.js +25 -0
- package/dist/model/transitions.js.map +1 -0
- package/dist/model/validation.d.ts +135 -0
- package/dist/model/validation.d.ts.map +1 -0
- package/dist/model/validation.js +191 -0
- package/dist/model/validation.js.map +1 -0
- package/dist/nav/manifest.d.ts +46 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +56 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +118 -0
- package/manifest.json +1044 -0
- package/nav-manifest.json +84 -0
- package/package.json +121 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/generated/schema.ts +1255 -0
- package/src/api/listingsApi.ts +237 -0
- package/src/api/types.ts +193 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/FavoritesPane.tsx +123 -0
- package/src/default/ListingCard.tsx +178 -0
- package/src/default/ListingComposerPage.tsx +466 -0
- package/src/default/ListingDetailPane.tsx +271 -0
- package/src/default/ListingPhoto.tsx +62 -0
- package/src/default/MyListingsPane.tsx +333 -0
- package/src/default/StatusTags.tsx +86 -0
- package/src/default/icons.tsx +31 -0
- package/src/default/index.ts +35 -0
- package/src/default/theme.tsx +67 -0
- package/src/default/types.ts +12 -0
- package/src/flows/registry.ts +45 -0
- package/src/headless/Favorites.tsx +137 -0
- package/src/headless/ListingActions.tsx +101 -0
- package/src/headless/ListingComposer.tsx +476 -0
- package/src/headless/ListingDetail.tsx +231 -0
- package/src/headless/ListingsProvider.tsx +20 -0
- package/src/headless/MyListings.tsx +148 -0
- package/src/headless/useMandateGate.ts +40 -0
- package/src/i18n/errorsMap.ts +62 -0
- package/src/i18n/es.ts +198 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +241 -0
- package/src/i18n/generated/errors.json +499 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +356 -0
- package/src/i18n/ru.ts +215 -0
- package/src/index.ts +260 -0
- package/src/model/context.tsx +28 -0
- package/src/model/draft.ts +307 -0
- package/src/model/features.ts +122 -0
- package/src/model/mineSource.ts +55 -0
- package/src/model/mutations.ts +224 -0
- package/src/model/queries.ts +150 -0
- package/src/model/queryKeys.ts +76 -0
- package/src/model/runtime.ts +87 -0
- package/src/model/status.ts +279 -0
- package/src/model/transitions.ts +62 -0
- package/src/model/validation.ts +271 -0
- package/src/nav/manifest.ts +97 -0
- package/tsconfig.json +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @stapel/listings-react
|
package/MODULE.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @stapel/listings-react — module guide
|
|
2
|
+
|
|
3
|
+
Pairs with **stapel-listings 0.6.1** (`>=0.6 <0.7`), 12 paths under
|
|
4
|
+
`/listings/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/ listingsApi.ts the 14 operations a storefront calls; the one home of path strings
|
|
12
|
+
types.ts wire aliases + the one hand-mirrored shape (the stored DAO)
|
|
13
|
+
generated/ openapi-typescript, drift-gated
|
|
14
|
+
src/model/ status.ts the two axes → one caption, 9 × 4, asserted
|
|
15
|
+
transitions.ts the server's whitelist, mirrored (UX only; the 409 is the verdict)
|
|
16
|
+
draft.ts the *_draft twin ↔ the composer's values
|
|
17
|
+
features.ts the stored DAO projection → what attributes-react formats
|
|
18
|
+
validation.ts the mirror, and the split of the two publish 400s
|
|
19
|
+
mineSource.ts THE GAP: no owner-scoped list endpoint exists
|
|
20
|
+
runtime · context · queryKeys · queries · mutations
|
|
21
|
+
src/flows/ registry.ts zero-flow shim (the module annotates no @flow_step)
|
|
22
|
+
src/headless/ ListingsProvider · ListingDetail · ListingComposer · MyListings
|
|
23
|
+
Favorites · ListingActions · useMandateGate
|
|
24
|
+
src/default/ the antd skin, `./default` subpath
|
|
25
|
+
src/i18n/ keys · ru · es · errorsMap + generated/
|
|
26
|
+
src/nav/ manifest.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Everything in `model/` except the hooks is pure — no React, no fetch — which
|
|
30
|
+
is what lets the status table, the draft conversions and the mirror be tested
|
|
31
|
+
without a DOM, and what lets an SSR render call them directly.
|
|
32
|
+
|
|
33
|
+
## 1. Two axes, and the divergence 0.5.0 introduced
|
|
34
|
+
|
|
35
|
+
`status` (nine states) is the lifecycle and the ONLY thing that decides public
|
|
36
|
+
visibility: `Listing.objects.published()` filters on it alone,
|
|
37
|
+
`INDEXED_STATUSES` is `{published}`, and the model says so out loud ("no
|
|
38
|
+
visibility-reads-moderation_status coupling", `models.py`).
|
|
39
|
+
`moderation_status` (four states) is the content verdict and decides nothing
|
|
40
|
+
about visibility.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
first publication status draft → PENDING, moderation → pending
|
|
44
|
+
nothing public until a verdict arrives
|
|
45
|
+
|
|
46
|
+
editing a LIVE listing status published (UNCHANGED), moderation → pending
|
|
47
|
+
the edit is visible immediately; a rejecting verdict
|
|
48
|
+
later lands as PUBLISHED → BLOCKED
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`model/status.ts` produces both halves of the sentence from BOTH fields, once.
|
|
52
|
+
`test/status.test.ts` walks all 36 combinations, and names the four that look
|
|
53
|
+
contradictory. `liveUnderReview` is a named boolean rather than an inference a
|
|
54
|
+
caller repeats.
|
|
55
|
+
|
|
56
|
+
The moderation axis is shown to the OWNER and to nobody else: a buyer has no
|
|
57
|
+
use for "changes under review", and showing a stranger that a listing was
|
|
58
|
+
refused would leak a verdict about someone else's content.
|
|
59
|
+
|
|
60
|
+
## 2. Contract deltas — what the schema does not say
|
|
61
|
+
|
|
62
|
+
### 2.1 The discriminator was broken in 0.6.0 and is fixed in 0.6.1
|
|
63
|
+
|
|
64
|
+
0.6.0 declared `discriminator: {propertyName: "type", mapping: {"null":
|
|
65
|
+
ConvertibleUnitDao}}` — one bogus entry instead of the ten type slugs — and
|
|
66
|
+
openapi-typescript answers that by stripping `type` from every member and
|
|
67
|
+
re-adding a synthetic discriminant, so the generated `IntDao` said
|
|
68
|
+
`type: "IntDao"` where the wire sends `"int"`. `@stapel/categories-react` hit
|
|
69
|
+
the same defect and routed around it through attributes-react's hand-mirrored
|
|
70
|
+
types. **0.6.1 emits the ten slugs**, so this pair's `Schemas["FeatureDao"]`
|
|
71
|
+
and `Schemas["FeatureDto"]` discriminate on the real values and ARE the wire
|
|
72
|
+
types it uses; `ListingFeatureType` is derived from the union rather than
|
|
73
|
+
hand-listed, so a type added upstream widens it automatically.
|
|
74
|
+
|
|
75
|
+
What still comes from `@stapel/attributes-react` is the BEHAVIOUR — the value
|
|
76
|
+
editors, the client mirror, the formatter — which is the spec's L0 seam and
|
|
77
|
+
never was a workaround.
|
|
78
|
+
|
|
79
|
+
### 2.2 A stored DAO carries `slug`; the schema's `FeatureDao` does not
|
|
80
|
+
|
|
81
|
+
`features` / `features_title` / `features_badges` are
|
|
82
|
+
`ListingFeaturesOutputField`, a plain `serializers.JSONField` whose OpenAPI
|
|
83
|
+
*description* is swapped for the DAO union by an extension. A JSONField
|
|
84
|
+
filters nothing: what reaches the wire is what `build_features_list` stored,
|
|
85
|
+
and that is `{**dao, "slug": slug}` (`services/features.py`). The slug is
|
|
86
|
+
load-bearing — it is how a card keys a badge and how a refusal finds its
|
|
87
|
+
control — so `ListingFeatureDao` mirrors the runtime shape and says why.
|
|
88
|
+
|
|
89
|
+
A row WITHOUT a slug is treated as malformed and counted
|
|
90
|
+
(`unreadableFeatureCount`), never silently dropped: a synthesized index would
|
|
91
|
+
key a badge to a position that moves whenever the category does.
|
|
92
|
+
|
|
93
|
+
### 2.3 The DAO carries its type's config inline — and that is the good news
|
|
94
|
+
|
|
95
|
+
`prefix`, `postfix`, `precision`, `trueLabel`, `maxSelected`, … all ride along
|
|
96
|
+
beside `value`, which is why `<ListingCard>` can format "1200 W" from the row
|
|
97
|
+
alone. A grid of forty cards costs one query and no category read. The one
|
|
98
|
+
thing a DAO does not carry is `select`'s `options` table, so an option value
|
|
99
|
+
falls back to its raw form — which is a translation KEY when the config is
|
|
100
|
+
translatable (the default), so a host whose bundle carries the catalogue's
|
|
101
|
+
copy still reads a word.
|
|
102
|
+
|
|
103
|
+
### 2.4 The publish 400 has two shapes
|
|
104
|
+
|
|
105
|
+
An invalid draft answers a BARE `ValidationBatchResult` (no
|
|
106
|
+
`localizable_error`, no envelope), which core wraps as `stapel.http.400` with
|
|
107
|
+
the batch on `StapelApiError.body`. A promotion that then fails
|
|
108
|
+
(`REQUIRE_IMAGE_ON_PUBLISH` with no photo) answers the ordinary
|
|
109
|
+
`error.400.publish_validation_failed`. `publishRefusal` branches on the BODY,
|
|
110
|
+
not the status: a caller that branched on `status === 400` alone would put a
|
|
111
|
+
sentence under a feature control.
|
|
112
|
+
|
|
113
|
+
### 2.5 The description's refusal is already keyed like a control
|
|
114
|
+
|
|
115
|
+
`services.publish.validate_draft` inserts `validate_description`'s row at the
|
|
116
|
+
front of the same list, and that row's slug is literally `"description"`
|
|
117
|
+
(`stapel_attributes/validation.py:726`) — the same key the mirror files its
|
|
118
|
+
own length refusal under. One routing table covers both.
|
|
119
|
+
|
|
120
|
+
## 3. Upstream asks
|
|
121
|
+
|
|
122
|
+
| # | Ask | Why it matters here |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| 1 | An owner-scoped list (`?owner=me`, or a `my/listings` action) | Without it a seller cannot be shown their own drafts by any call the contract offers. Today: an injected `MyListingsSource`, and a named failure when there is none. `src/model/mineSource.ts`. |
|
|
125
|
+
| 2 | A read that returns the `*_draft` twin | `GET /{pk}/` serializes the PUBLISHED fields only, so a draft abandoned and reopened later comes back empty. Editing a live listing is unaffected. |
|
|
126
|
+
| 3 | `_get_own` in front of `update` / `partial_update` | Both are the plain `ModelViewSet` implementations under `IsAuthenticatedOrReadOnly` over `Listing.objects.all()`: **any authenticated caller can write any listing's draft fields through `PUT`/`PATCH`.** Every other owner operation checks ownership. This pair declines both and uses `save-draft`, which performs the same write with the check — so nothing is lost, but the endpoints remain reachable by anything else that speaks the contract. |
|
|
127
|
+
| 4 | A `published()` filter (or an owner check) on `retrieve` | The detail endpoint answers 200 for a draft, a rejected and a blocked listing to anyone holding the id. The pair reports `publiclyVisible` from `status` and says which situation the reader is in, but it cannot stop the read. |
|
|
128
|
+
| 5 | A public read-by-reference for stored images | `Listing.images` is opaque `<type>/<hash>` and nothing in this fleet resolves a stranger's reference (stapel-cdn's `file/exists/` is owner-scoped). Today: a host-supplied `resolveImage`. |
|
|
129
|
+
| 6 | `slug` declared on `FeatureDao` | See §2.2 — it is on the wire and absent from the schema. |
|
|
130
|
+
|
|
131
|
+
## 4. The three seams, and why none of them is an import
|
|
132
|
+
|
|
133
|
+
`@stapel/cdn-react` and `@stapel/categories-react` are L2 pairs, and L2 pairs
|
|
134
|
+
never import each other (the monorepo README states the direction). So:
|
|
135
|
+
|
|
136
|
+
- **the gallery** is a structural `ListingImagesBag` — two members, satisfied
|
|
137
|
+
by `useUploadQueue()`'s bag. `bag.refs` IS the value of `images_draft` (same
|
|
138
|
+
order, first tile the cover) and `bag.settled` is the submit gate. That pair
|
|
139
|
+
wrote its bag to this contract on purpose (its §13.6 note 9);
|
|
140
|
+
- **the category schema** is a plain `readonly FeatureDef[]`, plus a
|
|
141
|
+
`categorySlot` for the chooser;
|
|
142
|
+
- **a stored image reference** is resolved by a host-supplied
|
|
143
|
+
`ListingImageResolver`.
|
|
144
|
+
|
|
145
|
+
`@stapel/attributes-react` IS a dependency; it is L0, like `@stapel/image`.
|
|
146
|
+
|
|
147
|
+
## 5. Gates and their reasons
|
|
148
|
+
|
|
149
|
+
Every write in the pair is behind an `ActionAvailability`. The mandate axis is
|
|
150
|
+
read through core's `MandateSource` seam — never derived here, because a
|
|
151
|
+
storefront's derivation is "is there a session?" and a tenant app's is
|
|
152
|
+
`@stapel/workspaces-react`'s. `matchMandate` has five required arms, so the
|
|
153
|
+
two `unresolved` outcomes cannot fall into the refusal's branch: "we have not
|
|
154
|
+
finished asking" and "we could not ask" are both distinct from "you may not".
|
|
155
|
+
|
|
156
|
+
The lifecycle mirror (`model/transitions.ts`) is a COPY of
|
|
157
|
+
`LISTING_TRANSITIONS`, not a summary of it, and it may never block what the
|
|
158
|
+
server would allow. The 409 stays the verdict, rendered as the named refusal
|
|
159
|
+
it is with `params.from_status` in the sentence.
|
|
160
|
+
|
|
161
|
+
## 6. Locales
|
|
162
|
+
|
|
163
|
+
`stapel-listings` ships no `translations/` directory, so 21 of the 63 registry
|
|
164
|
+
codes have no upstream catalogue. They split by owner: 42 cross-cutting
|
|
165
|
+
`stapel_core` codes are generated from core's catalogue, the 9
|
|
166
|
+
`stapel_listings` codes are authored in `src/i18n/{ru,es}.ts` (nine lines to
|
|
167
|
+
delete when upstream localizes), and the 12 `stapel_attributes` codes are
|
|
168
|
+
deliberately left to `@stapel/attributes-react`. `test/i18n.test.ts` asserts
|
|
169
|
+
over the UNION of the two bundles a host actually registers, so nobody can
|
|
170
|
+
make it pass by copying the other package's keys in.
|
|
171
|
+
|
|
172
|
+
## 7. Tests
|
|
173
|
+
|
|
174
|
+
142 in 8 files (138 under `test`, 4 under `test:pack`). The ones that carry
|
|
175
|
+
the weight:
|
|
176
|
+
|
|
177
|
+
- `status.test.ts` — the 9 × 4 table, the tab grouping (including BLOCKED,
|
|
178
|
+
which `my_counters` counts in no tab at all), and the transition mirror;
|
|
179
|
+
- `draft.test.ts` — the payload, in both directions: type tagging from the
|
|
180
|
+
category schema, blanks omitted rather than nulled, the countable/stock
|
|
181
|
+
cross-field rule, code-point length, the two kinds of publish 400;
|
|
182
|
+
- `compose.test.tsx` — the submission against the wire: the row is created
|
|
183
|
+
with the category and nothing else, the gallery's refs go out in order, the
|
|
184
|
+
save happens BEFORE the publish, a `ValidationBatchResult` lands on the
|
|
185
|
+
control that caused it, and the two publish outcomes are told apart by what
|
|
186
|
+
the server answered;
|
|
187
|
+
- `detail.test.tsx` — four absences and four sentences, the owner-only
|
|
188
|
+
moderation axis, and the favourite control blocked-but-visible for a
|
|
189
|
+
visitor;
|
|
190
|
+
- `mine.test.tsx` — the named gap, real counters beside it, and no badge at
|
|
191
|
+
all for a count that failed to load.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @stapel/listings-react
|
|
2
|
+
|
|
3
|
+
The frontend pair for **stapel-listings**: the listing page, the submission
|
|
4
|
+
flow, and the seller's dashboard.
|
|
5
|
+
|
|
6
|
+
Two facts about this module shape the whole package, and both are easy to get
|
|
7
|
+
subtly wrong:
|
|
8
|
+
|
|
9
|
+
1. **A listing has two independent state axes.** `status` decides whether
|
|
10
|
+
anyone can see it; `moderation_status` decides nothing about that. Since
|
|
11
|
+
stapel-listings 0.5.0 an edit to a LIVE listing keeps `status: published`
|
|
12
|
+
and moves only the moderation axis — so "published, and we are reviewing
|
|
13
|
+
your changes" is a real state, and a screen that computed either field from
|
|
14
|
+
the other would either hide a listing buyers are reading or never tell its
|
|
15
|
+
owner their edit is being screened.
|
|
16
|
+
2. **A publish refusal is per-field, and it arrives in an unusual envelope.**
|
|
17
|
+
`POST /{pk}/publish/` answers an invalid draft with a bare
|
|
18
|
+
`ValidationBatchResult` — no `localizable_error` — while a promotion that
|
|
19
|
+
fails afterwards answers the ordinary one. Two 400s, two meanings.
|
|
20
|
+
|
|
21
|
+
Business + state in the main entry, zero visual opinion; the antd skin lives
|
|
22
|
+
behind `./default`. Built on `@stapel/core` (typed client + `StapelApiError`
|
|
23
|
+
envelope, `LoadState`, `ActionAvailability`, the mandate seam, i18n engine,
|
|
24
|
+
TanStack Query), `@stapel/attributes-react` (the value editors, the client
|
|
25
|
+
mirror and the display formatter) and `@stapel/image` (the variant ladder).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pnpm add @stapel/listings-react @stapel/core @stapel/attributes-react @stapel/image @tanstack/react-query react
|
|
31
|
+
# for the default skin:
|
|
32
|
+
pnpm add antd @stapel/tokens-antd
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## A listing page, in six lines
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { createListingsRuntime, ListingsProvider } from "@stapel/listings-react";
|
|
39
|
+
import { ListingDetailPane } from "@stapel/listings-react/default";
|
|
40
|
+
|
|
41
|
+
const runtime = createListingsRuntime({
|
|
42
|
+
baseUrl: "/listings/api/v1/",
|
|
43
|
+
resolveImage: (ref) => myCdn.describe(ref), // see "Photos" below
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export function ListingRoute({ id }: { id: number }) {
|
|
47
|
+
return (
|
|
48
|
+
<ListingsProvider runtime={runtime}>
|
|
49
|
+
<ListingDetailPane id={id} />
|
|
50
|
+
</ListingsProvider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## The card is a slot, not an import
|
|
56
|
+
|
|
57
|
+
A marketplace's result grid goes through `@stapel/search-react`, because
|
|
58
|
+
`promoted` (DSA Art. 26) rides every search item under every sort and a card
|
|
59
|
+
list that carried the marking on some pages and not others would be worse than
|
|
60
|
+
one that never claimed it. The two pairs never import each other — the
|
|
61
|
+
container is the seam:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<SearchPage renderCard={(item) => <ListingCard listing={item.card} href={`/l/${item.id}`} />} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`<ListingCard>` renders its badges from `features_badges`, a stored projection
|
|
68
|
+
that carries each type's display config beside its value. A grid of forty
|
|
69
|
+
cards therefore costs one query and **no category read**.
|
|
70
|
+
|
|
71
|
+
## Submitting a listing
|
|
72
|
+
|
|
73
|
+
Four contracts meet on the composer, and three of them arrive as seams rather
|
|
74
|
+
than dependencies:
|
|
75
|
+
|
|
76
|
+
| what | how it arrives | why |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| the category's schema | `features: FeatureDef[]` | `@stapel/categories-react` is an L2 pair; L2 pairs do not import each other |
|
|
79
|
+
| the photos | `images` — two members of `@stapel/cdn-react`'s upload bag | same rule; `bag.refs` IS `images_draft` and `bag.settled` is the submit gate |
|
|
80
|
+
| the value editors | `@stapel/attributes-react` | L0, so a direct dependency |
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const gallery = useUploadQueue({ max: 10 }); // @stapel/cdn-react
|
|
84
|
+
const features = useCategoryFeatures(categoryId); // @stapel/categories-react
|
|
85
|
+
|
|
86
|
+
<ListingComposerPage
|
|
87
|
+
features={features}
|
|
88
|
+
images={gallery}
|
|
89
|
+
categorySlot={<CategoryPickerField value={categoryId} onChange={setCategoryId} />}
|
|
90
|
+
gallerySlot={<MediaGalleryField bag={gallery} />}
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The flow is `create draft → save into it → publish`. The composer always saves
|
|
95
|
+
before it publishes, because `publish` promotes the STORED draft: publishing
|
|
96
|
+
without saving would promote whatever was there before the last keystroke.
|
|
97
|
+
|
|
98
|
+
### Every switched-off publish button says which of six reasons it is
|
|
99
|
+
|
|
100
|
+
sign in · choose a category · we could not load what this category asks for ·
|
|
101
|
+
this build cannot draw one of these details · wait for the photos · fix the
|
|
102
|
+
highlighted fields. They are ordered the way a person would be told, and the
|
|
103
|
+
reason is rendered beside the button.
|
|
104
|
+
|
|
105
|
+
## Photos need a resolver, and the pair says so
|
|
106
|
+
|
|
107
|
+
`Listing.images` is a list of opaque CDN references (`<type>/<hash>`), and
|
|
108
|
+
**no contract in this fleet resolves a stranger's reference**: there is no
|
|
109
|
+
public read-by-reference in stapel-listings, and stapel-cdn's `file/exists/`
|
|
110
|
+
is owner-scoped. So the runtime takes a `resolveImage` from the deployment,
|
|
111
|
+
which knows where its CDN serves from, and a pane without one says "photos
|
|
112
|
+
cannot be shown here" instead of drawing a broken `<img>`. Inventing
|
|
113
|
+
`${cdnBase}/${ref}` would be writing a contract nobody agreed to.
|
|
114
|
+
|
|
115
|
+
## What this pair cannot do, and why
|
|
116
|
+
|
|
117
|
+
**List your own listings.** stapel-listings 0.6.1 has no owner-scoped list
|
|
118
|
+
endpoint: `GET /listings/` answers `published()` and takes no owner parameter.
|
|
119
|
+
The counters (`my/counters`) are real and are shown; the rows come from an
|
|
120
|
+
injected `MyListingsSource`, and with none the dashboard reports a NAMED
|
|
121
|
+
failure rather than an empty grid — "we cannot ask" and "you have no listings"
|
|
122
|
+
are different sentences. See `src/model/mineSource.ts` for the upstream asks.
|
|
123
|
+
|
|
124
|
+
**Reopen an abandoned draft.** No read returns the `*_draft` twin:
|
|
125
|
+
`GET /{pk}/` serializes the published fields. Editing a LIVE listing works
|
|
126
|
+
completely (the published half IS the listing); a draft reopened in a later
|
|
127
|
+
session comes back empty and the composer says so.
|
|
128
|
+
|
|
129
|
+
**Write through `PUT` / `PATCH`.** They are on the contract and absent from
|
|
130
|
+
`ListingsApi`. Every other owner operation routes through `views._get_own`;
|
|
131
|
+
these two are the plain `ModelViewSet` implementations under
|
|
132
|
+
`IsAuthenticatedOrReadOnly` over `Listing.objects.all()`, so any authenticated
|
|
133
|
+
caller can write any listing's draft fields through them.
|
|
134
|
+
`POST /{pk}/save-draft/` performs the same write WITH the ownership check, so
|
|
135
|
+
the pair uses that and nothing is lost.
|
|
136
|
+
|
|
137
|
+
## Layers
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/api/ listingsApi.ts · types.ts · generated/schema.ts
|
|
141
|
+
src/model/ status · transitions · draft · features · validation · mineSource
|
|
142
|
+
runtime · context · queryKeys · queries · mutations
|
|
143
|
+
src/flows/ registry.ts (zero-flow shim — the module annotates none)
|
|
144
|
+
src/headless/ ListingsProvider · ListingDetail · ListingComposer · MyListings
|
|
145
|
+
Favorites · ListingActions · useMandateGate
|
|
146
|
+
src/default/ the antd skin, `./default` subpath
|
|
147
|
+
src/i18n/ keys · ru · es · errorsMap + generated/
|
|
148
|
+
src/nav/ manifest.ts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`model/` is pure apart from the hooks: the status table, the draft
|
|
152
|
+
conversions, the mirror and the publish-400 split are all plain functions, so
|
|
153
|
+
the parts worth getting right are tested without a DOM.
|
|
154
|
+
|
|
155
|
+
## Locales
|
|
156
|
+
|
|
157
|
+
English is inline. `./i18n/ru` and `./i18n/es` are opt-in subpaths and carry
|
|
158
|
+
the UI copy as well as the nine `stapel_listings` error keys — the storefront
|
|
159
|
+
is ru-first (owner verdict F1), and a half-translated submission form is
|
|
160
|
+
visible immediately. The twelve `stapel_attributes` keys are deliberately NOT
|
|
161
|
+
here: `@stapel/attributes-react` owns and translates them, and one refusal must
|
|
162
|
+
not have two sentences.
|
|
163
|
+
|
|
164
|
+
## Documentation
|
|
165
|
+
|
|
166
|
+
- `MODULE.md` — the module guide: the two axes in full, the contract deltas,
|
|
167
|
+
the upstream asks.
|
|
168
|
+
- `manifest.json` / `llms.txt` — generated self-description, drift-gated.
|
|
169
|
+
- `nav-manifest.json` — the four routes this pair contributes.
|