@stapel/listings-react 0.22.0 → 0.23.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 +37 -0
- package/README.md +17 -6
- package/dist/api/listingsApi.d.ts +25 -0
- 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/default/FavoritesPane.d.ts +18 -2
- package/dist/default/FavoritesPane.d.ts.map +1 -1
- package/dist/default/FavoritesPane.js +3 -3
- package/dist/default/FavoritesPane.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts +22 -2
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +1 -1
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/default/index.d.ts +2 -2
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/ListingComposer.d.ts +16 -10
- package/dist/headless/ListingComposer.d.ts.map +1 -1
- package/dist/headless/ListingComposer.js +23 -4
- package/dist/headless/ListingComposer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/queries.d.ts +15 -1
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +20 -0
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +4 -0
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +1 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +26 -5
- package/dist/model/status.js.map +1 -1
- package/llms.txt +2 -1
- package/manifest.json +11 -1
- package/nav-manifest.json +1 -1
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/api/listingsApi.ts +30 -0
- package/src/default/FavoritesPane.tsx +20 -5
- package/src/default/MyListingsPane.tsx +23 -5
- package/src/default/index.ts +2 -1
- package/src/headless/ListingComposer.tsx +42 -17
- package/src/index.ts +1 -0
- package/src/model/queries.ts +25 -0
- package/src/model/queryKeys.ts +5 -0
- package/src/model/status.ts +26 -5
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
PublishResponse,
|
|
23
23
|
} from "../api/types.js";
|
|
24
24
|
import { useListingsRuntime } from "../model/context.js";
|
|
25
|
-
import { useListing } from "../model/queries.js";
|
|
25
|
+
import { useListing, useListingDraft } from "../model/queries.js";
|
|
26
26
|
import {
|
|
27
27
|
useCreateDraft,
|
|
28
28
|
usePublishListing,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
createDraftBody,
|
|
33
33
|
draftPatchFromValues,
|
|
34
34
|
draftValuesFromDetail,
|
|
35
|
+
draftValuesFromWire,
|
|
35
36
|
droppedFeatureSlugs,
|
|
36
37
|
emptyDraftValues,
|
|
37
38
|
retainKnownFeatureValues,
|
|
@@ -82,17 +83,23 @@ import { useMandateGate } from "./useMandateGate.js";
|
|
|
82
83
|
* the headless half must judge renderability without importing a skin, so
|
|
83
84
|
* the skin's `BUILTIN_VALUE_EDITOR_TYPES` is passed IN.
|
|
84
85
|
*
|
|
85
|
-
* ── Reopening a draft
|
|
86
|
+
* ── Reopening a draft, before and after stapel-listings 0.21.1 ─────────────
|
|
86
87
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
88
|
+
* `GET /{pk}/` serializes the PUBLISHED fields only (`ListingDetailSerializer`)
|
|
89
|
+
* and, before 0.21.1, `ListingDraftSerializer` appeared only as the RESPONSE
|
|
90
|
+
* of create / save-draft — no read returned the `*_draft` twin at all. So a
|
|
91
|
+
* draft abandoned and reopened came back empty, and a live listing's edit
|
|
92
|
+
* seeded from the published half instead (`draftValuesFromDetail`), which is
|
|
93
|
+
* what the person could actually see but not necessarily what they had last
|
|
94
|
+
* typed and not yet republished.
|
|
95
|
+
*
|
|
96
|
+
* 0.21.1 added the owner-only `GET /{pk}/draft/`, the exact `save-draft`
|
|
97
|
+
* response shape. The seeding effect now tries it first
|
|
98
|
+
* (`draftValuesFromWire`) for every reopened listing, live or not, and falls
|
|
99
|
+
* back to the published-half seed only when that read 404s — either nothing
|
|
100
|
+
* was ever saved, or the backend predates the route. `draftNotReadable`
|
|
101
|
+
* narrows to exactly that fallback case: the draft read failed AND the
|
|
102
|
+
* published half is empty too, so there is truly nothing to show.
|
|
96
103
|
*/
|
|
97
104
|
|
|
98
105
|
/** The two members of `@stapel/cdn-react`'s upload bag this composer needs.
|
|
@@ -264,6 +271,7 @@ export function useListingComposer(
|
|
|
264
271
|
const [saveRequest, setSaveRequest] = useState(0);
|
|
265
272
|
|
|
266
273
|
const existing = useListing(options.listingId);
|
|
274
|
+
const listingDraft = useListingDraft(options.listingId);
|
|
267
275
|
const createDraft = useCreateDraft();
|
|
268
276
|
const saveDraft = useSaveDraft();
|
|
269
277
|
const publishListing = usePublishListing();
|
|
@@ -272,21 +280,33 @@ export function useListingComposer(
|
|
|
272
280
|
// dependency guard because re-seeding on a refetch would throw away
|
|
273
281
|
// everything typed since — a background invalidation must never rewrite a
|
|
274
282
|
// form somebody is in the middle of.
|
|
283
|
+
//
|
|
284
|
+
// The draft-twin read (`GET {id}/draft/`, 0.21.1) is tried FIRST
|
|
285
|
+
// (`draftValuesFromWire`); the published-half seed (`draftValuesFromDetail`)
|
|
286
|
+
// is the fallback, taken once that read has settled to anything other than
|
|
287
|
+
// success — a 404 because nothing was ever saved, a 404 because this
|
|
288
|
+
// backend predates the route, or any other failure a composer must not
|
|
289
|
+
// hang on. Waiting for `listingDraft` to settle too (not only `existing`)
|
|
290
|
+
// is what stops a live listing with unpublished edits from flashing its
|
|
291
|
+
// published content before the true draft lands.
|
|
275
292
|
const seeded = useRef(false);
|
|
276
293
|
useEffect(() => {
|
|
277
294
|
if (seeded.current) return;
|
|
278
295
|
const detail = existing.data;
|
|
279
296
|
if (detail === undefined) return;
|
|
297
|
+
if (listingDraft.status === "pending") return;
|
|
280
298
|
seeded.current = true;
|
|
281
299
|
setListingId(detail.id);
|
|
282
300
|
setValues(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
listingDraft.status === "success"
|
|
302
|
+
? draftValuesFromWire(listingDraft.data, { currency: runtime.currency })
|
|
303
|
+
: draftValuesFromDetail(
|
|
304
|
+
detail,
|
|
305
|
+
featuresDtoFromDaoList(asFeatureDaoList(detail.features)) as FeaturesDto,
|
|
306
|
+
{ currency: runtime.currency }
|
|
307
|
+
)
|
|
288
308
|
);
|
|
289
|
-
}, [existing.data, runtime.currency]);
|
|
309
|
+
}, [existing.data, listingDraft.status, listingDraft.data, runtime.currency]);
|
|
290
310
|
|
|
291
311
|
// Pruning and SEEDING both run on the SCHEMA arriving, one render after the
|
|
292
312
|
// category changed: `setCategory` cannot prune against features it has not
|
|
@@ -368,9 +388,14 @@ export function useListingComposer(
|
|
|
368
388
|
);
|
|
369
389
|
|
|
370
390
|
const isLiveEdit = existing.data?.status === "published";
|
|
391
|
+
// Narrowed to the fallback case: the draft-twin read did not land (see the
|
|
392
|
+
// seeding effect above), and the published half is empty too, so there is
|
|
393
|
+
// truly nothing to show. A build on stapel-listings 0.21.1+ only reaches
|
|
394
|
+
// this when the row has genuinely never been saved into.
|
|
371
395
|
const draftNotReadable =
|
|
372
396
|
existing.data !== undefined &&
|
|
373
397
|
existing.data.status !== "published" &&
|
|
398
|
+
listingDraft.status !== "success" &&
|
|
374
399
|
(existing.data.title ?? "").length === 0 &&
|
|
375
400
|
(existing.data.description ?? "").length === 0;
|
|
376
401
|
|
package/src/index.ts
CHANGED
package/src/model/queries.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { StapelApiError } from "@stapel/core";
|
|
|
5
5
|
import type { ValidationBatchResult } from "@stapel/attributes-react";
|
|
6
6
|
import type {
|
|
7
7
|
ListingDetail,
|
|
8
|
+
ListingDraft,
|
|
8
9
|
ListingEngagementBatch,
|
|
9
10
|
ListingPageParams,
|
|
10
11
|
ListingStatusInfo,
|
|
@@ -55,6 +56,30 @@ export function useListing(
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The draft twin, read back (`GET /{pk}/draft/`, stapel-listings 0.21.1) —
|
|
61
|
+
* what `useListingComposer` seeds a reopened listing from.
|
|
62
|
+
*
|
|
63
|
+
* `retry: false` for the same reason as {@link useListing}: a 404 here is
|
|
64
|
+
* either "no draft was ever saved" or "this build predates the route", and
|
|
65
|
+
* three retries only delay the moment the composer can fall back to the
|
|
66
|
+
* detail seed. It is a separate query from `useListing` (own cache key),
|
|
67
|
+
* because a host that only reads the detail must not pay for this round
|
|
68
|
+
* trip, and a write to one must not evict the other.
|
|
69
|
+
*/
|
|
70
|
+
export function useListingDraft(
|
|
71
|
+
id: number | undefined,
|
|
72
|
+
options?: { readonly enabled?: boolean }
|
|
73
|
+
): UseQueryResult<ListingDraft, StapelApiError> {
|
|
74
|
+
const api = useListingsApi();
|
|
75
|
+
return useQuery({
|
|
76
|
+
queryKey: listingsQueryKeys.draft(id ?? -1),
|
|
77
|
+
queryFn: ({ signal }) => api.draft(id as number, { signal }),
|
|
78
|
+
enabled: (options?.enabled ?? true) && id !== undefined,
|
|
79
|
+
retry: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
/**
|
|
59
84
|
* The status probe — both axes plus `is_deleted`, for ANY listing id.
|
|
60
85
|
*
|
package/src/model/queryKeys.ts
CHANGED
|
@@ -63,6 +63,10 @@ export const listingsQueryKeys: {
|
|
|
63
63
|
allMine(): readonly ["listings", "my", "listings"];
|
|
64
64
|
/** `GET /{pk}/validate-draft/` — the dry run of a publish. */
|
|
65
65
|
validateDraft(id: number): readonly ["listings", "validate-draft", number];
|
|
66
|
+
/** `GET /{pk}/draft/` — the draft twin read back (stapel-listings 0.21.1).
|
|
67
|
+
* A separate entry from `detail`: the two answer different questions about
|
|
68
|
+
* the same id and a write to one must not evict the other's cache. */
|
|
69
|
+
draft(id: number): readonly ["listings", "draft", number];
|
|
66
70
|
/** Every favourites PAGE, for an invalidation after a toggle — the cursor
|
|
67
71
|
* is not known at the write, so the prefix is what a write can target. */
|
|
68
72
|
allFavorites(): readonly ["listings", "my", "favorites"];
|
|
@@ -91,6 +95,7 @@ export const listingsQueryKeys: {
|
|
|
91
95
|
mineUntabbed: () => [ROOT, "my", "listings", "untabbed"],
|
|
92
96
|
allMine: () => [ROOT, "my", "listings"],
|
|
93
97
|
validateDraft: (id) => [ROOT, "validate-draft", id],
|
|
98
|
+
draft: (id) => [ROOT, "draft", id],
|
|
94
99
|
allFavorites: () => [ROOT, "my", "favorites"],
|
|
95
100
|
allLists: () => [ROOT, "list"],
|
|
96
101
|
engagement: (ids) => [ROOT, "engagement", engagementIds(ids).join(",")],
|
package/src/model/status.ts
CHANGED
|
@@ -202,13 +202,34 @@ export function moderationNotice(
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
if (moderationStatus === "needs_review") {
|
|
205
|
+
// Same three-way split as `pending`, and for the same reason (D225): a
|
|
206
|
+
// manual-review verdict is exactly as capable of being OVERTAKEN by a
|
|
207
|
+
// lifecycle move as an automated one is, and this branch used to ignore
|
|
208
|
+
// that — a sold/paused/archived/draft row carrying a stale
|
|
209
|
+
// `needs_review` printed "a moderator is looking at this by hand" to an
|
|
210
|
+
// owner whose listing had not been on offer for however long the review
|
|
211
|
+
// has been sitting open.
|
|
212
|
+
if (live) {
|
|
213
|
+
return {
|
|
214
|
+
moderationStatus,
|
|
215
|
+
messageKey: LISTINGS_I18N_KEYS.moderationLiveNeedsReview,
|
|
216
|
+
tone: "waiting",
|
|
217
|
+
liveDuringReview: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (status === "pending") {
|
|
221
|
+
return {
|
|
222
|
+
moderationStatus,
|
|
223
|
+
messageKey: LISTINGS_I18N_KEYS.moderationNeedsReview,
|
|
224
|
+
tone: "waiting",
|
|
225
|
+
liveDuringReview: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
205
228
|
return {
|
|
206
229
|
moderationStatus,
|
|
207
|
-
messageKey:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
tone: "waiting",
|
|
211
|
-
liveDuringReview: live,
|
|
230
|
+
messageKey: LISTINGS_I18N_KEYS.moderationPendingOffline,
|
|
231
|
+
tone: "neutral",
|
|
232
|
+
liveDuringReview: false,
|
|
212
233
|
};
|
|
213
234
|
}
|
|
214
235
|
|