@stapel/listings-react 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/dist/api/generated/schema.d.ts +125 -2
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/listingsApi.d.ts +20 -1
- package/dist/api/listingsApi.d.ts.map +1 -1
- package/dist/api/listingsApi.js +1 -0
- package/dist/api/listingsApi.js.map +1 -1
- package/dist/api/types.d.ts +32 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js +7 -0
- package/dist/api/types.js.map +1 -1
- package/dist/default/ListingCard.d.ts +11 -0
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +86 -7
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingPhoto.d.ts +40 -0
- package/dist/default/ListingPhoto.d.ts.map +1 -1
- package/dist/default/ListingPhoto.js +53 -6
- package/dist/default/ListingPhoto.js.map +1 -1
- package/dist/default/ListingSerpCard.d.ts.map +1 -1
- package/dist/default/ListingSerpCard.js +14 -4
- package/dist/default/ListingSerpCard.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts +19 -1
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +99 -12
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/headless/ListingActions.d.ts +64 -13
- package/dist/headless/ListingActions.d.ts.map +1 -1
- package/dist/headless/ListingActions.js +60 -5
- package/dist/headless/ListingActions.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +8 -2
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +20 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +52 -1
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +12 -2
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/model/mine.d.ts +20 -0
- package/dist/model/mine.d.ts.map +1 -1
- package/dist/model/mine.js +22 -0
- package/dist/model/mine.js.map +1 -1
- package/dist/model/mutations.d.ts +24 -1
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +28 -0
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +26 -9
- package/dist/model/status.js.map +1 -1
- package/dist/model/transitions.d.ts +38 -1
- package/dist/model/transitions.d.ts.map +1 -1
- package/dist/model/transitions.js +73 -1
- package/dist/model/transitions.js.map +1 -1
- package/llms.txt +4 -2
- package/manifest.json +26 -2
- package/nav-manifest.json +1 -1
- package/package.json +5 -5
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +125 -2
- package/src/api/listingsApi.ts +27 -0
- package/src/api/types.ts +47 -2
- package/src/default/ListingCard.tsx +115 -12
- package/src/default/ListingPhoto.tsx +102 -18
- package/src/default/ListingSerpCard.tsx +17 -4
- package/src/default/MyListingsPane.tsx +205 -39
- package/src/headless/ListingActions.tsx +137 -16
- package/src/i18n/es.ts +8 -2
- package/src/i18n/keys.ts +52 -1
- package/src/i18n/ru.ts +12 -2
- package/src/index.ts +19 -3
- package/src/model/mine.ts +23 -0
- package/src/model/mutations.ts +41 -0
- package/src/model/status.ts +27 -9
- package/src/model/transitions.ts +80 -1
package/src/i18n/ru.ts
CHANGED
|
@@ -53,8 +53,12 @@ export const listingsI18nBundleRu: I18nDictionary = {
|
|
|
53
53
|
"error.403.listing_not_owner": "Это не ваше объявление",
|
|
54
54
|
"error.404.listing_not_found": "Объявление не найдено",
|
|
55
55
|
"error.409.already_favorited": "Объявление уже в избранном",
|
|
56
|
+
// No `{from_status}`: what landed in that slot was the wire value
|
|
57
|
+
// ('draft', 'archived'), and the status is already named in the reader's
|
|
58
|
+
// own words beside the control. See the note on this pair of keys in
|
|
59
|
+
// keys.ts.
|
|
56
60
|
"error.409.invalid_listing_transition":
|
|
57
|
-
"
|
|
61
|
+
"Из текущего состояния объявление так перевести нельзя",
|
|
58
62
|
"error.409.listing_cannot_delete_active":
|
|
59
63
|
"Сначала снимите объявление с публикации — активное удалить нельзя",
|
|
60
64
|
|
|
@@ -213,6 +217,12 @@ export const listingsI18nBundleRu: I18nDictionary = {
|
|
|
213
217
|
"listings.mine.archive": "В архив",
|
|
214
218
|
"listings.mine.complete": "Отметить проданным",
|
|
215
219
|
"listings.mine.delete": "Удалить",
|
|
220
|
+
"listings.mine.move.published": "Опубликовать снова",
|
|
221
|
+
"listings.mine.move.pending": "Отправить на проверку",
|
|
222
|
+
"listings.mine.move.paused": "Снять с публикации",
|
|
223
|
+
"listings.mine.move.draft": "Вернуть в черновики",
|
|
224
|
+
"listings.mine.move.renew": "Продлить",
|
|
225
|
+
"listings.mine.view": "Посмотреть",
|
|
216
226
|
"listings.mine.delete_confirm_title": "Удалить объявление?",
|
|
217
227
|
"listings.mine.delete_confirm_body":
|
|
218
228
|
"Оно исчезнет из кабинета, и вернуть его будет нельзя. Архив сохраняет его.",
|
|
@@ -240,7 +250,7 @@ export const listingsI18nBundleRu: I18nDictionary = {
|
|
|
240
250
|
"listings.blocked.mandate_unknown":
|
|
241
251
|
"Не удалось проверить ваш аккаунт, поэтому мы не угадываем, можно ли вам это",
|
|
242
252
|
"listings.blocked.transition":
|
|
243
|
-
"
|
|
253
|
+
"Из текущего состояния объявление так перевести нельзя",
|
|
244
254
|
"listings.blocked.delete_active":
|
|
245
255
|
"Сначала уберите в архив — объявление в продаже удалить нельзя",
|
|
246
256
|
"listings.blocked.in_flight": "Секунду — это уже выполняется",
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export type {
|
|
|
91
91
|
ListingFeatureView,
|
|
92
92
|
ListingLifecycleStatus,
|
|
93
93
|
ListingModerationStatus,
|
|
94
|
+
ListingOwnerTransition,
|
|
94
95
|
ListingPageParams,
|
|
95
96
|
ListingStatusInfo,
|
|
96
97
|
MyCounters,
|
|
@@ -120,7 +121,13 @@ export type {
|
|
|
120
121
|
ModerationNotice,
|
|
121
122
|
MyListingsTab,
|
|
122
123
|
} from "./model/status.js";
|
|
123
|
-
export {
|
|
124
|
+
export {
|
|
125
|
+
LISTING_TRANSITIONS,
|
|
126
|
+
OWNER_TRANSITIONS,
|
|
127
|
+
canDelete,
|
|
128
|
+
canTransition,
|
|
129
|
+
ownerMoves,
|
|
130
|
+
} from "./model/transitions.js";
|
|
124
131
|
|
|
125
132
|
// ── model: the owner's own rows ──────────────────────────────────────────────
|
|
126
133
|
export { defaultMyListingsSource } from "./model/mineSource.js";
|
|
@@ -219,8 +226,13 @@ export {
|
|
|
219
226
|
useFavoriteListing,
|
|
220
227
|
usePublishListing,
|
|
221
228
|
useSaveDraft,
|
|
229
|
+
useTransitionListing,
|
|
230
|
+
} from "./model/mutations.js";
|
|
231
|
+
export type {
|
|
232
|
+
FavoriteInput,
|
|
233
|
+
ListingTransitionInput,
|
|
234
|
+
SaveDraftInput,
|
|
222
235
|
} from "./model/mutations.js";
|
|
223
|
-
export type { FavoriteInput, SaveDraftInput } from "./model/mutations.js";
|
|
224
236
|
|
|
225
237
|
// ── flows (zero-flow shim — stapel-listings annotates none) ──────────────────
|
|
226
238
|
export { LISTINGS_FLOWS, flowEndpoints } from "./flows/registry.js";
|
|
@@ -275,7 +287,11 @@ export type {
|
|
|
275
287
|
UseFavoritesOptions,
|
|
276
288
|
} from "./headless/Favorites.js";
|
|
277
289
|
export { useListingActions } from "./headless/ListingActions.js";
|
|
278
|
-
export type {
|
|
290
|
+
export type {
|
|
291
|
+
ListingActionsBag,
|
|
292
|
+
ListingMove,
|
|
293
|
+
UseListingActionsOptions,
|
|
294
|
+
} from "./headless/ListingActions.js";
|
|
279
295
|
|
|
280
296
|
// ── nav manifest (the pair's public surface declaration) ─────────────────────
|
|
281
297
|
export { ACCOUNT_ROOT_ID, navEntries } from "./nav/manifest.js";
|
package/src/model/mine.ts
CHANGED
|
@@ -68,3 +68,26 @@ export function myListingImages(row: MyListingCard): readonly string[] {
|
|
|
68
68
|
export function showsDraft(row: MyListingCard): boolean {
|
|
69
69
|
return empty(row.title) && !empty(row.title_draft);
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Has this listing ever been in front of anybody?
|
|
74
|
+
*
|
|
75
|
+
* The server's own predicate, and it is the one that decides whether a row
|
|
76
|
+
* has a page to link to: `moderation_status` defaults to `NOT_SUBMITTED`
|
|
77
|
+
* (stapel-listings 0.20.0) and `publish_listing` sets `PENDING`
|
|
78
|
+
* unconditionally, so DRAFT + NOT_SUBMITTED means "nobody has ever pressed
|
|
79
|
+
* publish on this" and every other combination means somebody has.
|
|
80
|
+
*
|
|
81
|
+
* NOT {@link showsDraft}: that asks which HALF of the twin a row is showing,
|
|
82
|
+
* which is a different question and answers `false` for an empty draft that
|
|
83
|
+
* has no `title_draft` either — a row with no title at all would have been
|
|
84
|
+
* given a link to a page that does not exist.
|
|
85
|
+
*
|
|
86
|
+
* A row from a server older than 0.20.0 has no `moderation_status` of
|
|
87
|
+
* `not_submitted` to report, so it falls on the "has been submitted" side and
|
|
88
|
+
* keeps its link: an extra link to a draft its owner can read is a smaller
|
|
89
|
+
* harm than withholding one from a listing that is live.
|
|
90
|
+
*/
|
|
91
|
+
export function neverSubmitted(row: MyListingCard): boolean {
|
|
92
|
+
return row.status === "draft" && row.moderation_status === "not_submitted";
|
|
93
|
+
}
|
package/src/model/mutations.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ListingActionResponse,
|
|
8
8
|
ListingDraft,
|
|
9
9
|
ListingDraftPatch,
|
|
10
|
+
ListingOwnerTransition,
|
|
10
11
|
PublishResponse,
|
|
11
12
|
} from "../api/types.js";
|
|
12
13
|
import { useListingsApi } from "./context.js";
|
|
@@ -162,6 +163,46 @@ export function useCompleteListing(): UseMutationResult<
|
|
|
162
163
|
});
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
/** What a lifecycle move carries: the row, and where it is going. */
|
|
167
|
+
export interface ListingTransitionInput {
|
|
168
|
+
readonly id: number;
|
|
169
|
+
readonly to: ListingOwnerTransition;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Move a listing along ONE edge of the seller's half of the state machine
|
|
174
|
+
* (`POST listings/{id}/transition/`, stapel-listings 0.20.0).
|
|
175
|
+
*
|
|
176
|
+
* The generalisation of {@link useArchiveListing} and
|
|
177
|
+
* {@link useCompleteListing}, and the reason it exists is that those two were
|
|
178
|
+
* the whole owner API for two releases: both are EXITS, so a listing that
|
|
179
|
+
* reached SOLD, ARCHIVED, PAUSED or EXPIRED had no call that would move it
|
|
180
|
+
* again and a cabinet honestly had nothing to offer but Delete. The way back
|
|
181
|
+
* — SOLD or PAUSED to PUBLISHED, ARCHIVED to DRAFT, EXPIRED renewed — was in
|
|
182
|
+
* `LISTING_TRANSITIONS` the entire time and simply had no route.
|
|
183
|
+
*
|
|
184
|
+
* Same 409 as the two named exits (`error.409.invalid_listing_transition`
|
|
185
|
+
* with `params.from_status`), and the same invalidation: a move changes which
|
|
186
|
+
* tab the row belongs to, so the counters and every `my/listings` page go
|
|
187
|
+
* with it.
|
|
188
|
+
*/
|
|
189
|
+
export function useTransitionListing(): UseMutationResult<
|
|
190
|
+
ListingActionResponse,
|
|
191
|
+
StapelApiError,
|
|
192
|
+
ListingTransitionInput
|
|
193
|
+
> {
|
|
194
|
+
const api = useListingsApi();
|
|
195
|
+
const queryClient = useQueryClient();
|
|
196
|
+
return useMutation({
|
|
197
|
+
mutationFn: (input: ListingTransitionInput) =>
|
|
198
|
+
api.transition(input.id, input.to),
|
|
199
|
+
retry: false,
|
|
200
|
+
onSuccess: (_data, input) => {
|
|
201
|
+
invalidateListing(queryClient, input.id);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
165
206
|
/** Soft-delete. Refused with `error.409.listing_cannot_delete_active` while
|
|
166
207
|
* the listing is PUBLISHED or PENDING. */
|
|
167
208
|
export function useDeleteListing(): UseMutationResult<
|
package/src/model/status.ts
CHANGED
|
@@ -168,6 +168,11 @@ export function moderationNotice(
|
|
|
168
168
|
return undefined;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// NOTHING HAS BEEN SUBMITTED, so there is nothing for moderation to say.
|
|
172
|
+
// The lifecycle already calls this row a draft, and a second line beside
|
|
173
|
+
// that word can only add a claim the data does not support.
|
|
174
|
+
if (moderationStatus === "not_submitted") return undefined;
|
|
175
|
+
|
|
171
176
|
if (moderationStatus === "pending") {
|
|
172
177
|
if (live) {
|
|
173
178
|
return {
|
|
@@ -207,15 +212,28 @@ export function moderationNotice(
|
|
|
207
212
|
};
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
if (moderationStatus === "rejected") {
|
|
216
|
+
// …and it matters whether the lifecycle followed the verdict.
|
|
217
|
+
return {
|
|
218
|
+
moderationStatus,
|
|
219
|
+
messageKey: live
|
|
220
|
+
? LISTINGS_I18N_KEYS.moderationRejectedStillLive
|
|
221
|
+
: LISTINGS_I18N_KEYS.moderationRejected,
|
|
222
|
+
tone: "warning",
|
|
223
|
+
liveDuringReview: live,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// AN UNKNOWN VALUE SAYS NOTHING. This used to be the `rejected` branch's
|
|
228
|
+
// fallthrough, which made "not one of the three I know" mean "a moderator
|
|
229
|
+
// turned this down" — and then stapel-listings 0.20.0 added a fourth value
|
|
230
|
+
// and made it the DEFAULT, so every freshly created draft in the cabinet
|
|
231
|
+
// was announced as "A moderator turned this listing down. Fix it and send
|
|
232
|
+
// it again." to a person who had submitted nothing (D166). A row read from a
|
|
233
|
+
// server newer than this pair must degrade to silence, never to a verdict:
|
|
234
|
+
// the lifecycle caption beside it is still true, and an accusation is the
|
|
235
|
+
// one thing a client must not invent.
|
|
236
|
+
return undefined;
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
/** Both halves at once — what a dashboard row and a detail header render. */
|
package/src/model/transitions.ts
CHANGED
|
@@ -44,7 +44,13 @@ export const LISTING_TRANSITIONS: Readonly<
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/** Would the server accept this move? A same-status move is a no-op the
|
|
47
|
-
* server returns early on, so it counts as allowed.
|
|
47
|
+
* server returns early on, so it counts as allowed.
|
|
48
|
+
*
|
|
49
|
+
* "Accept" and "offer" are NOT the same question, and conflating them is the
|
|
50
|
+
* whole of defect D182: `canTransition("sold", "sold")` is true, so a SOLD
|
|
51
|
+
* row's "Mark sold" button was enabled, clickable, and did nothing — and so
|
|
52
|
+
* was an ARCHIVED row's "Archive". Ask {@link ownerMoves} what to OFFER.
|
|
53
|
+
*/
|
|
48
54
|
export function canTransition(
|
|
49
55
|
from: ListingLifecycleStatus,
|
|
50
56
|
to: ListingLifecycleStatus
|
|
@@ -52,6 +58,79 @@ export function canTransition(
|
|
|
52
58
|
return from === to || LISTING_TRANSITIONS[from].includes(to);
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The SELLER's half of the machine above, mirrored from
|
|
63
|
+
* `stapel_listings.models.OWNER_TRANSITIONS` (0.20.0).
|
|
64
|
+
*
|
|
65
|
+
* A subset of {@link LISTING_TRANSITIONS}, and what it leaves out is as
|
|
66
|
+
* load-bearing as what it keeps: `pending → published` and
|
|
67
|
+
* `blocked → published` are moderation's decisions, `published → blocked` is
|
|
68
|
+
* a takedown, and none of the three is a thing one does to oneself. Putting
|
|
69
|
+
* them here would make the cabinet a self-service publish gate.
|
|
70
|
+
*
|
|
71
|
+
* ── This is a FALLBACK, and the card's own field is the answer ─────────────
|
|
72
|
+
*
|
|
73
|
+
* `MyListingCard.available_transitions` is the server reporting this table
|
|
74
|
+
* for one row, and where it is present nothing here is consulted: the set a
|
|
75
|
+
* client offers and the set the route accepts are then one object rather than
|
|
76
|
+
* two that agree today. This copy is for a surface that holds only a `status`
|
|
77
|
+
* — a detail pane, a listing loaded before 0.20.0 — where the alternative is
|
|
78
|
+
* offering nothing at all.
|
|
79
|
+
*/
|
|
80
|
+
export const OWNER_TRANSITIONS: Readonly<
|
|
81
|
+
Record<ListingLifecycleStatus, readonly ListingLifecycleStatus[]>
|
|
82
|
+
> = {
|
|
83
|
+
draft: ["pending", "archived"],
|
|
84
|
+
pending: ["draft", "archived"],
|
|
85
|
+
published: ["paused", "sold", "archived"],
|
|
86
|
+
paused: ["published", "archived"],
|
|
87
|
+
expired: ["pending", "archived"],
|
|
88
|
+
sold: ["published", "archived"],
|
|
89
|
+
rejected: ["draft", "archived"],
|
|
90
|
+
blocked: ["draft", "archived"],
|
|
91
|
+
archived: ["draft"],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The order the moves are DRAWN in, wherever they came from.
|
|
96
|
+
*
|
|
97
|
+
* A row's action set changes as the listing moves through its life, and a set
|
|
98
|
+
* that also reorders itself makes a person hunt for the button they used
|
|
99
|
+
* yesterday. Forward first (back on sale, then submitted, then paused), exits
|
|
100
|
+
* last — so "Archive" is never where "Publish again" was a moment ago.
|
|
101
|
+
*/
|
|
102
|
+
const MOVE_ORDER: readonly ListingLifecycleStatus[] = [
|
|
103
|
+
"published",
|
|
104
|
+
"pending",
|
|
105
|
+
"paused",
|
|
106
|
+
"sold",
|
|
107
|
+
"draft",
|
|
108
|
+
"rejected",
|
|
109
|
+
"blocked",
|
|
110
|
+
"expired",
|
|
111
|
+
"archived",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What to OFFER a seller looking at a row in *status*, in drawing order.
|
|
116
|
+
*
|
|
117
|
+
* Never the status the row is already in: a control that would leave the
|
|
118
|
+
* world exactly as it found it is not an action, and four of them down a
|
|
119
|
+
* dashboard taught a seller that half the buttons on this screen do nothing.
|
|
120
|
+
*
|
|
121
|
+
* Pass the card's own `available_transitions` when it has one — see
|
|
122
|
+
* {@link OWNER_TRANSITIONS} for why the mirror is the second-best answer.
|
|
123
|
+
*/
|
|
124
|
+
export function ownerMoves(
|
|
125
|
+
status: ListingLifecycleStatus | undefined,
|
|
126
|
+
available?: readonly ListingLifecycleStatus[] | undefined
|
|
127
|
+
): readonly ListingLifecycleStatus[] {
|
|
128
|
+
if (status === undefined) return [];
|
|
129
|
+
const offered = available ?? OWNER_TRANSITIONS[status];
|
|
130
|
+
const set = new Set(offered.filter((to) => to !== status));
|
|
131
|
+
return MOVE_ORDER.filter((to) => set.has(to));
|
|
132
|
+
}
|
|
133
|
+
|
|
55
134
|
/**
|
|
56
135
|
* Deleting is not a transition and has its own rule
|
|
57
136
|
* (`views.destroy`): a PUBLISHED or PENDING listing is refused with
|