@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { ActionAvailability, FlowError, LoadState } from "@stapel/core";
|
|
3
|
+
import type { FeatureDef } from "@stapel/attributes-react";
|
|
4
|
+
import type { ListingDetail as ListingDetailData, ListingDraft, PublishResponse } from "../api/types.js";
|
|
5
|
+
import type { ListingDraftValues, ListingLocation } from "../model/draft.js";
|
|
6
|
+
import type { PublishRefusal } from "../model/validation.js";
|
|
7
|
+
/**
|
|
8
|
+
* The submission flow — the pair's long pole, and the place four contracts
|
|
9
|
+
* meet: the draft twin (listings), the value editors (attributes), the upload
|
|
10
|
+
* queue (cdn) and the category schema (categories).
|
|
11
|
+
*
|
|
12
|
+
* ── The steps, and why they are not a `@flow_step` funnel ──────────────────
|
|
13
|
+
*
|
|
14
|
+
* pick a category → create the draft row → save into it → publish
|
|
15
|
+
*
|
|
16
|
+
* The server does not sequence these (see `flows/registry.ts`). What DOES
|
|
17
|
+
* sequence them is here, and its stages are named so analytics can follow the
|
|
18
|
+
* funnel without the backend having declared one.
|
|
19
|
+
*
|
|
20
|
+
* ── The three seams, and why none of them is an import ─────────────────────
|
|
21
|
+
*
|
|
22
|
+
* `@stapel/listings-react`, `@stapel/cdn-react` and `@stapel/categories-react`
|
|
23
|
+
* are all L2 pairs, and L2 pairs never import each other (the monorepo README
|
|
24
|
+
* states the direction). So:
|
|
25
|
+
*
|
|
26
|
+
* - **the category schema** arrives as `features` — a plain
|
|
27
|
+
* `readonly FeatureDef[]`. The container gets it from
|
|
28
|
+
* `@stapel/categories-react`'s `useCategoryFeatures`;
|
|
29
|
+
* - **the gallery** arrives as {@link ListingImagesBag} — two members,
|
|
30
|
+
* satisfied STRUCTURALLY by `useUploadQueue()`'s bag from
|
|
31
|
+
* `@stapel/cdn-react`. `bag.refs` IS the value of `images_draft` (same
|
|
32
|
+
* order, first tile is the cover) and `bag.settled` is the submit gate
|
|
33
|
+
* that stops a publish while photos are in flight. That pair wrote its bag
|
|
34
|
+
* to this contract on purpose (its §13.6 note 9);
|
|
35
|
+
* - **the value editors** come from `@stapel/attributes-react`, which is L0
|
|
36
|
+
* and therefore a legitimate dependency. Only `editorTypes` crosses back:
|
|
37
|
+
* the headless half must judge renderability without importing a skin, so
|
|
38
|
+
* the skin's `BUILTIN_VALUE_EDITOR_TYPES` is passed IN.
|
|
39
|
+
*
|
|
40
|
+
* ── Reopening a draft is NOT possible on stapel-listings 0.6.0 ─────────────
|
|
41
|
+
*
|
|
42
|
+
* No read returns the `*_draft` twin: `GET /{pk}/` serializes the PUBLISHED
|
|
43
|
+
* fields (`ListingDetailSerializer`), and `ListingDraftSerializer` appears
|
|
44
|
+
* only as the RESPONSE of create / save-draft. So editing a live listing
|
|
45
|
+
* works completely — the composer seeds from the published half, which is
|
|
46
|
+
* exactly what the person sees on the page — while a draft abandoned and
|
|
47
|
+
* reopened later comes back empty. The composer says so
|
|
48
|
+
* (`draftNotReadable`) instead of showing a blank form as if nothing had been
|
|
49
|
+
* typed. Upstream ask, recorded in MODULE.md: put the draft twin on the
|
|
50
|
+
* retrieve, or add `GET /{pk}/draft/`.
|
|
51
|
+
*/
|
|
52
|
+
/** The two members of `@stapel/cdn-react`'s upload bag this composer needs.
|
|
53
|
+
* Declared structurally so the dependency stays a wiring decision. */
|
|
54
|
+
export interface ListingImagesBag {
|
|
55
|
+
/** Settled CDN references in display order — the value of `images_draft`. */
|
|
56
|
+
readonly refs: readonly string[];
|
|
57
|
+
/** Available when every pick has a reference; blocked WITH the reason
|
|
58
|
+
* (still uploading / one failed) otherwise. */
|
|
59
|
+
readonly settled: ActionAvailability;
|
|
60
|
+
}
|
|
61
|
+
/** Where the submission has got to. Named so a funnel can be measured. */
|
|
62
|
+
export type ComposeStage = "choosing_category" | "editing" | "saving" | "publishing" | "published";
|
|
63
|
+
/** What a successful publish turned out to be — the 0.5.0 distinction, taken
|
|
64
|
+
* from the server's own answer rather than guessed from what we sent. */
|
|
65
|
+
export type PublishOutcome = "submitted_for_review" | "live_edit_under_review";
|
|
66
|
+
export interface UseListingComposerOptions {
|
|
67
|
+
/** An existing listing to edit. Absent: a new one, whose row is created on
|
|
68
|
+
* the first save. */
|
|
69
|
+
readonly listingId?: number;
|
|
70
|
+
/** The chosen category's features. Empty while none is chosen. */
|
|
71
|
+
readonly features: readonly FeatureDef[];
|
|
72
|
+
/** The schema read is in flight. Blocks the submit with its own reason —
|
|
73
|
+
* publishing against a half-known schema is how a mandatory attribute gets
|
|
74
|
+
* skipped. */
|
|
75
|
+
readonly featuresLoading?: boolean;
|
|
76
|
+
/** The schema read failed. Same block, different sentence. */
|
|
77
|
+
readonly featuresError?: unknown;
|
|
78
|
+
/** Value types the rendering half can draw — pass
|
|
79
|
+
* `BUILTIN_VALUE_EDITOR_TYPES` from `@stapel/attributes-react/default`. */
|
|
80
|
+
readonly editorTypes?: readonly string[];
|
|
81
|
+
/** The upload queue. Absent: no gallery, and `images` is set through
|
|
82
|
+
* `setValue` by whatever the host uses instead. */
|
|
83
|
+
readonly images?: ListingImagesBag;
|
|
84
|
+
/** Seed for a brand-new draft (a category preselected from the URL, say). */
|
|
85
|
+
readonly initialValues?: Partial<ListingDraftValues>;
|
|
86
|
+
onDraftCreated?: (draft: ListingDraft) => void;
|
|
87
|
+
onPublished?: (response: PublishResponse, outcome: PublishOutcome) => void;
|
|
88
|
+
}
|
|
89
|
+
export interface ListingComposerBag {
|
|
90
|
+
readonly stage: ComposeStage;
|
|
91
|
+
readonly listingId: number | undefined;
|
|
92
|
+
readonly values: ListingDraftValues;
|
|
93
|
+
/** The listing being edited, when there is one. `loading` until it lands —
|
|
94
|
+
a composer must not let someone publish over a listing it has not read. */
|
|
95
|
+
readonly listingState: LoadState<ListingDetailData> | undefined;
|
|
96
|
+
/** Editing something already PUBLISHED: the submit becomes "send changes",
|
|
97
|
+
* and the listing stays visible throughout. */
|
|
98
|
+
readonly isLiveEdit: boolean;
|
|
99
|
+
/** The reopened draft came back empty because no read returns the draft
|
|
100
|
+
* twin — see this module's header. */
|
|
101
|
+
readonly draftNotReadable: boolean;
|
|
102
|
+
setValue<K extends keyof ListingDraftValues>(key: K, value: ListingDraftValues[K]): void;
|
|
103
|
+
setLocation(location: ListingLocation): void;
|
|
104
|
+
setFeature(slug: string, value: unknown): void;
|
|
105
|
+
/** Changing category keeps the answers the new schema also asks for. */
|
|
106
|
+
setCategory(categoryId: string): void;
|
|
107
|
+
/** Slugs cleared by the last category change — named, not silently lost. */
|
|
108
|
+
readonly droppedOnCategoryChange: readonly string[];
|
|
109
|
+
/** The client mirror, always current. */
|
|
110
|
+
readonly mirror: Readonly<Record<string, FlowError>>;
|
|
111
|
+
/** What a control should actually show: the mirror once the person has
|
|
112
|
+
* tried to submit, overlaid by the server's verdict when there is one. */
|
|
113
|
+
readonly fieldErrors: Readonly<Record<string, FlowError>>;
|
|
114
|
+
/** The server's last publish refusal, unrouted, for a summary line. */
|
|
115
|
+
readonly refusal: PublishRefusal | undefined;
|
|
116
|
+
/** Value types this build cannot draw — the fact, from attributes-react. */
|
|
117
|
+
readonly unsupported: readonly string[];
|
|
118
|
+
readonly saveGate: ActionAvailability;
|
|
119
|
+
readonly publishGate: ActionAvailability;
|
|
120
|
+
save(): void;
|
|
121
|
+
publish(): void;
|
|
122
|
+
readonly saving: boolean;
|
|
123
|
+
readonly publishing: boolean;
|
|
124
|
+
readonly saved: boolean;
|
|
125
|
+
readonly saveError: unknown;
|
|
126
|
+
readonly outcome: PublishOutcome | undefined;
|
|
127
|
+
}
|
|
128
|
+
export declare function useListingComposer(options: UseListingComposerOptions): ListingComposerBag;
|
|
129
|
+
/** Renderless: the bag, handed to a render prop. */
|
|
130
|
+
export declare function ListingComposer(props: UseListingComposerOptions & {
|
|
131
|
+
children: (bag: ListingComposerBag) => ReactNode;
|
|
132
|
+
}): ReactElement;
|
|
133
|
+
//# sourceMappingURL=ListingComposer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingComposer.d.ts","sourceRoot":"","sources":["../../src/headless/ListingComposer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { actionAvailable, actionBlocked, firstBlock, loadFailed, loadLoading, loadReady, } from "@stapel/core";
|
|
4
|
+
import { toFeaturesDto, unsupportedTypes } from "@stapel/attributes-react";
|
|
5
|
+
import { useListingsRuntime } from "../model/context.js";
|
|
6
|
+
import { useListing } from "../model/queries.js";
|
|
7
|
+
import { useCreateDraft, usePublishListing, useSaveDraft, } from "../model/mutations.js";
|
|
8
|
+
import { createDraftBody, draftPatchFromValues, draftValuesFromDetail, droppedFeatureSlugs, emptyDraftValues, retainKnownFeatureValues, } from "../model/draft.js";
|
|
9
|
+
import { asFeatureDaoList } from "../model/features.js";
|
|
10
|
+
import { featuresDtoFromDaoList } from "../model/features.js";
|
|
11
|
+
import { mirrorDraft, publishRefusal } from "../model/validation.js";
|
|
12
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
13
|
+
import { useMandateGate } from "./useMandateGate.js";
|
|
14
|
+
export function useListingComposer(options) {
|
|
15
|
+
const runtime = useListingsRuntime();
|
|
16
|
+
const mandate = useMandateGate();
|
|
17
|
+
const [listingId, setListingId] = useState(options.listingId);
|
|
18
|
+
const [values, setValues] = useState(() => ({
|
|
19
|
+
...emptyDraftValues({ currency: runtime.currency }),
|
|
20
|
+
...options.initialValues,
|
|
21
|
+
}));
|
|
22
|
+
const [showErrors, setShowErrors] = useState(false);
|
|
23
|
+
const [dropped, setDropped] = useState([]);
|
|
24
|
+
const [refusal, setRefusal] = useState(undefined);
|
|
25
|
+
const [outcome, setOutcome] = useState(undefined);
|
|
26
|
+
const [saved, setSaved] = useState(false);
|
|
27
|
+
const existing = useListing(options.listingId);
|
|
28
|
+
const createDraft = useCreateDraft();
|
|
29
|
+
const saveDraft = useSaveDraft();
|
|
30
|
+
const publishListing = usePublishListing();
|
|
31
|
+
// Seed once from the listing being edited. `seeded` is a ref rather than a
|
|
32
|
+
// dependency guard because re-seeding on a refetch would throw away
|
|
33
|
+
// everything typed since — a background invalidation must never rewrite a
|
|
34
|
+
// form somebody is in the middle of.
|
|
35
|
+
const seeded = useRef(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (seeded.current)
|
|
38
|
+
return;
|
|
39
|
+
const detail = existing.data;
|
|
40
|
+
if (detail === undefined)
|
|
41
|
+
return;
|
|
42
|
+
seeded.current = true;
|
|
43
|
+
setListingId(detail.id);
|
|
44
|
+
setValues(draftValuesFromDetail(detail, featuresDtoFromDaoList(asFeatureDaoList(detail.features)), { currency: runtime.currency }));
|
|
45
|
+
}, [existing.data, runtime.currency]);
|
|
46
|
+
// Pruning runs on the SCHEMA arriving, one render after the category
|
|
47
|
+
// changed: `setCategory` cannot prune against features it has not been
|
|
48
|
+
// handed. Gated on the schema being settled, because pruning against a
|
|
49
|
+
// half-loaded (or previous) category's features would delete answers that
|
|
50
|
+
// are perfectly valid for the one now chosen.
|
|
51
|
+
const schemaSettled = options.featuresLoading !== true && options.featuresError === undefined;
|
|
52
|
+
const { features } = options;
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!schemaSettled || features.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
setValues((current) => {
|
|
57
|
+
const gone = droppedFeatureSlugs(current.features, features);
|
|
58
|
+
if (gone.length === 0)
|
|
59
|
+
return current;
|
|
60
|
+
setDropped(gone);
|
|
61
|
+
return {
|
|
62
|
+
...current,
|
|
63
|
+
features: retainKnownFeatureValues(current.features, features),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}, [schemaSettled, features]);
|
|
67
|
+
// The gallery is the upload bag's, whenever there is one: two sources of
|
|
68
|
+
// truth for the same list is how a publish sends photos the person removed.
|
|
69
|
+
const images = options.images?.refs ?? values.images;
|
|
70
|
+
const effectiveValues = useMemo(() => ({ ...values, images }), [values, images]);
|
|
71
|
+
const featuresDto = useMemo(() => toFeaturesDto(options.features, effectiveValues.features), [options.features, effectiveValues.features]);
|
|
72
|
+
const mirror = useMemo(() => mirrorDraft(effectiveValues, options.features, featuresDto, runtime.limits), [effectiveValues, options.features, featuresDto, runtime.limits]);
|
|
73
|
+
const unsupported = useMemo(() => unsupportedTypes(options.features, options.editorTypes ?? []), [options.features, options.editorTypes]);
|
|
74
|
+
const isLiveEdit = existing.data?.status === "published";
|
|
75
|
+
const draftNotReadable = existing.data !== undefined &&
|
|
76
|
+
existing.data.status !== "published" &&
|
|
77
|
+
(existing.data.title ?? "").length === 0 &&
|
|
78
|
+
(existing.data.description ?? "").length === 0;
|
|
79
|
+
const fieldErrors = useMemo(() => {
|
|
80
|
+
const shown = showErrors ? mirror : {};
|
|
81
|
+
return refusal?.kind === "invalid_draft"
|
|
82
|
+
? { ...shown, ...refusal.fieldErrors }
|
|
83
|
+
: shown;
|
|
84
|
+
}, [showErrors, mirror, refusal]);
|
|
85
|
+
const listingState = options.listingId === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: existing.status === "error"
|
|
88
|
+
? loadFailed(existing.error)
|
|
89
|
+
: existing.data !== undefined
|
|
90
|
+
? loadReady(existing.data)
|
|
91
|
+
: loadLoading();
|
|
92
|
+
const busy = createDraft.isPending || saveDraft.isPending;
|
|
93
|
+
const schemaGate = options.featuresError !== undefined
|
|
94
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeBlockedDetailsUnavailable)
|
|
95
|
+
: options.featuresLoading === true
|
|
96
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeDetailsLoading)
|
|
97
|
+
: actionAvailable();
|
|
98
|
+
const categoryGate = effectiveValues.categoryId.length > 0
|
|
99
|
+
? actionAvailable()
|
|
100
|
+
: actionBlocked(LISTINGS_I18N_KEYS.composeBlockedNoCategory);
|
|
101
|
+
const busyGate = busy
|
|
102
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeBlockedBusy)
|
|
103
|
+
: actionAvailable();
|
|
104
|
+
const saveGate = firstBlock(mandate, categoryGate, busyGate);
|
|
105
|
+
// The publish gate is the save gate PLUS everything that must be true for a
|
|
106
|
+
// submission to be complete. Order is the order a person would be told:
|
|
107
|
+
// may you act at all → is the form even answerable → are the photos in →
|
|
108
|
+
// is the form itself clean.
|
|
109
|
+
const publishGate = firstBlock(mandate, categoryGate, schemaGate, unsupported.length > 0
|
|
110
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeBlockedUnsupportedType, {
|
|
111
|
+
types: unsupported.join(", "),
|
|
112
|
+
})
|
|
113
|
+
: actionAvailable(), options.images?.settled ?? actionAvailable(), busyGate, publishListing.isPending
|
|
114
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeBlockedBusy)
|
|
115
|
+
: actionAvailable(), Object.keys(mirror).length > 0
|
|
116
|
+
? actionBlocked(LISTINGS_I18N_KEYS.composeBlockedMirror)
|
|
117
|
+
: actionAvailable());
|
|
118
|
+
/** Create the row on demand: the id is what every other write needs. */
|
|
119
|
+
const ensureListingId = useCallback(async () => {
|
|
120
|
+
if (listingId !== undefined && listingId > 0)
|
|
121
|
+
return listingId;
|
|
122
|
+
if (effectiveValues.categoryId.length === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const draft = await createDraft.mutateAsync(createDraftBody(effectiveValues.categoryId));
|
|
125
|
+
setListingId(draft.id);
|
|
126
|
+
options.onDraftCreated?.(draft);
|
|
127
|
+
return draft.id;
|
|
128
|
+
}, [listingId, effectiveValues.categoryId, createDraft, options]);
|
|
129
|
+
const persist = useCallback(async () => {
|
|
130
|
+
const id = await ensureListingId();
|
|
131
|
+
if (id === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
// The id comes from `ensureListingId`, not from this closure's render:
|
|
134
|
+
// creating the row and saving into it happen in ONE gesture, and the
|
|
135
|
+
// state update that records the new id has not committed yet.
|
|
136
|
+
await saveDraft.mutateAsync({
|
|
137
|
+
id,
|
|
138
|
+
body: draftPatchFromValues(effectiveValues, options.features),
|
|
139
|
+
});
|
|
140
|
+
setSaved(true);
|
|
141
|
+
return id;
|
|
142
|
+
}, [ensureListingId, saveDraft, effectiveValues, options.features]);
|
|
143
|
+
const save = useCallback(() => {
|
|
144
|
+
setShowErrors(true);
|
|
145
|
+
if (!saveGate.available)
|
|
146
|
+
return;
|
|
147
|
+
setRefusal(undefined);
|
|
148
|
+
void persist().catch(() => {
|
|
149
|
+
// The mutation's own `error` carries it; swallowing the rejection here
|
|
150
|
+
// only stops an unhandled-rejection warning for a failure the bag
|
|
151
|
+
// already reports.
|
|
152
|
+
});
|
|
153
|
+
}, [saveGate.available, persist]);
|
|
154
|
+
const publish = useCallback(() => {
|
|
155
|
+
setShowErrors(true);
|
|
156
|
+
if (!publishGate.available)
|
|
157
|
+
return;
|
|
158
|
+
setRefusal(undefined);
|
|
159
|
+
void (async () => {
|
|
160
|
+
try {
|
|
161
|
+
// Always save first: publish reads the STORED draft, so publishing
|
|
162
|
+
// without saving would promote whatever was there before the last
|
|
163
|
+
// keystroke — and the person would be told their listing is fine
|
|
164
|
+
// while a field they just fixed is still wrong on the server.
|
|
165
|
+
const id = await persist();
|
|
166
|
+
if (id === undefined)
|
|
167
|
+
return;
|
|
168
|
+
const response = await publishListing.mutateAsync(id);
|
|
169
|
+
const result = response.status === "published"
|
|
170
|
+
? "live_edit_under_review"
|
|
171
|
+
: "submitted_for_review";
|
|
172
|
+
setOutcome(result);
|
|
173
|
+
options.onPublished?.(response, result);
|
|
174
|
+
}
|
|
175
|
+
catch (thrown) {
|
|
176
|
+
setRefusal(publishRefusal(thrown));
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
}, [publishGate.available, persist, publishListing, options]);
|
|
180
|
+
const stage = outcome !== undefined
|
|
181
|
+
? "published"
|
|
182
|
+
: publishListing.isPending
|
|
183
|
+
? "publishing"
|
|
184
|
+
: busy
|
|
185
|
+
? "saving"
|
|
186
|
+
: effectiveValues.categoryId.length === 0
|
|
187
|
+
? "choosing_category"
|
|
188
|
+
: "editing";
|
|
189
|
+
return {
|
|
190
|
+
stage,
|
|
191
|
+
listingId,
|
|
192
|
+
values: effectiveValues,
|
|
193
|
+
listingState,
|
|
194
|
+
isLiveEdit,
|
|
195
|
+
draftNotReadable,
|
|
196
|
+
setValue: (key, value) => {
|
|
197
|
+
setSaved(false);
|
|
198
|
+
setValues((current) => ({ ...current, [key]: value }));
|
|
199
|
+
},
|
|
200
|
+
setLocation: (location) => {
|
|
201
|
+
setSaved(false);
|
|
202
|
+
setValues((current) => ({ ...current, location }));
|
|
203
|
+
},
|
|
204
|
+
setFeature: (slug, value) => {
|
|
205
|
+
setSaved(false);
|
|
206
|
+
setValues((current) => ({
|
|
207
|
+
...current,
|
|
208
|
+
features: { ...current.features, [slug]: value },
|
|
209
|
+
}));
|
|
210
|
+
},
|
|
211
|
+
setCategory: (categoryId) => {
|
|
212
|
+
setSaved(false);
|
|
213
|
+
setValues((current) => {
|
|
214
|
+
if (current.categoryId === categoryId)
|
|
215
|
+
return current;
|
|
216
|
+
// The features of the NEW category are not known yet (the container's
|
|
217
|
+
// schema read has not run), so nothing is pruned here — pruning
|
|
218
|
+
// happens in the effect below, once they arrive. Recording the
|
|
219
|
+
// intent, not guessing the outcome.
|
|
220
|
+
return { ...current, categoryId };
|
|
221
|
+
});
|
|
222
|
+
setDropped([]);
|
|
223
|
+
setRefusal(undefined);
|
|
224
|
+
},
|
|
225
|
+
droppedOnCategoryChange: dropped,
|
|
226
|
+
mirror,
|
|
227
|
+
fieldErrors,
|
|
228
|
+
refusal,
|
|
229
|
+
unsupported,
|
|
230
|
+
saveGate,
|
|
231
|
+
publishGate,
|
|
232
|
+
save,
|
|
233
|
+
publish,
|
|
234
|
+
saving: busy,
|
|
235
|
+
publishing: publishListing.isPending,
|
|
236
|
+
saved,
|
|
237
|
+
saveError: saveDraft.error ?? createDraft.error,
|
|
238
|
+
outcome,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Renderless: the bag, handed to a render prop. */
|
|
242
|
+
export function ListingComposer(props) {
|
|
243
|
+
const bag = useListingComposer(props);
|
|
244
|
+
return _jsx(_Fragment, { children: props.children(bag) });
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=ListingComposer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingComposer.js","sourceRoot":"","sources":["../../src/headless/ListingComposer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { ActionAvailability, LoadState } from "@stapel/core";
|
|
3
|
+
import type { ListingDetail as ListingDetailData, ListingFeatureView, ListingStatusInfo } from "../api/types.js";
|
|
4
|
+
import type { ListingStatusView } from "../model/status.js";
|
|
5
|
+
/**
|
|
6
|
+
* Everything a listing page renders, with the two absences it must be able to
|
|
7
|
+
* tell apart.
|
|
8
|
+
*
|
|
9
|
+
* ── Why a second read runs beside the detail ───────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* `GET /listings/{pk}/` reads `Listing.objects`, which excludes soft-deleted
|
|
12
|
+
* rows: a removed listing answers 404, and a 404 is also what a made-up id
|
|
13
|
+
* answers. `GET /listings/{pk}/status/` reads `Listing.all_objects` and is
|
|
14
|
+
* `AllowAny`, so it still answers for the removed one. Running both is what
|
|
15
|
+
* turns "there is no listing at this address" and "this listing was removed"
|
|
16
|
+
* into two different sentences — and the second is the one a person following
|
|
17
|
+
* a stale link actually needs.
|
|
18
|
+
*
|
|
19
|
+
* The probe is not a fallback fired after a failure: it runs in parallel, so
|
|
20
|
+
* the page resolves in one round trip rather than two sequential ones.
|
|
21
|
+
*
|
|
22
|
+
* ── Visibility is reported, not assumed ────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* The detail endpoint has NO `published()` filter on its queryset, so a draft
|
|
25
|
+
* and a taken-down listing both answer 200 to anyone holding the id. The bag
|
|
26
|
+
* therefore carries `publiclyVisible` (computed from `status`, the one field
|
|
27
|
+
* that decides it) and `viewerIsOwner`, and a skin says which of the two
|
|
28
|
+
* situations it is in. Rendering a draft as a live shop page would be the
|
|
29
|
+
* pair repeating the server's omission instead of covering it.
|
|
30
|
+
*/
|
|
31
|
+
export interface ListingDetailBag {
|
|
32
|
+
readonly id: number;
|
|
33
|
+
readonly state: LoadState<ListingDetailData>;
|
|
34
|
+
/** The status probe, which answers for rows the detail cannot see. */
|
|
35
|
+
readonly statusState: LoadState<ListingStatusInfo>;
|
|
36
|
+
/** True when the detail 404s AND the probe says the row is soft-deleted —
|
|
37
|
+
* i.e. it existed and is gone, rather than never having existed. */
|
|
38
|
+
readonly removed: boolean;
|
|
39
|
+
/** The detail 404s and the probe finds nothing either. */
|
|
40
|
+
readonly notFound: boolean;
|
|
41
|
+
/** Both axes plus the sentence that comes out of the pair of them. */
|
|
42
|
+
readonly status: ListingStatusView | undefined;
|
|
43
|
+
readonly publiclyVisible: boolean;
|
|
44
|
+
/** Whether the person reading this is the owner, when both the probe and
|
|
45
|
+
* the viewer's id are known. `undefined` means "we did not ask" — a public
|
|
46
|
+
* page has no reason to. */
|
|
47
|
+
readonly viewerIsOwner: boolean | undefined;
|
|
48
|
+
/** The ordered specs table. */
|
|
49
|
+
readonly features: readonly ListingFeatureView[];
|
|
50
|
+
/** The `show_at_title` subset — what a heading appends. */
|
|
51
|
+
readonly titleFeatures: readonly ListingFeatureView[];
|
|
52
|
+
/** The `show_as_badge` subset — what a card shows. */
|
|
53
|
+
readonly badgeFeatures: readonly ListingFeatureView[];
|
|
54
|
+
/** Stored rows this build could not key. Reported, never rounded to zero. */
|
|
55
|
+
readonly unreadableFeatures: number;
|
|
56
|
+
/** Ordered CDN references. Resolving one to an image is the runtime's job
|
|
57
|
+
* (`resolveImage`), because no contract in this fleet resolves a
|
|
58
|
+
* stranger's reference — see `model/runtime.ts`. */
|
|
59
|
+
readonly images: readonly string[];
|
|
60
|
+
readonly isFavorited: boolean | undefined;
|
|
61
|
+
/** Saving a favourite needs a mandate; the block says which of the four
|
|
62
|
+
* reasons applies. */
|
|
63
|
+
readonly favoriteGate: ActionAvailability;
|
|
64
|
+
toggleFavorite(): void;
|
|
65
|
+
readonly favoriteInFlight: boolean;
|
|
66
|
+
refetch(): void;
|
|
67
|
+
}
|
|
68
|
+
export interface UseListingDetailOptions {
|
|
69
|
+
/** The reader's own id, when the host knows it — enables `viewerIsOwner`.
|
|
70
|
+
* A uuid string, matching `ListingDetail.owner` / `ListingStatus.owner_id`. */
|
|
71
|
+
readonly viewerId?: string;
|
|
72
|
+
}
|
|
73
|
+
export declare function useListingDetail(id: number, options?: UseListingDetailOptions): ListingDetailBag;
|
|
74
|
+
/** Renderless: the bag, handed to a render prop. Bring your own visuals, or
|
|
75
|
+
* import `<ListingDetailPane>` from `./default`. */
|
|
76
|
+
export declare function ListingDetail(props: {
|
|
77
|
+
id: number;
|
|
78
|
+
viewerId?: string;
|
|
79
|
+
children: (bag: ListingDetailBag) => ReactNode;
|
|
80
|
+
}): ReactElement;
|
|
81
|
+
//# sourceMappingURL=ListingDetail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingDetail.d.ts","sourceRoot":"","sources":["../../src/headless/ListingDetail.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EACV,aAAa,IAAI,iBAAiB,EAClC,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAc5D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAC7C,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC;IACnD;wEACoE;IACpE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC;;gCAE4B;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,2DAA2D;IAC3D,QAAQ,CAAC,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACtD,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACtD,6EAA6E;IAC7E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;wDAEoD;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C;0BACsB;IACtB,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C,cAAc,IAAI,IAAI,CAAC;IACvB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC;mFAC+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CA4GlB;AAED;oDACoD;AACpD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,SAAS,CAAC;CAChD,GAAG,YAAY,CAMf"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { actionAvailable, actionBlocked, errorStatus, firstBlock, loadFailed, loadLoading, loadReady, } from "@stapel/core";
|
|
4
|
+
import { useListing, useListingStatus } from "../model/queries.js";
|
|
5
|
+
import { useFavoriteListing } from "../model/mutations.js";
|
|
6
|
+
import { asFeatureDaoList, featuresFromDaoList, unreadableFeatureCount } from "../model/features.js";
|
|
7
|
+
import { listingStatusView } from "../model/status.js";
|
|
8
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
9
|
+
import { useMandateGate } from "./useMandateGate.js";
|
|
10
|
+
/**
|
|
11
|
+
* `images` is `string[] | NULL` on the wire, and the null is the SERVER
|
|
12
|
+
* saying "this listing has no photos" — not this pair saying "we do not
|
|
13
|
+
* know". Whether the read landed at all is carried by `state`, one field
|
|
14
|
+
* over, so collapsing the null here loses nothing: a named constant makes
|
|
15
|
+
* that argument visible where a bare `?? []` would look like the flattening
|
|
16
|
+
* `no-flattened-load-state` bans.
|
|
17
|
+
*/
|
|
18
|
+
const NO_IMAGES = [];
|
|
19
|
+
export function useListingDetail(id, options = {}) {
|
|
20
|
+
const detail = useListing(id);
|
|
21
|
+
const probe = useListingStatus(id);
|
|
22
|
+
const favorite = useFavoriteListing();
|
|
23
|
+
const mandate = useMandateGate();
|
|
24
|
+
const state = detail.status === "error"
|
|
25
|
+
? loadFailed(detail.error)
|
|
26
|
+
: detail.data !== undefined
|
|
27
|
+
? loadReady(detail.data)
|
|
28
|
+
: loadLoading();
|
|
29
|
+
const statusState = probe.status === "error"
|
|
30
|
+
? loadFailed(probe.error)
|
|
31
|
+
: probe.data !== undefined
|
|
32
|
+
? loadReady(probe.data)
|
|
33
|
+
: loadLoading();
|
|
34
|
+
const detailIs404 = detail.status === "error" && errorStatus(detail.error) === 404;
|
|
35
|
+
const removed = detailIs404 && probe.data?.is_deleted === true;
|
|
36
|
+
const notFound = detailIs404 && !removed && probe.status === "error";
|
|
37
|
+
const status = useMemo(() => {
|
|
38
|
+
if (detail.data !== undefined) {
|
|
39
|
+
return listingStatusView(detail.data.status ?? "draft", detail.data.moderation_status ?? "pending");
|
|
40
|
+
}
|
|
41
|
+
// The probe still knows both axes for a row the detail cannot return —
|
|
42
|
+
// `moderation_status` arrives as a bare string there, so it is narrowed
|
|
43
|
+
// by the same table rather than trusted.
|
|
44
|
+
if (probe.data !== undefined) {
|
|
45
|
+
const moderation = probe.data.moderation_status;
|
|
46
|
+
return listingStatusView(probe.data.status, moderation === "approved" ||
|
|
47
|
+
moderation === "rejected" ||
|
|
48
|
+
moderation === "needs_review"
|
|
49
|
+
? moderation
|
|
50
|
+
: "pending");
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}, [detail.data, probe.data]);
|
|
54
|
+
const owner = detail.data?.owner ?? probe.data?.owner_id;
|
|
55
|
+
const viewerIsOwner = options.viewerId === undefined || owner === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: owner === options.viewerId;
|
|
58
|
+
const features = useMemo(() => featuresFromDaoList(asFeatureDaoList(detail.data?.features)), [detail.data]);
|
|
59
|
+
const titleFeatures = useMemo(() => featuresFromDaoList(asFeatureDaoList(detail.data?.features_title)), [detail.data]);
|
|
60
|
+
const badgeFeatures = useMemo(() => featuresFromDaoList(asFeatureDaoList(detail.data?.features_badges)), [detail.data]);
|
|
61
|
+
const isFavorited = detail.data?.is_favorited ?? undefined;
|
|
62
|
+
const favoriteGate = firstBlock(mandate, favorite.isPending
|
|
63
|
+
? actionBlocked(LISTINGS_I18N_KEYS.blockedInFlight)
|
|
64
|
+
: actionAvailable(), detail.data === undefined
|
|
65
|
+
? actionBlocked(LISTINGS_I18N_KEYS.detailLoading)
|
|
66
|
+
: actionAvailable());
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
state,
|
|
70
|
+
statusState,
|
|
71
|
+
removed,
|
|
72
|
+
notFound,
|
|
73
|
+
status,
|
|
74
|
+
publiclyVisible: status?.lifecycle.publiclyVisible ?? false,
|
|
75
|
+
viewerIsOwner,
|
|
76
|
+
features,
|
|
77
|
+
titleFeatures,
|
|
78
|
+
badgeFeatures,
|
|
79
|
+
unreadableFeatures: unreadableFeatureCount(asFeatureDaoList(detail.data?.features)),
|
|
80
|
+
images: detail.data?.images ?? NO_IMAGES,
|
|
81
|
+
isFavorited,
|
|
82
|
+
favoriteGate,
|
|
83
|
+
toggleFavorite: () => {
|
|
84
|
+
if (!favoriteGate.available)
|
|
85
|
+
return;
|
|
86
|
+
favorite.mutate({ id, favorited: isFavorited !== true });
|
|
87
|
+
},
|
|
88
|
+
favoriteInFlight: favorite.isPending,
|
|
89
|
+
refetch: () => {
|
|
90
|
+
void detail.refetch();
|
|
91
|
+
void probe.refetch();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Renderless: the bag, handed to a render prop. Bring your own visuals, or
|
|
96
|
+
* import `<ListingDetailPane>` from `./default`. */
|
|
97
|
+
export function ListingDetail(props) {
|
|
98
|
+
const bag = useListingDetail(props.id, props.viewerId !== undefined ? { viewerId: props.viewerId } : {});
|
|
99
|
+
return _jsx(_Fragment, { children: props.children(bag) });
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=ListingDetail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingDetail.js","sourceRoot":"","sources":["../../src/headless/ListingDetail.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { ListingsRuntime } from "../model/runtime.js";
|
|
3
|
+
/**
|
|
4
|
+
* Renderless provider — puts one wired {@link ListingsRuntime} in context for
|
|
5
|
+
* every hook and headless component in the pair. Bring your own visual shell.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ListingsProvider runtime={createListingsRuntime({ baseUrl: "/listings/api/v1/" })}>
|
|
9
|
+
* <YourShopWindow />
|
|
10
|
+
* </ListingsProvider>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function ListingsProvider(props: {
|
|
14
|
+
runtime: ListingsRuntime;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): ReactElement;
|
|
17
|
+
//# sourceMappingURL=ListingsProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingsProvider.d.ts","sourceRoot":"","sources":["../../src/headless/ListingsProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,YAAY,CAEf"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ModuleProvider } from "../model/context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Renderless provider — puts one wired {@link ListingsRuntime} in context for
|
|
5
|
+
* every hook and headless component in the pair. Bring your own visual shell.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ListingsProvider runtime={createListingsRuntime({ baseUrl: "/listings/api/v1/" })}>
|
|
9
|
+
* <YourShopWindow />
|
|
10
|
+
* </ListingsProvider>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function ListingsProvider(props) {
|
|
14
|
+
return _jsx(ModuleProvider, { runtime: props.runtime, children: props.children });
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=ListingsProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListingsProvider.js","sourceRoot":"","sources":["../../src/headless/ListingsProvider.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAGhC;IACC,OAAO,KAAC,cAAc,IAAC,OAAO,EAAE,KAAK,CAAC,OAAO,YAAG,KAAK,CAAC,QAAQ,GAAkB,CAAC;AACnF,CAAC"}
|