@stapel/listings-react 0.20.0 → 0.22.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 +75 -0
- package/dist/api/generated/schema.d.ts +125 -2
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/listingsApi.d.ts +20 -1
- package/dist/api/listingsApi.d.ts.map +1 -1
- package/dist/api/listingsApi.js +1 -0
- package/dist/api/listingsApi.js.map +1 -1
- package/dist/api/types.d.ts +32 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js +7 -0
- package/dist/api/types.js.map +1 -1
- package/dist/default/ListingCard.d.ts +11 -0
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +86 -7
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingPhoto.d.ts +40 -0
- package/dist/default/ListingPhoto.d.ts.map +1 -1
- package/dist/default/ListingPhoto.js +53 -6
- package/dist/default/ListingPhoto.js.map +1 -1
- package/dist/default/ListingSerpCard.d.ts.map +1 -1
- package/dist/default/ListingSerpCard.js +14 -4
- package/dist/default/ListingSerpCard.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts +19 -1
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +99 -12
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/headless/ListingActions.d.ts +64 -13
- package/dist/headless/ListingActions.d.ts.map +1 -1
- package/dist/headless/ListingActions.js +60 -5
- package/dist/headless/ListingActions.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +8 -2
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +20 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +52 -1
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +12 -2
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/model/mine.d.ts +20 -0
- package/dist/model/mine.d.ts.map +1 -1
- package/dist/model/mine.js +22 -0
- package/dist/model/mine.js.map +1 -1
- package/dist/model/mutations.d.ts +24 -1
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +28 -0
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +26 -9
- package/dist/model/status.js.map +1 -1
- package/dist/model/transitions.d.ts +38 -1
- package/dist/model/transitions.d.ts.map +1 -1
- package/dist/model/transitions.js +73 -1
- package/dist/model/transitions.js.map +1 -1
- package/llms.txt +4 -2
- package/manifest.json +26 -2
- package/nav-manifest.json +1 -1
- package/package.json +5 -5
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +125 -2
- package/src/api/listingsApi.ts +27 -0
- package/src/api/types.ts +47 -2
- package/src/default/ListingCard.tsx +115 -12
- package/src/default/ListingPhoto.tsx +102 -18
- package/src/default/ListingSerpCard.tsx +17 -4
- package/src/default/MyListingsPane.tsx +205 -39
- package/src/headless/ListingActions.tsx +137 -16
- package/src/i18n/es.ts +8 -2
- package/src/i18n/keys.ts +52 -1
- package/src/i18n/ru.ts +12 -2
- package/src/index.ts +19 -3
- package/src/model/mine.ts +23 -0
- package/src/model/mutations.ts +41 -0
- package/src/model/status.ts +27 -9
- package/src/model/transitions.ts +80 -1
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @stapel/listings-react 0.
|
|
1
|
+
# @stapel/listings-react 0.22.0
|
|
2
2
|
|
|
3
|
-
Headless React flow pair for stapel-listings (contract >=0.
|
|
3
|
+
Headless React flow pair for stapel-listings (contract >=0.21 <0.22) — business + state, zero visual opinion.
|
|
4
4
|
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
5
5
|
verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
|
|
6
6
|
|
|
@@ -36,6 +36,7 @@ Paths are relative to `/listings/api/v1/`.
|
|
|
36
36
|
- listings_api_v1_listings_retrieve: GET /listings/{id}/
|
|
37
37
|
- listings_api_v1_listings_save_draft_create: POST /listings/{id}/save-draft/
|
|
38
38
|
- listings_api_v1_listings_status_retrieve: GET /listings/{id}/status/
|
|
39
|
+
- listings_api_v1_listings_transition_create: POST /listings/{id}/transition/
|
|
39
40
|
- listings_api_v1_listings_unfavorite_create: POST /listings/{id}/unfavorite/
|
|
40
41
|
- listings_api_v1_listings_update: PUT /listings/{id}/
|
|
41
42
|
- listings_api_v1_listings_validate_draft_retrieve: GET /listings/{id}/validate-draft/
|
|
@@ -54,6 +55,7 @@ Paths are relative to `/listings/api/v1/`.
|
|
|
54
55
|
- useMyFavorites (query) → myFavorites
|
|
55
56
|
- usePublishListing (mutation) → publish
|
|
56
57
|
- useSaveDraft (mutation) → saveDraft
|
|
58
|
+
- useTransitionListing (mutation) → transition
|
|
57
59
|
- useValidateDraft (query) → validateDraft
|
|
58
60
|
|
|
59
61
|
## Errors (render t(code, params); UX from remediation)
|
package/manifest.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
|
|
3
3
|
"package": "@stapel/listings-react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-listings",
|
|
7
|
-
"contract": ">=0.
|
|
7
|
+
"contract": ">=0.21 <0.22"
|
|
8
8
|
},
|
|
9
9
|
"layers": [
|
|
10
10
|
"api",
|
|
@@ -121,6 +121,13 @@
|
|
|
121
121
|
"request": null,
|
|
122
122
|
"response": "ListingStatus"
|
|
123
123
|
},
|
|
124
|
+
"listings_api_v1_listings_transition_create": {
|
|
125
|
+
"method": "POST",
|
|
126
|
+
"path": "/listings/api/v1/listings/{id}/transition/",
|
|
127
|
+
"tag": "listings",
|
|
128
|
+
"request": "ListingTransitionRequest",
|
|
129
|
+
"response": "ListingActionResponse"
|
|
130
|
+
},
|
|
124
131
|
"listings_api_v1_listings_unfavorite_create": {
|
|
125
132
|
"method": "POST",
|
|
126
133
|
"path": "/listings/api/v1/listings/{id}/unfavorite/",
|
|
@@ -263,6 +270,10 @@
|
|
|
263
270
|
"kind": "mutation",
|
|
264
271
|
"operation": "saveDraft"
|
|
265
272
|
},
|
|
273
|
+
"useTransitionListing": {
|
|
274
|
+
"kind": "mutation",
|
|
275
|
+
"operation": "transition"
|
|
276
|
+
},
|
|
266
277
|
"useValidateDraft": {
|
|
267
278
|
"kind": "query",
|
|
268
279
|
"operation": "validateDraft",
|
|
@@ -1058,11 +1069,17 @@
|
|
|
1058
1069
|
"listings.mine.live_under_review",
|
|
1059
1070
|
"listings.mine.load_failed",
|
|
1060
1071
|
"listings.mine.loading",
|
|
1072
|
+
"listings.mine.move.draft",
|
|
1073
|
+
"listings.mine.move.paused",
|
|
1074
|
+
"listings.mine.move.pending",
|
|
1075
|
+
"listings.mine.move.published",
|
|
1076
|
+
"listings.mine.move.renew",
|
|
1061
1077
|
"listings.mine.retry",
|
|
1062
1078
|
"listings.mine.tab.active",
|
|
1063
1079
|
"listings.mine.tab.archived",
|
|
1064
1080
|
"listings.mine.tab.drafts",
|
|
1065
1081
|
"listings.mine.title",
|
|
1082
|
+
"listings.mine.view",
|
|
1066
1083
|
"listings.moderation.first_review",
|
|
1067
1084
|
"listings.moderation.live_edit_pending",
|
|
1068
1085
|
"listings.moderation.live_needs_review",
|
|
@@ -1123,6 +1140,7 @@
|
|
|
1123
1140
|
"MY_LISTINGS_TAB_STATUSES",
|
|
1124
1141
|
"MY_LISTINGS_UNTABBED_STATUSES",
|
|
1125
1142
|
"MyListings",
|
|
1143
|
+
"OWNER_TRANSITIONS",
|
|
1126
1144
|
"PRICE_FIELD",
|
|
1127
1145
|
"TITLE_FIELD",
|
|
1128
1146
|
"asFeatureDaoList",
|
|
@@ -1164,6 +1182,7 @@
|
|
|
1164
1182
|
"myListingPrice",
|
|
1165
1183
|
"myListingTitle",
|
|
1166
1184
|
"navEntries",
|
|
1185
|
+
"ownerMoves",
|
|
1167
1186
|
"pageKey",
|
|
1168
1187
|
"publishRefusal",
|
|
1169
1188
|
"registerListingsI18n",
|
|
@@ -1199,6 +1218,7 @@
|
|
|
1199
1218
|
"useMyListings",
|
|
1200
1219
|
"usePublishListing",
|
|
1201
1220
|
"useSaveDraft",
|
|
1221
|
+
"useTransitionListing",
|
|
1202
1222
|
"useValidateDraft",
|
|
1203
1223
|
"withEngagement"
|
|
1204
1224
|
],
|
|
@@ -1241,11 +1261,14 @@
|
|
|
1241
1261
|
"ListingLifecycleStatus",
|
|
1242
1262
|
"ListingLocation",
|
|
1243
1263
|
"ListingModerationStatus",
|
|
1264
|
+
"ListingMove",
|
|
1265
|
+
"ListingOwnerTransition",
|
|
1244
1266
|
"ListingPageKey",
|
|
1245
1267
|
"ListingPageParams",
|
|
1246
1268
|
"ListingStatusInfo",
|
|
1247
1269
|
"ListingStatusTone",
|
|
1248
1270
|
"ListingStatusView",
|
|
1271
|
+
"ListingTransitionInput",
|
|
1249
1272
|
"ListingsApi",
|
|
1250
1273
|
"ListingsErrorCode",
|
|
1251
1274
|
"ListingsErrorSpec",
|
|
@@ -1269,6 +1292,7 @@
|
|
|
1269
1292
|
"SaveDraftInput",
|
|
1270
1293
|
"Schemas",
|
|
1271
1294
|
"UseFavoritesOptions",
|
|
1295
|
+
"UseListingActionsOptions",
|
|
1272
1296
|
"UseListingComposerOptions",
|
|
1273
1297
|
"UseListingDetailOptions",
|
|
1274
1298
|
"UseListingEngagementOverlayOptions",
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/listings-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Headless React pair for stapel-listings: a typed client over the draft twin, the composer that turns a category schema into a submittable listing, and the owner's dashboard. Two independent axes are rendered as two — lifecycle `status` decides visibility and `moderation_status` decides nothing about it, so an edit to a live listing stays live and says 'under review' instead of vanishing. Feature values are drawn and mirrored by @stapel/attributes-react; photos arrive as an injected upload bag whose `refs` ARE `images_draft` and whose `settled` gates the submit; a publish refusal is routed onto the control that caused it by slug. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"limit": "14 KB"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets. Raised 20 -> 21 KB (measured 20.22) for the engagement wave, which is four small things and no new component: the disclosure became a controlled open/close so a pointer that hovers and then clicks does not shut the only explanation the control has; the heart draws a filled accent when saved and states a failed save through the shared ErrorAlert; an already-seen card dims through one rule in the sheet all three cards already hoist; and the listing page gained a view-count row. 216 bytes is what a control that reports its own outcome costs",
|
|
56
|
+
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets. Raised 20 -> 21 KB (measured 20.22) for the engagement wave, which is four small things and no new component: the disclosure became a controlled open/close so a pointer that hovers and then clicks does not shut the only explanation the control has; the heart draws a filled accent when saved and states a failed save through the shared ErrorAlert; an already-seen card dims through one rule in the sheet all three cards already hoist; and the listing page gained a view-count row. 216 bytes is what a control that reports its own outcome costs. Raised 21 -> 22 KB for the seller's board becoming a door in both directions: the transition route and useTransitionListing, OWNER_TRANSITIONS/ownerMoves as the fallback mirror, ListingActionsBag.moves drawn in a stable order, listingHref/linkComponent on the cabinet pane, the refusal that stops quoting the wire, and the row that fits a phone. Measured on a clean tree, this package's src reverted to the previous commit with dependencies held constant: 20.82 -> 21.60 KB. The old ceiling had 180 B of room and the work needs 780; none of it is copy, and a fourth axis of owner state is not a rounding error",
|
|
57
57
|
"path": "dist/default/index.js",
|
|
58
|
-
"limit": "
|
|
58
|
+
"limit": "22 KB"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
|
|
@@ -103,12 +103,12 @@
|
|
|
103
103
|
"typescript": "^5.8.3",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
105
|
"@stapel/attributes-react": "^0.14.1",
|
|
106
|
-
"@stapel/core": "^0.
|
|
106
|
+
"@stapel/core": "^0.24.0",
|
|
107
107
|
"@stapel/currencies-react": "^0.3.0",
|
|
108
108
|
"@stapel/image": "^0.4.2",
|
|
109
109
|
"@stapel/showcase": "^0.3.0",
|
|
110
110
|
"@stapel/tokens": "^0.7.0",
|
|
111
|
-
"@stapel/tokens-antd": "^0.16.
|
|
111
|
+
"@stapel/tokens-antd": "^0.16.1"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22"
|
|
@@ -260,6 +260,40 @@ export interface paths {
|
|
|
260
260
|
patch?: never;
|
|
261
261
|
trace?: never;
|
|
262
262
|
};
|
|
263
|
+
"/listings/api/v1/listings/{id}/transition/": {
|
|
264
|
+
parameters: {
|
|
265
|
+
query?: never;
|
|
266
|
+
header?: never;
|
|
267
|
+
path?: never;
|
|
268
|
+
cookie?: never;
|
|
269
|
+
};
|
|
270
|
+
get?: never;
|
|
271
|
+
put?: never;
|
|
272
|
+
/**
|
|
273
|
+
* @description Move the listing to ``to``, if that is a move its OWNER may make.
|
|
274
|
+
*
|
|
275
|
+
* The seller's way forward, as one route instead of one endpoint per
|
|
276
|
+
* edge. ``archive`` and ``complete`` were the only two the API ever
|
|
277
|
+
* offered, and both of them are exits — so a listing that reached
|
|
278
|
+
* ARCHIVED, PAUSED, EXPIRED, SOLD, REJECTED or BLOCKED had no call left
|
|
279
|
+
* that would move it, and the cabinet correctly showed its owner
|
|
280
|
+
* nothing but «удалить».
|
|
281
|
+
*
|
|
282
|
+
* Which moves exist is ``models.OWNER_TRANSITIONS``, and it is the same
|
|
283
|
+
* list ``available_transitions`` puts on the card — the point of routing
|
|
284
|
+
* every edge through one allowlist is that the set a client is offered
|
|
285
|
+
* and the set the server accepts are one object, not two that agree
|
|
286
|
+
* today.
|
|
287
|
+
*
|
|
288
|
+
* **Permissions:** `IsAuthenticated`
|
|
289
|
+
*/
|
|
290
|
+
post: operations["listings_api_v1_listings_transition_create"];
|
|
291
|
+
delete?: never;
|
|
292
|
+
options?: never;
|
|
293
|
+
head?: never;
|
|
294
|
+
patch?: never;
|
|
295
|
+
trace?: never;
|
|
296
|
+
};
|
|
263
297
|
"/listings/api/v1/listings/{id}/unfavorite/": {
|
|
264
298
|
parameters: {
|
|
265
299
|
query?: never;
|
|
@@ -437,6 +471,8 @@ export interface paths {
|
|
|
437
471
|
export type webhooks = Record<string, never>;
|
|
438
472
|
export interface components {
|
|
439
473
|
schemas: {
|
|
474
|
+
/** @enum {string} */
|
|
475
|
+
AvailableTransitionsEnum: "draft" | "pending" | "published" | "paused" | "expired" | "sold" | "rejected" | "blocked" | "archived";
|
|
440
476
|
/** @description Serializer for boolean feature DAO. */
|
|
441
477
|
BoolDao: {
|
|
442
478
|
name?: string | null;
|
|
@@ -815,6 +851,11 @@ export interface components {
|
|
|
815
851
|
lat?: string | null;
|
|
816
852
|
/** Format: decimal */
|
|
817
853
|
lon?: string | null;
|
|
854
|
+
/**
|
|
855
|
+
* Format: double
|
|
856
|
+
* @description How wide the area `lat`/`lon` describe, in kilometres. On a PUBLIC read this is ~1.113 (the pair is rounded to two decimals and `geohash` comes back empty): draw a CIRCLE, never a marker — the listing is somewhere in it, and for a private seller the true point is a home address. `0` means the exact point, which only the listing's own owner, staff and the service transport get. Proximity itself is unaffected: `distance_km` on a search hit is computed server-side from the true coordinates.
|
|
857
|
+
*/
|
|
858
|
+
readonly geo_precision_km: number;
|
|
818
859
|
countable?: boolean;
|
|
819
860
|
/** Format: int64 */
|
|
820
861
|
stock_quantity?: number | null;
|
|
@@ -845,6 +886,11 @@ export interface components {
|
|
|
845
886
|
lat?: string | null;
|
|
846
887
|
/** Format: decimal */
|
|
847
888
|
lon?: string | null;
|
|
889
|
+
/**
|
|
890
|
+
* Format: double
|
|
891
|
+
* @description How wide the area `lat`/`lon` describe, in kilometres. On a PUBLIC read this is ~1.113 (the pair is rounded to two decimals and `geohash` comes back empty): draw a CIRCLE, never a marker — the listing is somewhere in it, and for a private seller the true point is a home address. `0` means the exact point, which only the listing's own owner, staff and the service transport get. Proximity itself is unaffected: `distance_km` on a search hit is computed server-side from the true coordinates.
|
|
892
|
+
*/
|
|
893
|
+
readonly geo_precision_km: number;
|
|
848
894
|
readonly features: (components["schemas"]["FeatureDao"] | {
|
|
849
895
|
slug?: string;
|
|
850
896
|
type?: string;
|
|
@@ -981,13 +1027,29 @@ export interface components {
|
|
|
981
1027
|
owner_id: string;
|
|
982
1028
|
};
|
|
983
1029
|
/**
|
|
984
|
-
* @description
|
|
1030
|
+
* @description The body of ``POST listings/{id}/transition/``: where to move it.
|
|
1031
|
+
*
|
|
1032
|
+
* A ``ChoiceField`` over the whole lifecycle rather than over the moves this
|
|
1033
|
+
* particular listing has, because the two refusals are different sentences
|
|
1034
|
+
* and a client should be able to tell them apart: a status that does not
|
|
1035
|
+
* exist is a 400 (the caller is confused about the vocabulary), a status
|
|
1036
|
+
* that exists but is not this listing's to reach is a 409 with
|
|
1037
|
+
* ``from_status`` (the caller is confused about the row). Narrowing the
|
|
1038
|
+
* field to the per-row set would collapse both into 400 and lose the
|
|
1039
|
+
* ``from_status`` that tells a storefront what to re-render.
|
|
1040
|
+
*/
|
|
1041
|
+
ListingTransitionRequest: {
|
|
1042
|
+
to: components["schemas"]["ToEnum"];
|
|
1043
|
+
};
|
|
1044
|
+
/**
|
|
1045
|
+
* @description * `not_submitted` - Not submitted for review
|
|
1046
|
+
* * `pending` - Pending Review
|
|
985
1047
|
* * `approved` - Approved
|
|
986
1048
|
* * `rejected` - Rejected
|
|
987
1049
|
* * `needs_review` - Needs Manual Review
|
|
988
1050
|
* @enum {string}
|
|
989
1051
|
*/
|
|
990
|
-
ModerationStatusEnum: "pending" | "approved" | "rejected" | "needs_review";
|
|
1052
|
+
ModerationStatusEnum: "not_submitted" | "pending" | "approved" | "rejected" | "needs_review";
|
|
991
1053
|
/** @description Listing counts by tab for the current user. */
|
|
992
1054
|
MyCountersResponse: {
|
|
993
1055
|
active: number;
|
|
@@ -1059,6 +1121,11 @@ export interface components {
|
|
|
1059
1121
|
lat?: string | null;
|
|
1060
1122
|
/** Format: decimal */
|
|
1061
1123
|
lon?: string | null;
|
|
1124
|
+
/**
|
|
1125
|
+
* Format: double
|
|
1126
|
+
* @description How wide the area `lat`/`lon` describe, in kilometres. On a PUBLIC read this is ~1.113 (the pair is rounded to two decimals and `geohash` comes back empty): draw a CIRCLE, never a marker — the listing is somewhere in it, and for a private seller the true point is a home address. `0` means the exact point, which only the listing's own owner, staff and the service transport get. Proximity itself is unaffected: `distance_km` on a search hit is computed server-side from the true coordinates.
|
|
1127
|
+
*/
|
|
1128
|
+
readonly geo_precision_km: number;
|
|
1062
1129
|
countable?: boolean;
|
|
1063
1130
|
/** Format: int64 */
|
|
1064
1131
|
stock_quantity?: number | null;
|
|
@@ -1067,6 +1134,7 @@ export interface components {
|
|
|
1067
1134
|
readonly viewed: boolean | null;
|
|
1068
1135
|
readonly view_count: number;
|
|
1069
1136
|
moderation_status?: components["schemas"]["ModerationStatusEnum"];
|
|
1137
|
+
readonly available_transitions: components["schemas"]["AvailableTransitionsEnum"][];
|
|
1070
1138
|
title_draft?: string;
|
|
1071
1139
|
/** Format: decimal */
|
|
1072
1140
|
price_draft?: string | null;
|
|
@@ -1286,6 +1354,19 @@ export interface components {
|
|
|
1286
1354
|
* @enum {string}
|
|
1287
1355
|
*/
|
|
1288
1356
|
StyleEnum: "l" | "m";
|
|
1357
|
+
/**
|
|
1358
|
+
* @description * `draft` - Draft
|
|
1359
|
+
* * `pending` - Pending Moderation
|
|
1360
|
+
* * `published` - Published
|
|
1361
|
+
* * `paused` - Paused
|
|
1362
|
+
* * `expired` - Expired
|
|
1363
|
+
* * `sold` - Sold
|
|
1364
|
+
* * `rejected` - Rejected
|
|
1365
|
+
* * `blocked` - Blocked (moderation takedown)
|
|
1366
|
+
* * `archived` - Archived
|
|
1367
|
+
* @enum {string}
|
|
1368
|
+
*/
|
|
1369
|
+
ToEnum: "draft" | "pending" | "published" | "paused" | "expired" | "sold" | "rejected" | "blocked" | "archived";
|
|
1289
1370
|
/**
|
|
1290
1371
|
* @description * `convertible_unit` - convertible_unit
|
|
1291
1372
|
* @enum {string}
|
|
@@ -1663,6 +1744,48 @@ export interface operations {
|
|
|
1663
1744
|
};
|
|
1664
1745
|
};
|
|
1665
1746
|
};
|
|
1747
|
+
listings_api_v1_listings_transition_create: {
|
|
1748
|
+
parameters: {
|
|
1749
|
+
query?: never;
|
|
1750
|
+
header?: never;
|
|
1751
|
+
path: {
|
|
1752
|
+
/** @description A unique integer value identifying this listing. */
|
|
1753
|
+
id: number;
|
|
1754
|
+
};
|
|
1755
|
+
cookie?: never;
|
|
1756
|
+
};
|
|
1757
|
+
requestBody: {
|
|
1758
|
+
content: {
|
|
1759
|
+
"application/json": components["schemas"]["ListingTransitionRequest"];
|
|
1760
|
+
"application/x-www-form-urlencoded": components["schemas"]["ListingTransitionRequest"];
|
|
1761
|
+
"multipart/form-data": components["schemas"]["ListingTransitionRequest"];
|
|
1762
|
+
};
|
|
1763
|
+
};
|
|
1764
|
+
responses: {
|
|
1765
|
+
200: {
|
|
1766
|
+
headers: {
|
|
1767
|
+
[name: string]: unknown;
|
|
1768
|
+
};
|
|
1769
|
+
content: {
|
|
1770
|
+
"application/json": components["schemas"]["ListingActionResponse"];
|
|
1771
|
+
};
|
|
1772
|
+
};
|
|
1773
|
+
/** @description No response body */
|
|
1774
|
+
400: {
|
|
1775
|
+
headers: {
|
|
1776
|
+
[name: string]: unknown;
|
|
1777
|
+
};
|
|
1778
|
+
content?: never;
|
|
1779
|
+
};
|
|
1780
|
+
/** @description No response body */
|
|
1781
|
+
409: {
|
|
1782
|
+
headers: {
|
|
1783
|
+
[name: string]: unknown;
|
|
1784
|
+
};
|
|
1785
|
+
content?: never;
|
|
1786
|
+
};
|
|
1787
|
+
};
|
|
1788
|
+
};
|
|
1666
1789
|
listings_api_v1_listings_unfavorite_create: {
|
|
1667
1790
|
parameters: {
|
|
1668
1791
|
query?: never;
|
package/src/api/listingsApi.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ListingEngagementBatch,
|
|
8
8
|
ListingDraft,
|
|
9
9
|
ListingDraftPatch,
|
|
10
|
+
ListingOwnerTransition,
|
|
10
11
|
ListingPageParams,
|
|
11
12
|
ListingStatusInfo,
|
|
12
13
|
MyCounters,
|
|
@@ -204,6 +205,29 @@ export interface ListingsApi {
|
|
|
204
205
|
/** Mark SOLD. Same 409 contract as {@link archive}. */
|
|
205
206
|
complete(id: number): Promise<ListingActionResponse>;
|
|
206
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Move the listing to `to` — ONE route for every edge a seller owns
|
|
210
|
+
* (`POST listings/{id}/transition/`, stapel-listings 0.20.0).
|
|
211
|
+
*
|
|
212
|
+
* {@link archive} and {@link complete} are still here and still work; they
|
|
213
|
+
* are two named exits, and for two releases they were the ONLY two the
|
|
214
|
+
* owner API had. That is what made a cabinet a one-way door: every status a
|
|
215
|
+
* seller could put a listing INTO — SOLD, ARCHIVED, PAUSED, EXPIRED — was
|
|
216
|
+
* one no call could get it out of, and `DELETE` was the only answer left.
|
|
217
|
+
*
|
|
218
|
+
* The set this accepts is `models.OWNER_TRANSITIONS`, which is the same set
|
|
219
|
+
* `MyListingCard.available_transitions` reports, deliberately: the moves a
|
|
220
|
+
* client is offered and the moves the server takes are one object rather
|
|
221
|
+
* than two that agree today. Two refusals, and they are different
|
|
222
|
+
* sentences — a status that does not exist is a 400, a status that exists
|
|
223
|
+
* but is not this row's to reach is `error.409.invalid_listing_transition`
|
|
224
|
+
* with `params.from_status`.
|
|
225
|
+
*/
|
|
226
|
+
transition(
|
|
227
|
+
id: number,
|
|
228
|
+
to: ListingOwnerTransition
|
|
229
|
+
): Promise<ListingActionResponse>;
|
|
230
|
+
|
|
207
231
|
/** Favourite a listing. Idempotent server-side (`get_or_create`). */
|
|
208
232
|
favorite(id: number): Promise<FavoriteToggleResponse>;
|
|
209
233
|
|
|
@@ -306,6 +330,9 @@ export function createListingsApi(client: StapelClient): ListingsApi {
|
|
|
306
330
|
|
|
307
331
|
complete: (id) => client.post(`${listingPath(id)}complete/`),
|
|
308
332
|
|
|
333
|
+
transition: (id, to) =>
|
|
334
|
+
client.post(`${listingPath(id)}transition/`, { to }),
|
|
335
|
+
|
|
309
336
|
favorite: (id) => client.post(`${listingPath(id)}favorite/`),
|
|
310
337
|
|
|
311
338
|
unfavorite: (id) => client.post(`${listingPath(id)}unfavorite/`),
|
package/src/api/types.ts
CHANGED
|
@@ -140,8 +140,27 @@ export type ListingDetail = WithOptionalEngagement<Schemas["ListingDetail"]>;
|
|
|
140
140
|
* type, for data no one can supply; it is precisely the case
|
|
141
141
|
* `<ListingEngagementScope>` exists to answer. A deployment running a server
|
|
142
142
|
* older than 0.16 is the same shape of fact.
|
|
143
|
+
*
|
|
144
|
+
* `geo_precision_km` is relaxed for the same reason and by the same argument.
|
|
145
|
+
* stapel-listings 0.21.0 puts it on every card this module serves — how wide
|
|
146
|
+
* an area `lat`/`lon` describe, so a reader draws a circle instead of pinning
|
|
147
|
+
* a private seller's front door with a marker — and the SEARCH document does
|
|
148
|
+
* not carry it, because stapel-search coarsens its own card independently
|
|
149
|
+
* (`CARD_COORD_PRECISION`, the same ~1.1km). Requiring it on the prop would
|
|
150
|
+
* make the pair's primary consumer unable to satisfy its own type for a
|
|
151
|
+
* number nobody in that path holds. A reader that needs the precision must
|
|
152
|
+
* treat its absence as UNSTATED and never as `0`: `0` means the exact point,
|
|
153
|
+
* which is the one reading that could publish an address.
|
|
143
154
|
*/
|
|
144
|
-
|
|
155
|
+
type WithOptionalPrecision<Row extends { geo_precision_km?: number }> = Omit<
|
|
156
|
+
Row,
|
|
157
|
+
"geo_precision_km"
|
|
158
|
+
> &
|
|
159
|
+
Partial<Pick<Schemas["ListingCard"], "geo_precision_km">>;
|
|
160
|
+
|
|
161
|
+
export type ListingCard = WithOptionalPrecision<
|
|
162
|
+
WithOptionalEngagement<Schemas["ListingCard"]>
|
|
163
|
+
>;
|
|
145
164
|
|
|
146
165
|
/** `POST /listings/` request+response and `POST /{pk}/save-draft/` response —
|
|
147
166
|
* the draft twin. Every user-editable field is a `*_draft` one, promoted onto
|
|
@@ -197,7 +216,9 @@ export type PaginatedListingCards = Omit<
|
|
|
197
216
|
* tab keyed off them is a column of blank rows. `myListingTitle` /
|
|
198
217
|
* `myListingPrice` (`model/mine.ts`) are the one place the fallback lives.
|
|
199
218
|
*/
|
|
200
|
-
export type MyListingCard =
|
|
219
|
+
export type MyListingCard = WithOptionalPrecision<
|
|
220
|
+
WithOptionalEngagement<Schemas["MyListingCard"]>
|
|
221
|
+
>;
|
|
201
222
|
|
|
202
223
|
/** The keyset envelope `GET /listings/my/listings/` comes back in — the same
|
|
203
224
|
* `IDAnchorPagination` shape as {@link PaginatedListingCards}, over the owner
|
|
@@ -232,9 +253,33 @@ export const LISTING_STATUSES: readonly ListingLifecycleStatus[] = [
|
|
|
232
253
|
* only one (`models.py`: "no visibility-reads-moderation_status coupling"). */
|
|
233
254
|
export type ListingLifecycleStatus = Schemas["StatusD41Enum"];
|
|
234
255
|
|
|
256
|
+
/**
|
|
257
|
+
* One move the OWNER of a listing may make from where it is now — the third
|
|
258
|
+
* axis, and the one stapel-listings 0.20.0 added because the first two cannot
|
|
259
|
+
* answer it.
|
|
260
|
+
*
|
|
261
|
+
* `status` says where the listing IS and `moderation_status` says what is
|
|
262
|
+
* being waited on. Neither says *what can I do about it*, and a dashboard
|
|
263
|
+
* that works it out for itself is re-implementing `models.OWNER_TRANSITIONS`
|
|
264
|
+
* from the outside — which is how a cabinet ends up drawing "Mark sold" on a
|
|
265
|
+
* listing that is already sold, and drawing no way back at all.
|
|
266
|
+
*
|
|
267
|
+
* The values are lifecycle states (the DESTINATION of a move), so this is
|
|
268
|
+
* `ListingLifecycleStatus` narrowed by the server rather than a second
|
|
269
|
+
* vocabulary.
|
|
270
|
+
*/
|
|
271
|
+
export type ListingOwnerTransition = Schemas["AvailableTransitionsEnum"];
|
|
272
|
+
|
|
235
273
|
/** The four content-moderation states, as `models.ModerationStatus` declares
|
|
236
274
|
* them. Independent of the lifecycle: see `model/status.ts`. */
|
|
237
275
|
export const MODERATION_STATUSES: readonly ListingModerationStatus[] = [
|
|
276
|
+
// NOBODY HAS ASKED YET, and it is the DEFAULT (stapel-listings 0.20.0).
|
|
277
|
+
// Before it, a draft was born `pending` and every dashboard row announced a
|
|
278
|
+
// moderation decision that no case existed behind — one live stand held 167
|
|
279
|
+
// of them. The distinction lives in the data because every reader asks the
|
|
280
|
+
// same question and would each otherwise re-derive "…unless it was never
|
|
281
|
+
// submitted" from a second column.
|
|
282
|
+
"not_submitted",
|
|
238
283
|
"pending",
|
|
239
284
|
"approved",
|
|
240
285
|
"rejected",
|