@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TWO AXES, BOTH ON SCREEN, NEITHER STANDING IN FOR THE OTHER.
|
|
3
|
+
*
|
|
4
|
+
* `status` (nine states) is the lifecycle and the ONLY thing that decides
|
|
5
|
+
* whether a listing is public: `Listing.objects.published()` filters on it
|
|
6
|
+
* alone, `INDEXED_STATUSES` is `{published}`, and the model says so out loud
|
|
7
|
+
* ("no visibility-reads-moderation_status coupling", `models.py`).
|
|
8
|
+
* `moderation_status` (four states) is the content verdict and decides
|
|
9
|
+
* NOTHING about visibility.
|
|
10
|
+
*
|
|
11
|
+
* Since stapel-listings 0.5.0 the two genuinely diverge, and that divergence
|
|
12
|
+
* is the reason this file exists:
|
|
13
|
+
*
|
|
14
|
+
* first publication status draft → PENDING, moderation → pending
|
|
15
|
+
* (nothing public until a verdict arrives)
|
|
16
|
+
* editing a LIVE listing status published (UNCHANGED), moderation → pending
|
|
17
|
+
* (the edit is visible immediately; a rejecting
|
|
18
|
+
* verdict later lands as PUBLISHED → BLOCKED)
|
|
19
|
+
*
|
|
20
|
+
* A dashboard that computed "under re-review" from `status` would show
|
|
21
|
+
* nothing at all for the second row — the listing looks exactly as it did —
|
|
22
|
+
* and the owner would never learn their edit is being screened. A dashboard
|
|
23
|
+
* that computed "visible" from `moderation_status` would tell them their live
|
|
24
|
+
* listing is offline while strangers are reading it. Both mistakes are one
|
|
25
|
+
* `if` away, which is why the sentence a person reads is produced HERE, from
|
|
26
|
+
* BOTH fields, once.
|
|
27
|
+
*
|
|
28
|
+
* Everything below is pure: no React, no antd, no fetch. `test/status.test.ts`
|
|
29
|
+
* asserts the whole 9 × 4 table, so a state added upstream is a red test and
|
|
30
|
+
* not a bare enum value on a page.
|
|
31
|
+
*/
|
|
32
|
+
import type { ListingLifecycleStatus, ListingModerationStatus } from "../api/types.js";
|
|
33
|
+
/**
|
|
34
|
+
* How a status should READ, independent of the words: neutral (nothing is
|
|
35
|
+
* happening), waiting (something is in progress and needs no action), good
|
|
36
|
+
* (live), warning (the owner has something to do), stopped (the listing is
|
|
37
|
+
* over, by choice or by verdict).
|
|
38
|
+
*
|
|
39
|
+
* A tone rather than a colour: `/default` maps it to antd tags, another skin
|
|
40
|
+
* maps it to whatever it maps it to, and the token layer stays the only place
|
|
41
|
+
* a hex exists.
|
|
42
|
+
*/
|
|
43
|
+
export type ListingStatusTone = "neutral" | "waiting" | "good" | "warning" | "stopped";
|
|
44
|
+
/** The lifecycle half of what a person is told. */
|
|
45
|
+
export interface LifecycleCaption {
|
|
46
|
+
readonly status: ListingLifecycleStatus;
|
|
47
|
+
readonly labelKey: string;
|
|
48
|
+
readonly tone: ListingStatusTone;
|
|
49
|
+
/** Is the listing readable by a stranger right now? `published` and
|
|
50
|
+
* nothing else — the same predicate the queryset uses. */
|
|
51
|
+
readonly publiclyVisible: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The lifecycle caption for one status. */
|
|
54
|
+
export declare function lifecycleCaption(status: ListingLifecycleStatus): LifecycleCaption;
|
|
55
|
+
/**
|
|
56
|
+
* Is this listing readable by someone who is not its owner?
|
|
57
|
+
*
|
|
58
|
+
* One predicate, one field, matching the server's. Exported because the
|
|
59
|
+
* DETAIL read needs it and cannot get it from a 404: `GET /listings/{pk}/`
|
|
60
|
+
* has no `published()` filter on its queryset, so a draft answers 200 to a
|
|
61
|
+
* stranger who knows the id (upstream note in MODULE.md). The pair reports
|
|
62
|
+
* the fact instead of rendering a draft as if it were a shop window.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isPubliclyVisible(status: ListingLifecycleStatus): boolean;
|
|
65
|
+
/** The owner-facing note about the moderation axis, when there is one. */
|
|
66
|
+
export interface ModerationNotice {
|
|
67
|
+
readonly moderationStatus: ListingModerationStatus;
|
|
68
|
+
readonly messageKey: string;
|
|
69
|
+
readonly tone: ListingStatusTone;
|
|
70
|
+
/**
|
|
71
|
+
* Is the listing live WHILE this verdict is pending? True only for the
|
|
72
|
+
* re-moderation row — a published listing whose edit is being screened.
|
|
73
|
+
* The sentence differs from a first submission's precisely here.
|
|
74
|
+
*/
|
|
75
|
+
readonly liveDuringReview: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What to say about the moderation axis, given BOTH fields.
|
|
79
|
+
*
|
|
80
|
+
* `undefined` means "nothing to add": an approved listing needs no note, and
|
|
81
|
+
* neither does a draft nobody has submitted. Every other combination has a
|
|
82
|
+
* sentence, including the ones that look contradictory — those are the ones
|
|
83
|
+
* worth naming.
|
|
84
|
+
*/
|
|
85
|
+
export declare function moderationNotice(status: ListingLifecycleStatus, moderationStatus: ListingModerationStatus): ModerationNotice | undefined;
|
|
86
|
+
/** Both halves at once — what a dashboard row and a detail header render. */
|
|
87
|
+
export interface ListingStatusView {
|
|
88
|
+
readonly lifecycle: LifecycleCaption;
|
|
89
|
+
readonly moderation: ModerationNotice | undefined;
|
|
90
|
+
/** True when the two axes disagree in the way 0.5.0 made possible: live to
|
|
91
|
+
* the public, under review for its owner. The one combination a host is
|
|
92
|
+
* most likely to get wrong, so it is a named boolean and not an inference
|
|
93
|
+
* a caller repeats. */
|
|
94
|
+
readonly liveUnderReview: boolean;
|
|
95
|
+
/** What the owner can do next, in lifecycle terms — see
|
|
96
|
+
* {@link LISTING_TRANSITIONS}. */
|
|
97
|
+
readonly allowedTransitions: readonly ListingLifecycleStatus[];
|
|
98
|
+
}
|
|
99
|
+
export declare function listingStatusView(status: ListingLifecycleStatus, moderationStatus: ListingModerationStatus): ListingStatusView;
|
|
100
|
+
/**
|
|
101
|
+
* The dashboard tabs, and the statuses each one folds together.
|
|
102
|
+
*
|
|
103
|
+
* The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
|
|
104
|
+
* caption and its count cannot describe different sets: `active` includes
|
|
105
|
+
* PENDING beside PUBLISHED, and `drafts` includes REJECTED beside DRAFT —
|
|
106
|
+
* both because a listing in those states is something the owner is still
|
|
107
|
+
* working on. A client that grouped them its own way would show "3 active"
|
|
108
|
+
* over two rows.
|
|
109
|
+
*/
|
|
110
|
+
export declare const MY_LISTINGS_TABS: readonly ["active", "drafts", "archived"];
|
|
111
|
+
export type MyListingsTab = (typeof MY_LISTINGS_TABS)[number];
|
|
112
|
+
export declare const MY_LISTINGS_TAB_STATUSES: Readonly<Record<MyListingsTab, readonly ListingLifecycleStatus[]>>;
|
|
113
|
+
/** Which tab a listing belongs to, or `undefined` for BLOCKED — the one
|
|
114
|
+
* status `my/counters` counts in no tab at all. A dashboard that silently
|
|
115
|
+
* dropped it would hide exactly the listing whose owner most needs to know. */
|
|
116
|
+
export declare function tabOf(status: ListingLifecycleStatus): MyListingsTab | undefined;
|
|
117
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/model/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,iBAAiB,CAAC;AAIzB;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,SAAS,GACT,MAAM,GACN,SAAS,GACT,SAAS,CAAC;AAEd,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC;8DAC0D;IAC1D,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAoDD,4CAA4C;AAC5C,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,GAC7B,gBAAgB,CAElB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAEzE;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,uBAAuB,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,EAC9B,gBAAgB,EAAE,uBAAuB,GACxC,gBAAgB,GAAG,SAAS,CA0D9B;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAClD;;;2BAGuB;IACvB,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC;sCACkC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAChE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,sBAAsB,EAC9B,gBAAgB,EAAE,uBAAuB,GACxC,iBAAiB,CAQnB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,2CAA4C,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAC7C,MAAM,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAKzD,CAAC;AAEF;;+EAE+E;AAC/E,wBAAgB,KAAK,CACnB,MAAM,EAAE,sBAAsB,GAC7B,aAAa,GAAG,SAAS,CAK3B"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LISTING_TRANSITIONS } from "./transitions.js";
|
|
2
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
3
|
+
const LIFECYCLE = {
|
|
4
|
+
draft: {
|
|
5
|
+
labelKey: LISTINGS_I18N_KEYS.statusDraft,
|
|
6
|
+
tone: "neutral",
|
|
7
|
+
publiclyVisible: false,
|
|
8
|
+
},
|
|
9
|
+
pending: {
|
|
10
|
+
labelKey: LISTINGS_I18N_KEYS.statusPending,
|
|
11
|
+
tone: "waiting",
|
|
12
|
+
publiclyVisible: false,
|
|
13
|
+
},
|
|
14
|
+
published: {
|
|
15
|
+
labelKey: LISTINGS_I18N_KEYS.statusPublished,
|
|
16
|
+
tone: "good",
|
|
17
|
+
publiclyVisible: true,
|
|
18
|
+
},
|
|
19
|
+
paused: {
|
|
20
|
+
labelKey: LISTINGS_I18N_KEYS.statusPaused,
|
|
21
|
+
tone: "neutral",
|
|
22
|
+
publiclyVisible: false,
|
|
23
|
+
},
|
|
24
|
+
expired: {
|
|
25
|
+
labelKey: LISTINGS_I18N_KEYS.statusExpired,
|
|
26
|
+
tone: "warning",
|
|
27
|
+
publiclyVisible: false,
|
|
28
|
+
},
|
|
29
|
+
sold: {
|
|
30
|
+
labelKey: LISTINGS_I18N_KEYS.statusSold,
|
|
31
|
+
tone: "stopped",
|
|
32
|
+
publiclyVisible: false,
|
|
33
|
+
},
|
|
34
|
+
rejected: {
|
|
35
|
+
labelKey: LISTINGS_I18N_KEYS.statusRejected,
|
|
36
|
+
tone: "warning",
|
|
37
|
+
publiclyVisible: false,
|
|
38
|
+
},
|
|
39
|
+
blocked: {
|
|
40
|
+
labelKey: LISTINGS_I18N_KEYS.statusBlocked,
|
|
41
|
+
tone: "stopped",
|
|
42
|
+
publiclyVisible: false,
|
|
43
|
+
},
|
|
44
|
+
archived: {
|
|
45
|
+
labelKey: LISTINGS_I18N_KEYS.statusArchived,
|
|
46
|
+
tone: "neutral",
|
|
47
|
+
publiclyVisible: false,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/** The lifecycle caption for one status. */
|
|
51
|
+
export function lifecycleCaption(status) {
|
|
52
|
+
return { status, ...LIFECYCLE[status] };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Is this listing readable by someone who is not its owner?
|
|
56
|
+
*
|
|
57
|
+
* One predicate, one field, matching the server's. Exported because the
|
|
58
|
+
* DETAIL read needs it and cannot get it from a 404: `GET /listings/{pk}/`
|
|
59
|
+
* has no `published()` filter on its queryset, so a draft answers 200 to a
|
|
60
|
+
* stranger who knows the id (upstream note in MODULE.md). The pair reports
|
|
61
|
+
* the fact instead of rendering a draft as if it were a shop window.
|
|
62
|
+
*/
|
|
63
|
+
export function isPubliclyVisible(status) {
|
|
64
|
+
return LIFECYCLE[status].publiclyVisible;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What to say about the moderation axis, given BOTH fields.
|
|
68
|
+
*
|
|
69
|
+
* `undefined` means "nothing to add": an approved listing needs no note, and
|
|
70
|
+
* neither does a draft nobody has submitted. Every other combination has a
|
|
71
|
+
* sentence, including the ones that look contradictory — those are the ones
|
|
72
|
+
* worth naming.
|
|
73
|
+
*/
|
|
74
|
+
export function moderationNotice(status, moderationStatus) {
|
|
75
|
+
const live = status === "published";
|
|
76
|
+
if (moderationStatus === "approved") {
|
|
77
|
+
// A takedown keeps `approved` on the moderation axis in some flows (the
|
|
78
|
+
// lifecycle moved, the verdict did not), so an approved BLOCKED listing
|
|
79
|
+
// still owes its owner an explanation — and it is the lifecycle's.
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (moderationStatus === "pending") {
|
|
83
|
+
if (live) {
|
|
84
|
+
return {
|
|
85
|
+
moderationStatus,
|
|
86
|
+
messageKey: LISTINGS_I18N_KEYS.moderationLiveEditPending,
|
|
87
|
+
tone: "waiting",
|
|
88
|
+
liveDuringReview: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (status === "pending") {
|
|
92
|
+
return {
|
|
93
|
+
moderationStatus,
|
|
94
|
+
messageKey: LISTINGS_I18N_KEYS.moderationFirstReview,
|
|
95
|
+
tone: "waiting",
|
|
96
|
+
liveDuringReview: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// A draft/archived/sold row carrying a pending verdict: the submission
|
|
100
|
+
// was overtaken by a lifecycle move. Say that, rather than promising a
|
|
101
|
+
// review that will not put it back on the shelf.
|
|
102
|
+
return {
|
|
103
|
+
moderationStatus,
|
|
104
|
+
messageKey: LISTINGS_I18N_KEYS.moderationPendingOffline,
|
|
105
|
+
tone: "neutral",
|
|
106
|
+
liveDuringReview: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (moderationStatus === "needs_review") {
|
|
110
|
+
return {
|
|
111
|
+
moderationStatus,
|
|
112
|
+
messageKey: live
|
|
113
|
+
? LISTINGS_I18N_KEYS.moderationLiveNeedsReview
|
|
114
|
+
: LISTINGS_I18N_KEYS.moderationNeedsReview,
|
|
115
|
+
tone: "waiting",
|
|
116
|
+
liveDuringReview: live,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// rejected — and it matters whether the lifecycle followed the verdict.
|
|
120
|
+
return {
|
|
121
|
+
moderationStatus,
|
|
122
|
+
messageKey: live
|
|
123
|
+
? LISTINGS_I18N_KEYS.moderationRejectedStillLive
|
|
124
|
+
: LISTINGS_I18N_KEYS.moderationRejected,
|
|
125
|
+
tone: "warning",
|
|
126
|
+
liveDuringReview: live,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function listingStatusView(status, moderationStatus) {
|
|
130
|
+
const moderation = moderationNotice(status, moderationStatus);
|
|
131
|
+
return {
|
|
132
|
+
lifecycle: lifecycleCaption(status),
|
|
133
|
+
moderation,
|
|
134
|
+
liveUnderReview: moderation?.liveDuringReview === true,
|
|
135
|
+
allowedTransitions: LISTING_TRANSITIONS[status],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The dashboard tabs, and the statuses each one folds together.
|
|
140
|
+
*
|
|
141
|
+
* The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
|
|
142
|
+
* caption and its count cannot describe different sets: `active` includes
|
|
143
|
+
* PENDING beside PUBLISHED, and `drafts` includes REJECTED beside DRAFT —
|
|
144
|
+
* both because a listing in those states is something the owner is still
|
|
145
|
+
* working on. A client that grouped them its own way would show "3 active"
|
|
146
|
+
* over two rows.
|
|
147
|
+
*/
|
|
148
|
+
export const MY_LISTINGS_TABS = ["active", "drafts", "archived"];
|
|
149
|
+
export const MY_LISTINGS_TAB_STATUSES = {
|
|
150
|
+
active: ["published", "pending"],
|
|
151
|
+
drafts: ["draft", "rejected"],
|
|
152
|
+
archived: ["archived", "paused", "expired", "sold"],
|
|
153
|
+
};
|
|
154
|
+
/** Which tab a listing belongs to, or `undefined` for BLOCKED — the one
|
|
155
|
+
* status `my/counters` counts in no tab at all. A dashboard that silently
|
|
156
|
+
* dropped it would hide exactly the listing whose owner most needs to know. */
|
|
157
|
+
export function tabOf(status) {
|
|
158
|
+
for (const tab of MY_LISTINGS_TABS) {
|
|
159
|
+
if (MY_LISTINGS_TAB_STATUSES[tab].includes(status))
|
|
160
|
+
return tab;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/model/status.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lifecycle whitelist, mirrored from `stapel_listings.models
|
|
3
|
+
* .LISTING_TRANSITIONS`.
|
|
4
|
+
*
|
|
5
|
+
* ── A mirror, not a verdict ────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The server decides. `transition_to` raises `TransitionError` and the view
|
|
8
|
+
* answers **409 `error.409.invalid_listing_transition`** with
|
|
9
|
+
* `params.from_status`, and that refusal is rendered as the named sentence it
|
|
10
|
+
* is. What the mirror buys is the difference between a button that is there
|
|
11
|
+
* and does nothing and a button that is switched off WITH its reason: "you
|
|
12
|
+
* cannot mark a draft sold — publish it first" beats a 409 toast after the
|
|
13
|
+
* click, and beats a hidden control (which teaches nothing) by the whole
|
|
14
|
+
* private-space canon §6.3.
|
|
15
|
+
*
|
|
16
|
+
* The mirror is deliberately CONSERVATIVE in one direction only: it may never
|
|
17
|
+
* block something the server would allow. That is why it is a copy of the
|
|
18
|
+
* table rather than a hand-written summary of it, and why `test/status.test
|
|
19
|
+
* .ts` walks every source state.
|
|
20
|
+
*
|
|
21
|
+
* Two entries look odd and are correct:
|
|
22
|
+
* - `published → blocked` exists, but the owner API has no route to it: only
|
|
23
|
+
* `apply_moderation("rejected")` walks it. It is in the table because the
|
|
24
|
+
* table is the model's, and leaving it out would make the mirror a
|
|
25
|
+
* paraphrase.
|
|
26
|
+
* - `blocked → published` is reinstatement after a successful appeal, also
|
|
27
|
+
* moderation-driven. `archive` and `complete` are the only two transitions
|
|
28
|
+
* this pair's API can actually request.
|
|
29
|
+
*/
|
|
30
|
+
import type { ListingLifecycleStatus } from "../api/types.js";
|
|
31
|
+
export declare const LISTING_TRANSITIONS: Readonly<Record<ListingLifecycleStatus, readonly ListingLifecycleStatus[]>>;
|
|
32
|
+
/** Would the server accept this move? A same-status move is a no-op the
|
|
33
|
+
* server returns early on, so it counts as allowed. */
|
|
34
|
+
export declare function canTransition(from: ListingLifecycleStatus, to: ListingLifecycleStatus): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Deleting is not a transition and has its own rule
|
|
37
|
+
* (`views.destroy`): a PUBLISHED or PENDING listing is refused with
|
|
38
|
+
* `error.409.listing_cannot_delete_active` — archive it first.
|
|
39
|
+
*/
|
|
40
|
+
export declare function canDelete(status: ListingLifecycleStatus): boolean;
|
|
41
|
+
//# sourceMappingURL=transitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitions.d.ts","sourceRoot":"","sources":["../../src/model/transitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CACxC,MAAM,CAAC,sBAAsB,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAWlE,CAAC;AAEF;uDACuD;AACvD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,sBAAsB,GACzB,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const LISTING_TRANSITIONS = {
|
|
2
|
+
draft: ["pending", "archived"],
|
|
3
|
+
pending: ["published", "rejected", "draft", "archived"],
|
|
4
|
+
published: ["paused", "expired", "sold", "blocked", "archived"],
|
|
5
|
+
blocked: ["published", "draft", "archived"],
|
|
6
|
+
paused: ["published", "archived", "expired"],
|
|
7
|
+
expired: ["pending", "published", "archived"],
|
|
8
|
+
sold: ["archived", "published"],
|
|
9
|
+
rejected: ["draft", "archived"],
|
|
10
|
+
archived: ["draft"],
|
|
11
|
+
};
|
|
12
|
+
/** Would the server accept this move? A same-status move is a no-op the
|
|
13
|
+
* server returns early on, so it counts as allowed. */
|
|
14
|
+
export function canTransition(from, to) {
|
|
15
|
+
return from === to || LISTING_TRANSITIONS[from].includes(to);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deleting is not a transition and has its own rule
|
|
19
|
+
* (`views.destroy`): a PUBLISHED or PENDING listing is refused with
|
|
20
|
+
* `error.409.listing_cannot_delete_active` — archive it first.
|
|
21
|
+
*/
|
|
22
|
+
export function canDelete(status) {
|
|
23
|
+
return status !== "published" && status !== "pending";
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=transitions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitions.js","sourceRoot":"","sources":["../../src/model/transitions.ts"],"names":[],"mappings":"AA+BA,MAAM,CAAC,MAAM,mBAAmB,GAE5B;IACF,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,OAAO,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC;IACvD,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC;IAC/D,OAAO,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC;IAC3C,MAAM,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC;IAC5C,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,CAAC;IAC7C,IAAI,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF;uDACuD;AACvD,MAAM,UAAU,aAAa,CAC3B,IAA4B,EAC5B,EAA0B;IAE1B,OAAO,IAAI,KAAK,EAAE,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAA8B;IACtD,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,SAAS,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the composer knows before it asks, and what it does with the answer.
|
|
3
|
+
*
|
|
4
|
+
* Three jobs, and the boundaries between them are the point:
|
|
5
|
+
*
|
|
6
|
+
* 1. **The mirror.** Feature values are mirrored by
|
|
7
|
+
* `@stapel/attributes-react`'s `mirrorValidate` — the engine's own rules,
|
|
8
|
+
* the engine's own error keys, one implementation. This module adds only
|
|
9
|
+
* the checks that belong to LISTINGS rather than to a value type: the
|
|
10
|
+
* description length bounds and the price shape. Nothing here is a
|
|
11
|
+
* verdict; the server re-runs all of it.
|
|
12
|
+
*
|
|
13
|
+
* 2. **The split of the publish 400.** `POST /{pk}/publish/` answers a bad
|
|
14
|
+
* draft with a BARE `ValidationBatchResult` — no `localizable_error`, no
|
|
15
|
+
* envelope — so core wraps it as `stapel.http.400` with the batch sitting
|
|
16
|
+
* on `StapelApiError.body`. A promotion that fails afterwards answers the
|
|
17
|
+
* ordinary envelope (`error.400.publish_validation_failed`, or
|
|
18
|
+
* `error.400.image_required` where the deployment requires a photo). Two
|
|
19
|
+
* different 400s that mean different things; `publishRefusal` is the one
|
|
20
|
+
* place that tells them apart.
|
|
21
|
+
*
|
|
22
|
+
* 3. **Routing a refusal onto the control that caused it.**
|
|
23
|
+
* `featureErrorsBySlug` (attributes-react) adds the `field` param the
|
|
24
|
+
* fleet's `useFieldError` convention routes on — the engine sends
|
|
25
|
+
* `{feature, slug}` and never `field`. Listing-level rows (the
|
|
26
|
+
* description, which the batch carries under the slug `"description"`)
|
|
27
|
+
* are lifted out to their own control here.
|
|
28
|
+
*
|
|
29
|
+
* ── The mirror's ceilings are CONFIGURABLE, and that is not a nicety ───────
|
|
30
|
+
*
|
|
31
|
+
* `DESCRIPTION_MIN_LENGTH` / `DESCRIPTION_MAX_LENGTH` are `STAPEL_LISTINGS`
|
|
32
|
+
* settings a deployment moves (`conf.py`). A hardcoded 4/500 would refuse a
|
|
33
|
+
* perfectly valid submission on a host that widened them — and the server
|
|
34
|
+
* would never even hear about the refusal, so there would be nothing to
|
|
35
|
+
* appeal to. Same rule the cdn pair wrote down: **the mirror may not refuse
|
|
36
|
+
* what the server would accept.**
|
|
37
|
+
*/
|
|
38
|
+
import { StapelApiError } from "@stapel/core";
|
|
39
|
+
import type { FlowError } from "@stapel/core";
|
|
40
|
+
import type { FeatureDef, FeaturesDto, FeatureValidationResult, ValidationBatchResult } from "@stapel/attributes-react";
|
|
41
|
+
import type { ListingDraftValues } from "./draft.js";
|
|
42
|
+
/**
|
|
43
|
+
* The deployment's own ceilings, mirrored client-side.
|
|
44
|
+
*
|
|
45
|
+
* Defaults are the LIBRARY defaults from `stapel_listings/conf.py`; a host
|
|
46
|
+
* whose settings differ passes its own through `createListingsRuntime`.
|
|
47
|
+
*/
|
|
48
|
+
export interface ListingDraftLimits {
|
|
49
|
+
/** `STAPEL_LISTINGS["DESCRIPTION_MIN_LENGTH"]`. */
|
|
50
|
+
readonly descriptionMin: number;
|
|
51
|
+
/** `STAPEL_LISTINGS["DESCRIPTION_MAX_LENGTH"]`. */
|
|
52
|
+
readonly descriptionMax: number;
|
|
53
|
+
/** `Listing.title` is `max_length=255` — a MODEL constraint, so it moves
|
|
54
|
+
* only with a migration, but mirrored from the schema all the same. */
|
|
55
|
+
readonly titleMax: number;
|
|
56
|
+
/** How many photos a gallery may carry. stapel-cdn has no opinion and
|
|
57
|
+
* stapel-listings stores an unbounded list; the storefront's ceiling is 10
|
|
58
|
+
* (spec §4.1), which makes it the PAIR's rule — hence a setting and not a
|
|
59
|
+
* constant, and hence a refusal in this pair's own namespace. */
|
|
60
|
+
readonly maxImages: number;
|
|
61
|
+
/** `STAPEL_LISTINGS["REQUIRE_IMAGE_ON_PUBLISH"]` — library default `True`.
|
|
62
|
+
* The server raises a bare `ValidationError` for it, which the view turns
|
|
63
|
+
* into `error.400.publish_validation_failed`; mirroring it lets the
|
|
64
|
+
* composer say WHICH thing is missing before the round trip. */
|
|
65
|
+
readonly requireImageOnPublish: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare const DEFAULT_DRAFT_LIMITS: ListingDraftLimits;
|
|
68
|
+
/** The slug the description's refusal is filed under — the engine files it
|
|
69
|
+
* the same way (`validate_description` returns a `FeatureValidationResult`
|
|
70
|
+
* whose slug is not a feature's), so the composer has one routing table. */
|
|
71
|
+
export declare const DESCRIPTION_FIELD = "description";
|
|
72
|
+
export declare const TITLE_FIELD = "title";
|
|
73
|
+
export declare const PRICE_FIELD = "price";
|
|
74
|
+
export declare const IMAGES_FIELD = "images";
|
|
75
|
+
export declare const CATEGORY_FIELD = "category_id";
|
|
76
|
+
/**
|
|
77
|
+
* The listing-level half of the mirror: everything that is not a feature
|
|
78
|
+
* value. Returns refusals keyed by control, empty when nothing is wrong.
|
|
79
|
+
*
|
|
80
|
+
* Note what is NOT judged: a missing title, a missing price and a missing
|
|
81
|
+
* location. The server accepts a published listing without any of them
|
|
82
|
+
* (`publish_listing` promotes `title_draft or title` and leaves the rest),
|
|
83
|
+
* so refusing them here would block a submission the backend would take. A
|
|
84
|
+
* skin may still *encourage* them; that is copy, not a gate.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mirrorListingFields(values: ListingDraftValues, limits: ListingDraftLimits): Readonly<Record<string, FlowError>>;
|
|
87
|
+
/**
|
|
88
|
+
* The whole mirror: listing fields + feature values, in one routing table.
|
|
89
|
+
*
|
|
90
|
+
* Feature refusals go through `featureErrorsBySlug`, so a mirrored "too long"
|
|
91
|
+
* and a server "too long" render the SAME sentence from the SAME key — they
|
|
92
|
+
* are the same key, because the mirror speaks the engine's vocabulary.
|
|
93
|
+
*/
|
|
94
|
+
export declare function mirrorDraft(values: ListingDraftValues, features: readonly FeatureDef[], featuresDto: FeaturesDto, limits: ListingDraftLimits): Readonly<Record<string, FlowError>>;
|
|
95
|
+
/** Is this batch clean? `valid` is the server's own summary; a caller that
|
|
96
|
+
* recomputed it from `results` could disagree with the field the API sends. */
|
|
97
|
+
export declare function isBatchValid(batch: ValidationBatchResult): boolean;
|
|
98
|
+
/** What a failed publish actually was. */
|
|
99
|
+
export type PublishRefusal = {
|
|
100
|
+
/** The draft did not validate: per-control refusals, already routed. */
|
|
101
|
+
readonly kind: "invalid_draft";
|
|
102
|
+
readonly batch: ValidationBatchResult;
|
|
103
|
+
readonly fieldErrors: Readonly<Record<string, FlowError>>;
|
|
104
|
+
} | {
|
|
105
|
+
/** Anything else the server said, in the ordinary error dialect — a
|
|
106
|
+
* missing photo, a 403 on somebody else's listing, a 503. */
|
|
107
|
+
readonly kind: "error";
|
|
108
|
+
readonly error: StapelApiError;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Split a thrown publish failure into its two meanings.
|
|
112
|
+
*
|
|
113
|
+
* The check is on the BODY, not on the status: a 400 whose body is a batch is
|
|
114
|
+
* a per-field verdict, and a 400 whose body is an envelope is a sentence. A
|
|
115
|
+
* caller that branched on `status === 400` alone would put
|
|
116
|
+
* `error.400.image_required` under a feature control.
|
|
117
|
+
*/
|
|
118
|
+
export declare function publishRefusal(thrown: unknown): PublishRefusal;
|
|
119
|
+
/**
|
|
120
|
+
* A server batch → refusals keyed by CONTROL.
|
|
121
|
+
*
|
|
122
|
+
* One call, because the server already keys everything the way the composer
|
|
123
|
+
* does. `services.publish.validate_draft` inserts `validate_description`'s
|
|
124
|
+
* row at the FRONT of the same list, and that row's slug is literally
|
|
125
|
+
* `"description"` (`stapel_attributes/validation.py:726`) — the same key
|
|
126
|
+
* {@link DESCRIPTION_FIELD} names and the same key the mirror files its own
|
|
127
|
+
* length refusal under. So the description's control receives the server's
|
|
128
|
+
* verdict and the mirror's through one lookup, and neither can land in a
|
|
129
|
+
* page-level banner because nothing routed it.
|
|
130
|
+
*/
|
|
131
|
+
export declare function listingFieldErrors(batch: ValidationBatchResult): Readonly<Record<string, FlowError>>;
|
|
132
|
+
/** The rows of a batch that failed, in the order the server sent them —
|
|
133
|
+
* for a summary line above the form ("3 details need attention"). */
|
|
134
|
+
export declare function failedResults(batch: ValidationBatchResult): readonly FeatureValidationResult[];
|
|
135
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/model/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,cAAc,EAAoB,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;qEAGiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;oEAGgE;IAChE,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;CACzC;AAED,eAAO,MAAM,oBAAoB,EAAE,kBAMlC,CAAC;AAEF;;4EAE4E;AAC5E,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAU5C;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,kBAAkB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAmDrC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,SAAS,UAAU,EAAE,EAC/B,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,kBAAkB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAKrC;AAED;+EAC+E;AAC/E,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAElE;AAQD,0CAA0C;AAC1C,MAAM,MAAM,cAAc,GACtB;IACE,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CAC3D,GACD;IACE;iEAC6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,CAoB9D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,qBAAqB,GAC3B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAErC;AAED;qEACqE;AACrE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,SAAS,uBAAuB,EAAE,CAEpC"}
|