@stapel/listings-react 0.26.0 → 0.28.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 +129 -0
- package/MODULE.md +3 -1
- package/README.md +102 -8
- package/dist/api/generated/schema.d.ts +20 -10
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/ListingActions.d.ts +8 -1
- package/dist/default/ListingActions.d.ts.map +1 -1
- package/dist/default/ListingActions.js +1 -1
- package/dist/default/ListingActions.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts +148 -9
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +131 -19
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +10 -6
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/default/ShareAction.d.ts +16 -1
- package/dist/default/ShareAction.d.ts.map +1 -1
- package/dist/default/ShareAction.js +16 -9
- package/dist/default/ShareAction.js.map +1 -1
- package/dist/default/actionRow.d.ts +24 -0
- package/dist/default/actionRow.d.ts.map +1 -1
- package/dist/default/actionRow.js +50 -7
- package/dist/default/actionRow.js.map +1 -1
- package/dist/default/detailGallery.d.ts +77 -0
- package/dist/default/detailGallery.d.ts.map +1 -0
- package/dist/default/detailGallery.js +91 -0
- package/dist/default/detailGallery.js.map +1 -0
- package/dist/default/index.d.ts +4 -2
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +5 -1
- package/dist/default/index.js.map +1 -1
- package/dist/default/movableCluster.d.ts +18 -0
- package/dist/default/movableCluster.d.ts.map +1 -0
- package/dist/default/movableCluster.js +110 -0
- package/dist/default/movableCluster.js.map +1 -0
- package/dist/headless/MyListings.d.ts +31 -10
- package/dist/headless/MyListings.d.ts.map +1 -1
- package/dist/headless/MyListings.js +19 -2
- package/dist/headless/MyListings.js.map +1 -1
- package/dist/headless/Share.d.ts +60 -2
- package/dist/headless/Share.d.ts.map +1 -1
- package/dist/headless/Share.js +55 -3
- package/dist/headless/Share.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/model/cardBadges.d.ts +25 -0
- package/dist/model/cardBadges.d.ts.map +1 -1
- package/dist/model/cardBadges.js +41 -5
- package/dist/model/cardBadges.js.map +1 -1
- package/dist/model/draft.d.ts +42 -1
- package/dist/model/draft.d.ts.map +1 -1
- package/dist/model/draft.js +40 -3
- package/dist/model/draft.js.map +1 -1
- package/dist/model/status.d.ts +31 -16
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +30 -15
- package/dist/model/status.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +8 -1
- package/nav-manifest.json +1 -1
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +20 -10
- package/src/default/ListingActions.tsx +9 -1
- package/src/default/ListingDetailPane.tsx +316 -18
- package/src/default/MyListingsPane.tsx +10 -6
- package/src/default/ShareAction.tsx +36 -10
- package/src/default/actionRow.ts +55 -7
- package/src/default/detailGallery.ts +97 -0
- package/src/default/index.ts +15 -1
- package/src/default/movableCluster.tsx +137 -0
- package/src/headless/MyListings.tsx +52 -12
- package/src/headless/Share.tsx +98 -5
- package/src/index.ts +7 -0
- package/src/model/cardBadges.ts +42 -5
- package/src/model/draft.ts +61 -3
- package/src/model/status.ts +31 -16
package/src/model/draft.ts
CHANGED
|
@@ -224,6 +224,28 @@ function toWireFeatures(
|
|
|
224
224
|
return toFeaturesDto(features, values) as unknown as WireFeaturesDraft;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** One field of the `save-draft` body, by the name the wire uses. */
|
|
228
|
+
export type ListingDraftField = keyof ListingDraftPatch;
|
|
229
|
+
|
|
230
|
+
/** Options for {@link draftPatchFromValues}. */
|
|
231
|
+
export interface DraftPatchOptions {
|
|
232
|
+
/**
|
|
233
|
+
* NAME THE FIELDS THIS SAVE IS WRITING, and the body carries no others.
|
|
234
|
+
*
|
|
235
|
+
* `save-draft` REPLACES every field in the body — it does not merge — so a
|
|
236
|
+
* body is not "the values I have", it is "the fields I am claiming". A save
|
|
237
|
+
* fired by one control (a photo settling, a blurred title) previously
|
|
238
|
+
* claimed all fourteen, which is only harmless while every one of them is
|
|
239
|
+
* loaded and true.
|
|
240
|
+
*
|
|
241
|
+
* Given, only these keys are spelled; the omission rules below still apply
|
|
242
|
+
* inside the selection, so naming `features_draft` without a schema still
|
|
243
|
+
* writes nothing. Absent, the whole body is sent, which is right for the
|
|
244
|
+
* composer's own save — it holds every value on the form.
|
|
245
|
+
*/
|
|
246
|
+
readonly fields?: readonly ListingDraftField[];
|
|
247
|
+
}
|
|
248
|
+
|
|
227
249
|
/**
|
|
228
250
|
* The composer's values → the `save-draft` body.
|
|
229
251
|
*
|
|
@@ -234,12 +256,34 @@ function toWireFeatures(
|
|
|
234
256
|
* (`error.400.listing_feature_not_allowed`), which is why
|
|
235
257
|
* {@link retainKnownFeatureValues} prunes on the way in rather than letting
|
|
236
258
|
* the server explain it.
|
|
259
|
+
*
|
|
260
|
+
* ── NO SCHEMA IS NOT AN EMPTY ANSWER SHEET ───────────────────────────────
|
|
261
|
+
*
|
|
262
|
+
* An absent or empty `features` used to produce `features_draft: {}`, and
|
|
263
|
+
* `save-draft` REPLACES that map rather than merging into it — so a save that
|
|
264
|
+
* left before the category's schema arrived DELETED every characteristic the
|
|
265
|
+
* row was holding, while the form on screen still showed them. Measured by a
|
|
266
|
+
* live container over two cold loads of one draft: a reopen's own
|
|
267
|
+
* settled-photo save fires in the first commit, before the row's category has
|
|
268
|
+
* even been adopted, and the row alternated between the draft's answers and
|
|
269
|
+
* none of them (a client storefront's README, "Named gaps").
|
|
270
|
+
*
|
|
271
|
+
* `{}` is a claim — "this listing has no characteristics" — and a caller with
|
|
272
|
+
* no schema is in no position to make it. So the key is OMITTED entirely
|
|
273
|
+
* whenever there is no schema to tag values with, and `save-draft` then
|
|
274
|
+
* leaves the stored map exactly as it was. A category that genuinely declares
|
|
275
|
+
* no features writes nothing either, which is the same answer arrived at
|
|
276
|
+
* honestly: there is nothing to say and the row already says it.
|
|
277
|
+
*
|
|
278
|
+
* {@link DraftPatchOptions.fields} is the general form of the same rule — a
|
|
279
|
+
* save that names what it is writing cannot erase what it is not.
|
|
237
280
|
*/
|
|
238
281
|
export function draftPatchFromValues(
|
|
239
282
|
values: ListingDraftValues,
|
|
240
|
-
features: readonly FeatureDef[]
|
|
283
|
+
features: readonly FeatureDef[] | undefined,
|
|
284
|
+
options: DraftPatchOptions = {}
|
|
241
285
|
): ListingDraftPatch {
|
|
242
|
-
|
|
286
|
+
const full: ListingDraftPatch = {
|
|
243
287
|
// Omitted while unchosen rather than sent as `""`: a draft is allowed to
|
|
244
288
|
// have no category (0.21.4), and `""` is not "no category" on the wire —
|
|
245
289
|
// it is an empty id the serializer refuses. The category is written by
|
|
@@ -258,7 +302,11 @@ export function draftPatchFromValues(
|
|
|
258
302
|
// sent here is discarded. Sending one would be a claim the wire ignores.
|
|
259
303
|
lat_draft: values.location.lat,
|
|
260
304
|
lon_draft: values.location.lon,
|
|
261
|
-
|
|
305
|
+
// Omitted, not emptied, when there is no schema to tag with — see the
|
|
306
|
+
// doc above. `{}` is a claim about the listing; silence is not.
|
|
307
|
+
...(features !== undefined && features.length > 0
|
|
308
|
+
? { features_draft: toWireFeatures(features, values.features) }
|
|
309
|
+
: {}),
|
|
262
310
|
countable: values.countable,
|
|
263
311
|
// The pair mirrors the model's cross-field rule rather than sending a
|
|
264
312
|
// contradiction: a service carries no quantity, and `validate_countable
|
|
@@ -266,6 +314,16 @@ export function draftPatchFromValues(
|
|
|
266
314
|
stock_quantity: values.countable ? values.stockQuantity : null,
|
|
267
315
|
auto_republish: values.autoRepublish,
|
|
268
316
|
};
|
|
317
|
+
const named = options.fields;
|
|
318
|
+
if (named === undefined) return full;
|
|
319
|
+
// A selection, not a second body: the field's VALUE is still whatever the
|
|
320
|
+
// rules above produced, so a named field the rules omit stays omitted.
|
|
321
|
+
const wanted = new Set<string>(named);
|
|
322
|
+
const out: Record<string, unknown> = {};
|
|
323
|
+
for (const [key, value] of Object.entries(full)) {
|
|
324
|
+
if (wanted.has(key)) out[key] = value;
|
|
325
|
+
}
|
|
326
|
+
return out as ListingDraftPatch;
|
|
269
327
|
}
|
|
270
328
|
|
|
271
329
|
/**
|
package/src/model/status.ts
CHANGED
|
@@ -285,8 +285,8 @@ export function listingStatusView(
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
|
-
* The three tabs
|
|
289
|
-
* together.
|
|
288
|
+
* The three tabs whose ROWS come off a keyset page, and the statuses each one
|
|
289
|
+
* folds together.
|
|
290
290
|
*
|
|
291
291
|
* The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
|
|
292
292
|
* caption and its count cannot describe different sets: `active` includes
|
|
@@ -294,10 +294,17 @@ export function listingStatusView(
|
|
|
294
294
|
* both because a listing in those states is something the owner is still
|
|
295
295
|
* working on. A client that grouped them its own way would show "3 active"
|
|
296
296
|
* over two rows.
|
|
297
|
+
*
|
|
298
|
+
* The name is about the SOURCE, not about the counter: `MyCountersResponse`
|
|
299
|
+
* carries four integers since stapel-listings 0.22.4 (`blocked` joined the
|
|
300
|
+
* three), and this is still the set a {@link MyListingsCountedTab} —
|
|
301
|
+
* `MyListingsSource`'s whole parameter type — may name. The removed tab reads
|
|
302
|
+
* its rows off `?status=blocked` directly and unpaged, so a host that
|
|
303
|
+
* implemented the seam before the fourth tab existed is never handed it.
|
|
297
304
|
*/
|
|
298
305
|
export const MY_LISTINGS_COUNTED_TABS = ["active", "drafts", "archived"] as const;
|
|
299
306
|
|
|
300
|
-
/** One of the three tabs `
|
|
307
|
+
/** One of the three tabs a `MyListingsSource` is asked for. */
|
|
301
308
|
export type MyListingsCountedTab = (typeof MY_LISTINGS_COUNTED_TABS)[number];
|
|
302
309
|
|
|
303
310
|
const COUNTED_TAB_STATUSES: Readonly<
|
|
@@ -308,8 +315,9 @@ const COUNTED_TAB_STATUSES: Readonly<
|
|
|
308
315
|
archived: ["archived", "paused", "expired", "sold"],
|
|
309
316
|
};
|
|
310
317
|
|
|
311
|
-
/** Which of the three
|
|
312
|
-
* one
|
|
318
|
+
/** Which of the three keyset-paged tabs a status belongs to, or `undefined`
|
|
319
|
+
* for one none of them folds in — `blocked`, today, which has a tab and a
|
|
320
|
+
* counter of its own and a different row source under it. */
|
|
313
321
|
export function countedTabOf(
|
|
314
322
|
status: ListingLifecycleStatus
|
|
315
323
|
): MyListingsCountedTab | undefined {
|
|
@@ -320,13 +328,16 @@ export function countedTabOf(
|
|
|
320
328
|
}
|
|
321
329
|
|
|
322
330
|
/**
|
|
323
|
-
* The statuses the
|
|
324
|
-
*
|
|
331
|
+
* The statuses none of the three paged tabs folds in — `blocked`, and nothing
|
|
332
|
+
* else today.
|
|
325
333
|
*
|
|
326
334
|
* DERIVED, not written down a second time: a status added upstream and left
|
|
327
|
-
* out of the
|
|
328
|
-
*
|
|
329
|
-
* stale.
|
|
335
|
+
* out of the tab groupings lands here automatically and gets shown, which is
|
|
336
|
+
* the opposite of what a hardcoded `["blocked"]` would do the day it goes
|
|
337
|
+
* stale. That the server now has a `blocked` COUNTER for the same set does not
|
|
338
|
+
* make the derivation redundant: the counter names one status and this names
|
|
339
|
+
* whatever the three groupings leave over, which is the set the fourth tab
|
|
340
|
+
* must ASK for.
|
|
330
341
|
*/
|
|
331
342
|
export const MY_LISTINGS_UNTABBED_STATUSES: readonly ListingLifecycleStatus[] =
|
|
332
343
|
LISTING_STATUSES.filter((status) => countedTabOf(status) === undefined);
|
|
@@ -350,12 +361,16 @@ export const MY_LISTINGS_UNTABBED_STATUSES: readonly ListingLifecycleStatus[] =
|
|
|
350
361
|
* ── Why a fourth tab and not the archive ─────────────────────────────────
|
|
351
362
|
*
|
|
352
363
|
* Folding `blocked` into `archived` is the other shape this could take, and
|
|
353
|
-
* it
|
|
354
|
-
* archive tab holding takedowns would read the
|
|
355
|
-
* `0` — until the tab was opened and its rows
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* moment D407 was
|
|
364
|
+
* it cost the count: when D407 was written `my/counters` had three integers
|
|
365
|
+
* and no fourth, so an archive tab holding takedowns would have read the
|
|
366
|
+
* server's `archived` number — `0` — until the tab was opened and its rows
|
|
367
|
+
* could raise it. The fourth tab was counted from its OWN read
|
|
368
|
+
* (`?status=blocked`, unpaged) so that the number was right while the seller
|
|
369
|
+
* was looking at a different tab, which is exactly the moment D407 was
|
|
370
|
+
* measured at. stapel-listings 0.22.4 puts `blocked` in the counter, so the
|
|
371
|
+
* number is the server's now and that read is only the tab's rows — but the
|
|
372
|
+
* property it was there to defend is the same one, and it is now defended by
|
|
373
|
+
* the wire instead of by a page.
|
|
359
374
|
*
|
|
360
375
|
* The archive keeps its meaning too, which is not nothing: "I put this away"
|
|
361
376
|
* and "a moderator took this down" are not the same sentence and a tab that
|