@wix/himalaya-cli 0.1537.0 → 0.1539.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/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +23 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +1 -0
- package/dist/cli.mjs +50 -7
- package/guides/component-reference.json +24 -39
- package/guides/component-reference.md +59 -13
- package/guides/dynamic-list.service.ts +7 -1
- package/guides/form-submit.service.ts +7 -1
- package/guides/himi-json-output.md +4 -2
- package/guides/iap-products.md +13 -5
- package/guides/index.json +2 -2
- package/guides/recipe-four-state.md +10 -1
- package/guides/sdk-reference.json +966 -46
- package/guides/search-list.service.ts +7 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +5021 -4164
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/beauty-salon/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/beauty-salon/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/branded-lite/dev/index.ts +4933 -4079
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/branded-lite/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/branded-lite/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/community-club/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/community-club/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/events-meetups/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/gym-fitness/dev/index.ts +5018 -4161
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/gym-fitness/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/gym-fitness/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/nonprofit-cause/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/podcast-creator/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/portfolio-creative/dev/index.ts +5017 -4157
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/portfolio-creative/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/portfolio-creative/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/restaurant/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/retail-store/dev/index.ts +5159 -4302
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/retail-store/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/retail-store/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/yoga-studio/dev/index.ts +0 -3
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/yoga-studio/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
|
@@ -41,11 +41,20 @@ interface OrderRow {
|
|
|
41
41
|
hasImage: boolean;
|
|
42
42
|
noImage: boolean;
|
|
43
43
|
detailScreenId: string;
|
|
44
|
+
dateLabel?: string;
|
|
45
|
+
itemSummary?: string;
|
|
46
|
+
cancelledLabel?: string;
|
|
47
|
+
notCancelled?: boolean;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface State {
|
|
47
51
|
status: "loading" | "ready" | "error";
|
|
52
|
+
permissionDenied?: boolean;
|
|
53
|
+
hideLoadError?: boolean;
|
|
54
|
+
hidePermissionError?: boolean;
|
|
48
55
|
rows: OrderRow[];
|
|
56
|
+
displayRows?: Array<OrderRow & { dateHeading: string; noDateHeading: boolean }>;
|
|
57
|
+
filterOptions?: Array<{ id: string; label: string; selected: boolean }>;
|
|
49
58
|
identityLine: string;
|
|
50
59
|
emptyText: string;
|
|
51
60
|
isLoading: boolean;
|
|
@@ -112,6 +121,9 @@ function orderRow(order: WixOrder): OrderRow {
|
|
|
112
121
|
const firstName = order.lineItems?.[0]?.productName?.original || order.lineItems?.[0]?.productName?.translated || "";
|
|
113
122
|
const title = copyTemplate("orders.orderTitle", "Order #{number}", { number: order.number || order.id || "" }).trim();
|
|
114
123
|
const image = wixMediaUrl(mediaUrl(order.lineItems?.[0]));
|
|
124
|
+
const itemSummary = copyTemplate(count === 1 ? "orders.oneItem" : "orders.manyItems", count === 1 ? "{count} item" : "{count} items", { count });
|
|
125
|
+
const status = (order as WixOrder & { status?: string }).status;
|
|
126
|
+
const cancelled = status === "CANCELED" || status === "CANCELLED";
|
|
115
127
|
return {
|
|
116
128
|
id,
|
|
117
129
|
title,
|
|
@@ -125,15 +137,42 @@ function orderRow(order: WixOrder): OrderRow {
|
|
|
125
137
|
// WS-C — canonical param-less detail screenId; the descriptor row binds the
|
|
126
138
|
// separate `id` field as a first-class nav param (no more baked suffixes).
|
|
127
139
|
detailScreenId: "himi_order",
|
|
140
|
+
dateLabel: formatDate(order.createdDate),
|
|
141
|
+
itemSummary,
|
|
142
|
+
cancelledLabel: cancelled ? copyParam("orders.cancelled", "Cancelled") : "",
|
|
143
|
+
notCancelled: !cancelled,
|
|
128
144
|
};
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
function derive(state: State): State {
|
|
132
148
|
const hasRows = state.rows.length > 0;
|
|
149
|
+
const rows = state.rows.map(row => {
|
|
150
|
+
// Earlier ownership-tagged cache entries only stored the combined meta and
|
|
151
|
+
// display status. Split those fields without inventing a missing date or
|
|
152
|
+
// cancellation status; current rows keep their explicit structured values.
|
|
153
|
+
const parts = (row.meta ?? "").split(" · ");
|
|
154
|
+
const cancelled = row.notCancelled === false || row.status === "Canceled" || row.status === "Cancelled";
|
|
155
|
+
return { ...row,
|
|
156
|
+
dateLabel: row.dateLabel ?? (parts.length > 1 ? parts[0] : ""),
|
|
157
|
+
itemSummary: row.itemSummary ?? (parts.length > 1 ? parts.slice(1).join(" · ") : row.meta ?? ""),
|
|
158
|
+
notCancelled: row.notCancelled ?? !cancelled,
|
|
159
|
+
cancelledLabel: row.cancelledLabel ?? (cancelled ? copyParam("orders.cancelled", "Cancelled") : ""),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
133
162
|
return {
|
|
134
163
|
...state,
|
|
164
|
+
displayRows: rows.map((row, index, rows) => {
|
|
165
|
+
// Older owned cache rows retain their original date in meta. Group only
|
|
166
|
+
// adjacent days so pagination and an explicit backend sort remain authoritative.
|
|
167
|
+
const date = row.dateLabel;
|
|
168
|
+
const previous = rows[index - 1];
|
|
169
|
+
const previousDate = previous?.dateLabel;
|
|
170
|
+
return { ...row, dateHeading: date, noDateHeading: !date || date === previousDate };
|
|
171
|
+
}),
|
|
135
172
|
isLoading: state.status === "loading",
|
|
136
173
|
notLoading: state.status !== "loading",
|
|
174
|
+
hideLoadError: state.status !== "error" || state.permissionDenied === true,
|
|
175
|
+
hidePermissionError: state.status !== "error" || state.permissionDenied !== true,
|
|
137
176
|
isErrored: state.status === "error",
|
|
138
177
|
notErrored: state.status !== "error",
|
|
139
178
|
// A guest gets the shared member gate, not the "Browse products" empty card.
|
|
@@ -152,6 +191,7 @@ function derive(state: State): State {
|
|
|
152
191
|
const FULFILLMENT: Record<string, string> = {
|
|
153
192
|
"Not fulfilled": "NOT_FULFILLED", "Partially fulfilled": "PARTIALLY_FULFILLED", "Fulfilled": "FULFILLED",
|
|
154
193
|
};
|
|
194
|
+
let lastReadDenied = false;
|
|
155
195
|
let pageMember: OrderMember | undefined;
|
|
156
196
|
let siteOpenInFlight = false;
|
|
157
197
|
// Whose open failed: the hint belongs to that member and scope only, never to whoever signs in next.
|
|
@@ -165,10 +205,13 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
165
205
|
source: {
|
|
166
206
|
id: (row: OrderRow) => row.id,
|
|
167
207
|
clearOnError: orderAccessDenied,
|
|
168
|
-
errorMessage: error =>
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
errorMessage: error => {
|
|
209
|
+
lastReadDenied = orderAccessDenied(error);
|
|
210
|
+
return lastReadDenied ? copyParam("orders.accessErrorText", "Orders are unavailable for this account. Try signing in again.")
|
|
211
|
+
: copyParam("orders.loadErrorText", "Orders could not be loaded.");
|
|
212
|
+
},
|
|
171
213
|
async fetch(criteria, cursor) {
|
|
214
|
+
lastReadDenied = false;
|
|
172
215
|
const member = await orderMember();
|
|
173
216
|
if (cursor && (!pageMember || member.email !== pageMember.email || member.scope !== pageMember.scope)) throw orderAccessChanged();
|
|
174
217
|
const narrowed = !!criteria.query.trim() || criteria.filter !== "all";
|
|
@@ -224,7 +267,8 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
224
267
|
// Rows Wix does return still list here.
|
|
225
268
|
const onSite = isHostedWorker() && !!member.email && status === "ready" && !page.items.length && !page.nextCursor
|
|
226
269
|
&& !page.pageError && page.metadata?.fromCache !== true;
|
|
227
|
-
return derive({ ...state, status, rows: page.items, nextCursor: page.nextCursor, complete: page.complete,
|
|
270
|
+
return derive({ ...state, permissionDenied: status === "error" && lastReadDenied, status, rows: page.items, nextCursor: page.nextCursor, complete: page.complete,
|
|
271
|
+
filterOptions: [{ id: "all", label: copyParam("collection.all", "All") }, ...Object.keys(FULFILLMENT).map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))].map(option => ({ ...option, selected: option.id === page.criteria.filter })),
|
|
228
272
|
pageError: page.pageError, loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
229
273
|
needsSignIn: page.metadata?.needsSignIn === true,
|
|
230
274
|
onSite,
|
|
@@ -233,10 +277,19 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
233
277
|
identityLine: member.email ? copyTemplate("orders.identityLine", "Signed in as {email}", { email: member.email }) : "",
|
|
234
278
|
emptyText: status === "error" ? page.error : !member.email ? copyParam("orders.signInEmptyText", "Sign in to see your orders.")
|
|
235
279
|
: onSite ? copyParam("orders.onSiteText", "Your orders are on the site.")
|
|
236
|
-
: page.items.length ? "" : copyParam("orders.emptyText", "No orders for this member yet."),
|
|
280
|
+
: page.items.length ? "" : page.metadata?.narrowed ? copyParam("orders.noMatches", "No orders match your search or filter.") : copyParam("orders.emptyText", "No orders for this member yet."),
|
|
237
281
|
});
|
|
238
282
|
},
|
|
239
283
|
actions: {
|
|
284
|
+
chooseFilter(_s: State, arg: unknown): undefined {
|
|
285
|
+
const id = (arg as { id?: string } | undefined)?.id;
|
|
286
|
+
if (id === "all" || (id?.startsWith("value:") && FULFILLMENT[id.slice(6)])) {
|
|
287
|
+
// Return before the queued collection reducer runs; awaiting a reducer
|
|
288
|
+
// of the same service would prevent it from executing in native workers.
|
|
289
|
+
void svc.collectionFilter({ selectedIds: [id] });
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
},
|
|
240
293
|
// The site's My Orders page, opened by the host signed in as the lent member where it can (a visitor who
|
|
241
294
|
// signs in on the site otherwise). Nothing is charged, so the guard only re-checks the member is unchanged.
|
|
242
295
|
async openOnSite(s: State): Promise<State | undefined> {
|
|
@@ -1,99 +1,48 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Prefer Catalog V3 Categories API (`queryStoreCategories`); V1 `queryCollections`
|
|
3
|
-
// returns HTTP 428 on modern CATALOG_V3 sites (docs/wix-authentication.md).
|
|
1
|
+
// Categories share the complete, site-ordered reader used by Home and Shop.
|
|
4
2
|
import { defineService } from "@himalaya/state";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
queryCollections,
|
|
9
|
-
queryStoreCategories,
|
|
10
|
-
type StoreCategory,
|
|
11
|
-
type WixCollection,
|
|
12
|
-
} from "@himalaya/wix-stores-collections";
|
|
13
|
-
|
|
14
|
-
interface Row { id: string; name: string; subtitle: string }
|
|
15
|
-
interface S {
|
|
3
|
+
import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
|
|
4
|
+
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
5
|
+
interface State {
|
|
16
6
|
status: "loading" | "ready" | "error";
|
|
17
|
-
|
|
7
|
+
all: StoreCategoryCard[];
|
|
8
|
+
collections: StoreCategoryCard[];
|
|
9
|
+
query: string;
|
|
18
10
|
emptyText: string;
|
|
19
|
-
|
|
11
|
+
noCollections: boolean;
|
|
12
|
+
notLoading: boolean;
|
|
13
|
+
notErrored: boolean;
|
|
14
|
+
noEmpty: boolean;
|
|
20
15
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
function derive(s: State): State {
|
|
17
|
+
const query = s.query.trim().toLowerCase();
|
|
18
|
+
const collections = s.all.filter(row => row.name.toLowerCase().includes(query));
|
|
19
|
+
return { ...s, collections, noCollections: !collections.length, notLoading: s.status !== "loading",
|
|
20
|
+
notErrored: s.status !== "error", noEmpty: s.status !== "ready" || !!collections.length,
|
|
21
|
+
emptyText: query ? copyParam("collections.noMatches", "No categories match your search.") : copyParam("collections.empty", "No categories yet.") };
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
|
|
33
|
-
|
|
34
|
-
async function loadCollections(): Promise<Loaded> {
|
|
35
|
-
try {
|
|
36
|
-
const r = await queryStoreCategories(50);
|
|
37
|
-
// A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
|
|
38
|
-
// line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
|
|
39
|
-
// that simply has no categories fell through to the V1 call — which `himi sdk
|
|
40
|
-
// wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
|
|
41
|
-
// catch below turned that 428 into an empty list. The screen then told the visitor "No
|
|
42
|
-
// categories yet" over a dead endpoint.
|
|
43
|
-
//
|
|
44
|
-
// Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
|
|
45
|
-
// the fallback exists for.
|
|
46
|
-
//
|
|
47
|
-
// WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
|
|
48
|
-
// reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
|
|
49
|
-
// wix-stores-collections` documents it and names the message. ASSUMED, and not yet
|
|
50
|
-
// verified against a live legacy site: that the reverse — the V3 Categories API, a separate
|
|
51
|
-
// app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
|
|
52
|
-
// `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
|
|
53
|
-
// "No categories yet" instead of its collections: the same wrong-empty-state this change
|
|
54
|
-
// fixes, moved to the other site type.
|
|
55
|
-
//
|
|
56
|
-
// It is left as a throw-only fallback deliberately, because the alternative is worse:
|
|
57
|
-
// falling back on a V3 empty result sends every modern site with a genuinely empty
|
|
58
|
-
// catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
|
|
59
|
-
// categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
|
|
60
|
-
// before changing this; five templates carry this file.
|
|
61
|
-
return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
|
|
62
|
-
} catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
|
|
63
|
-
try {
|
|
64
|
-
// No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
|
|
65
|
-
// declares `paging`, so this argument type-checks as written. Reach for a cast only when the
|
|
66
|
-
// types really cannot express the call — silencing them by habit hides the next real
|
|
67
|
-
// mismatch.
|
|
68
|
-
const r = await queryCollections({ query: { paging: { limit: 50 } } });
|
|
69
|
-
return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", withEmptyFlags({
|
|
76
|
-
state: { status: "loading", collections: [], emptyText: "", error: "" },
|
|
23
|
+
let generation = 0;
|
|
24
|
+
let currentQuery = "";
|
|
25
|
+
const svc = defineService<State, {
|
|
26
|
+
load: (s: State, arg?: unknown) => Promise<State | undefined>;
|
|
27
|
+
search: (s: State, arg?: { value?: string }) => State;
|
|
28
|
+
}>("spacesCollections", {
|
|
29
|
+
state: derive({ status: "loading", all: [], collections: [], query: "", emptyText: "", noCollections: true, notLoading: false, notErrored: true, noEmpty: true }),
|
|
77
30
|
actions: {
|
|
78
|
-
load: async () => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const line = "Couldn't load categories. Pull to refresh.";
|
|
87
|
-
return { status: "error", collections: [], emptyText: line, error: res.error };
|
|
31
|
+
load: async (s, arg) => {
|
|
32
|
+
stashCopy(arg);
|
|
33
|
+
const run = ++generation;
|
|
34
|
+
try {
|
|
35
|
+
const all = await loadStoreCategories({ includeAllProducts: true });
|
|
36
|
+
return run === generation ? derive({ ...s, query: currentQuery, all, status: "ready" }) : undefined;
|
|
37
|
+
} catch {
|
|
38
|
+
return run === generation ? derive({ ...s, query: currentQuery, status: "error" }) : undefined;
|
|
88
39
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
error: "",
|
|
94
|
-
};
|
|
40
|
+
},
|
|
41
|
+
search: (s, arg) => {
|
|
42
|
+
currentQuery = (arg?.value ?? "").slice(0, 256);
|
|
43
|
+
return derive({ ...s, query: currentQuery });
|
|
95
44
|
},
|
|
96
45
|
},
|
|
97
|
-
}
|
|
46
|
+
});
|
|
98
47
|
svc.load();
|
|
99
48
|
export { svc };
|
|
@@ -2,6 +2,8 @@ import { compareText, compareIso } from "./engine-intl.js";
|
|
|
2
2
|
/** Shared OTA collection contract. No host APIs: used by workers and builders. */
|
|
3
3
|
export interface CollectionConfig {
|
|
4
4
|
rows: string;
|
|
5
|
+
/** Flat customer history rows, divided rather than boxed. */
|
|
6
|
+
rowStyle?: "flat";
|
|
5
7
|
instanceInput?: string;
|
|
6
8
|
title: string;
|
|
7
9
|
subtitle?: string;
|
|
@@ -84,7 +86,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
84
86
|
himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
|
|
85
87
|
himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
|
|
86
88
|
himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
|
|
87
|
-
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
|
|
89
|
+
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
|
|
88
90
|
searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
|
|
89
91
|
sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
|
|
90
92
|
himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
|
|
@@ -139,7 +141,10 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
139
141
|
density: s.density === 'compact' ? 'compact' : 'comfortable',
|
|
140
142
|
sort: s.sort === 'az' || s.sort === 'za' ? s.sort : 'default',
|
|
141
143
|
query: typeof s.query === 'string' ? s.query.slice(0, 256) : '',
|
|
142
|
-
|
|
144
|
+
// Flat collections no longer offer Saved. Migrate its persisted value before
|
|
145
|
+
// the first backend request so an invisible old filter cannot empty the list.
|
|
146
|
+
filter: config.rowStyle === 'flat' && s.filter === 'saved' ? 'all'
|
|
147
|
+
: typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
|
|
143
148
|
savedIds: Array.isArray(s.savedIds) ? [...new Set(s.savedIds.filter((id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
|
|
144
149
|
savedViews: Array.isArray(s.savedViews) ? s.savedViews.filter((v): v is SavedCollectionView => !!v && typeof v === 'object' && typeof v.name === 'string' && !!v.name.trim()).slice(0, 8).map((v, i) => ({
|
|
145
150
|
id: `view-${i}`, name: v.name.trim().slice(0, 40),
|
|
@@ -147,7 +152,8 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
147
152
|
density: v.density === 'compact' ? 'compact' : 'comfortable',
|
|
148
153
|
sort: v.sort === 'az' || v.sort === 'za' ? v.sort : 'default',
|
|
149
154
|
query: typeof v.query === 'string' ? v.query.slice(0, 256) : '',
|
|
150
|
-
filter:
|
|
155
|
+
filter: config.rowStyle === 'flat' && v.filter === 'saved' ? 'all'
|
|
156
|
+
: typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
|
|
151
157
|
})) : [],
|
|
152
158
|
};
|
|
153
159
|
}
|
|
@@ -95,7 +95,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
95
95
|
if (sort === 'default' || sort === 'az' || sort === 'za') { prefs.sort = sort; changed = true; }
|
|
96
96
|
}
|
|
97
97
|
if (Object.prototype.hasOwnProperty.call(input, 'collectionFilter')) {
|
|
98
|
-
const
|
|
98
|
+
const requestedFilter = String(input.collectionFilter ?? '');
|
|
99
|
+
// Old links to Saved must migrate with the stored preference when this
|
|
100
|
+
// collection no longer exposes that filter.
|
|
101
|
+
const filter = config.rowStyle === 'flat' && requestedFilter === 'saved' ? 'all' : requestedFilter;
|
|
99
102
|
const configured = config.filterValues ?? [];
|
|
100
103
|
const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
|
|
101
104
|
const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
|
|
@@ -139,7 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
139
142
|
copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
|
|
140
143
|
});
|
|
141
144
|
if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
|
|
142
|
-
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
|
|
145
|
+
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
|
|
143
146
|
...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
|
|
144
147
|
const loading = s.status === 'loading' || s.isLoading === true;
|
|
145
148
|
// Older domain reducers expose `errored` rather than `isErrored`; backend
|
|
@@ -220,7 +223,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
220
223
|
? [copyParam(`collection.${name}.countOne`, config.countNoun[0]), copyParam(`collection.${name}.countMany`, config.countNoun[1])] : undefined }),
|
|
221
224
|
selectedSummary: `${prefs.selectedIds.length} ${copyParam('collection.selected', 'selected')}`,
|
|
222
225
|
filterChoices,
|
|
223
|
-
chips: config.filterValues ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
226
|
+
chips: config.filterValues && config.rowStyle !== 'flat' ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
224
227
|
} };
|
|
225
228
|
};
|
|
226
229
|
const actions: Record<string, Reducer> = {};
|
|
@@ -410,8 +410,11 @@ export function compareDateDesc(a: string, b: string): number {
|
|
|
410
410
|
// kind of number. So: one money pattern, one clock, one range.
|
|
411
411
|
|
|
412
412
|
// Money uses `formatCurrency(amount, currency, "en")` — the currency's NATURAL decimals
|
|
413
|
-
// ("₪70.00", "¥500")
|
|
414
|
-
//
|
|
413
|
+
// ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
|
|
414
|
+
// SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
|
|
415
|
+
// "₪69.50"), the format the redesigned service screen and services list were approved with. It
|
|
416
|
+
// must read the same on every screen that shows a service (the list, the service screen, the
|
|
417
|
+
// time screen, Home, the agenda); do not put those back on natural decimals one screen at a time.
|
|
415
418
|
|
|
416
419
|
const KNOWN_SYMBOLS = new Set(Object.values(CURRENCY_SYMBOL));
|
|
417
420
|
|
|
@@ -12110,7 +12110,6 @@ const SCREENS = runtimeSetting({
|
|
|
12110
12110
|
"showGrabber": false,
|
|
12111
12111
|
"autoDismissMs": 8000,
|
|
12112
12112
|
"presentedPath": "$service:himiBookingBookings.resultOpen",
|
|
12113
|
-
"label": "$service:himiBookingBookings.resultText",
|
|
12114
12113
|
"onDismiss": {
|
|
12115
12114
|
"actionId": "tier5.himiBookingBookings.closeResult"
|
|
12116
12115
|
},
|
|
@@ -29402,7 +29401,6 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
29402
29401
|
"modal": false,
|
|
29403
29402
|
"placement": "inline",
|
|
29404
29403
|
"showGrabber": false,
|
|
29405
|
-
"label": "$service:himiBookingTime.resultText",
|
|
29406
29404
|
"onDismiss": {
|
|
29407
29405
|
"actionId": "tier5.himiBookingTime.closeResult"
|
|
29408
29406
|
},
|
|
@@ -29532,7 +29530,6 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
29532
29530
|
"placement": "inline",
|
|
29533
29531
|
"showGrabber": false,
|
|
29534
29532
|
"autoDismissMs": 8000,
|
|
29535
|
-
"label": "$service:himiBookingTime.resultText",
|
|
29536
29533
|
"onDismiss": {
|
|
29537
29534
|
"actionId": "tier5.himiBookingTime.closeResult"
|
|
29538
29535
|
},
|
|
@@ -2,6 +2,8 @@ import { compareText, compareIso } from "./engine-intl.js";
|
|
|
2
2
|
/** Shared OTA collection contract. No host APIs: used by workers and builders. */
|
|
3
3
|
export interface CollectionConfig {
|
|
4
4
|
rows: string;
|
|
5
|
+
/** Flat customer history rows, divided rather than boxed. */
|
|
6
|
+
rowStyle?: "flat";
|
|
5
7
|
instanceInput?: string;
|
|
6
8
|
title: string;
|
|
7
9
|
subtitle?: string;
|
|
@@ -84,7 +86,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
84
86
|
himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
|
|
85
87
|
himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
|
|
86
88
|
himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
|
|
87
|
-
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
|
|
89
|
+
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
|
|
88
90
|
searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
|
|
89
91
|
sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
|
|
90
92
|
himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
|
|
@@ -139,7 +141,10 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
139
141
|
density: s.density === 'compact' ? 'compact' : 'comfortable',
|
|
140
142
|
sort: s.sort === 'az' || s.sort === 'za' ? s.sort : 'default',
|
|
141
143
|
query: typeof s.query === 'string' ? s.query.slice(0, 256) : '',
|
|
142
|
-
|
|
144
|
+
// Flat collections no longer offer Saved. Migrate its persisted value before
|
|
145
|
+
// the first backend request so an invisible old filter cannot empty the list.
|
|
146
|
+
filter: config.rowStyle === 'flat' && s.filter === 'saved' ? 'all'
|
|
147
|
+
: typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
|
|
143
148
|
savedIds: Array.isArray(s.savedIds) ? [...new Set(s.savedIds.filter((id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
|
|
144
149
|
savedViews: Array.isArray(s.savedViews) ? s.savedViews.filter((v): v is SavedCollectionView => !!v && typeof v === 'object' && typeof v.name === 'string' && !!v.name.trim()).slice(0, 8).map((v, i) => ({
|
|
145
150
|
id: `view-${i}`, name: v.name.trim().slice(0, 40),
|
|
@@ -147,7 +152,8 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
147
152
|
density: v.density === 'compact' ? 'compact' : 'comfortable',
|
|
148
153
|
sort: v.sort === 'az' || v.sort === 'za' ? v.sort : 'default',
|
|
149
154
|
query: typeof v.query === 'string' ? v.query.slice(0, 256) : '',
|
|
150
|
-
filter:
|
|
155
|
+
filter: config.rowStyle === 'flat' && v.filter === 'saved' ? 'all'
|
|
156
|
+
: typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
|
|
151
157
|
})) : [],
|
|
152
158
|
};
|
|
153
159
|
}
|
|
@@ -95,7 +95,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
95
95
|
if (sort === 'default' || sort === 'az' || sort === 'za') { prefs.sort = sort; changed = true; }
|
|
96
96
|
}
|
|
97
97
|
if (Object.prototype.hasOwnProperty.call(input, 'collectionFilter')) {
|
|
98
|
-
const
|
|
98
|
+
const requestedFilter = String(input.collectionFilter ?? '');
|
|
99
|
+
// Old links to Saved must migrate with the stored preference when this
|
|
100
|
+
// collection no longer exposes that filter.
|
|
101
|
+
const filter = config.rowStyle === 'flat' && requestedFilter === 'saved' ? 'all' : requestedFilter;
|
|
99
102
|
const configured = config.filterValues ?? [];
|
|
100
103
|
const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
|
|
101
104
|
const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
|
|
@@ -139,7 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
139
142
|
copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
|
|
140
143
|
});
|
|
141
144
|
if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
|
|
142
|
-
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
|
|
145
|
+
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
|
|
143
146
|
...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
|
|
144
147
|
const loading = s.status === 'loading' || s.isLoading === true;
|
|
145
148
|
// Older domain reducers expose `errored` rather than `isErrored`; backend
|
|
@@ -220,7 +223,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
220
223
|
? [copyParam(`collection.${name}.countOne`, config.countNoun[0]), copyParam(`collection.${name}.countMany`, config.countNoun[1])] : undefined }),
|
|
221
224
|
selectedSummary: `${prefs.selectedIds.length} ${copyParam('collection.selected', 'selected')}`,
|
|
222
225
|
filterChoices,
|
|
223
|
-
chips: config.filterValues ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
226
|
+
chips: config.filterValues && config.rowStyle !== 'flat' ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
224
227
|
} };
|
|
225
228
|
};
|
|
226
229
|
const actions: Record<string, Reducer> = {};
|
|
@@ -410,8 +410,11 @@ export function compareDateDesc(a: string, b: string): number {
|
|
|
410
410
|
// kind of number. So: one money pattern, one clock, one range.
|
|
411
411
|
|
|
412
412
|
// Money uses `formatCurrency(amount, currency, "en")` — the currency's NATURAL decimals
|
|
413
|
-
// ("₪70.00", "¥500")
|
|
414
|
-
//
|
|
413
|
+
// ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
|
|
414
|
+
// SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
|
|
415
|
+
// "₪69.50"), the format the redesigned service screen and services list were approved with. It
|
|
416
|
+
// must read the same on every screen that shows a service (the list, the service screen, the
|
|
417
|
+
// time screen, Home, the agenda); do not put those back on natural decimals one screen at a time.
|
|
415
418
|
|
|
416
419
|
const KNOWN_SYMBOLS = new Set(Object.values(CURRENCY_SYMBOL));
|
|
417
420
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Catalog V3 Categories first, V1 collections only when V3 THROWS (a legacy V1 site):
|
|
5
5
|
// a V3 success with zero rows is an empty catalogue, and V1 on a V3 site answers 428.
|
|
6
6
|
// Same rule as wix-spaces-collections; see the reasoning there.
|
|
7
|
-
import { categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
|
|
7
|
+
import { categoryCount, categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
|
|
8
8
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
9
9
|
|
|
10
10
|
export interface StoreCategoryCard {
|
|
@@ -13,6 +13,7 @@ export interface StoreCategoryCard {
|
|
|
13
13
|
image: string;
|
|
14
14
|
hasImage: boolean;
|
|
15
15
|
noImage: boolean;
|
|
16
|
+
subtitle?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/** "All Products" is a Wix system collection — every product is in it, so it is
|
|
@@ -20,12 +21,12 @@ export interface StoreCategoryCard {
|
|
|
20
21
|
const SYSTEM_NAMES = ["all products"];
|
|
21
22
|
const SYSTEM_COLLECTION_IDS = ["00000000-000000-000000-000000000001"];
|
|
22
23
|
|
|
23
|
-
function card(id: string, name: string, image: string): StoreCategoryCard {
|
|
24
|
-
return { id, name, image, hasImage: !!image, noImage: !image };
|
|
24
|
+
function card(id: string, name: string, image: string, subtitle = ""): StoreCategoryCard {
|
|
25
|
+
return { id, name, image, hasImage: !!image, noImage: !image, subtitle };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function fromCategory(c: StoreCategory): StoreCategoryCard {
|
|
28
|
-
return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c));
|
|
29
|
+
return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c), categoryCount(c));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function fromCollection(c: WixCollection): StoreCategoryCard {
|
|
@@ -34,19 +35,24 @@ function fromCollection(c: WixCollection): StoreCategoryCard {
|
|
|
34
35
|
return card(raw.id ?? "", (raw.name ?? "").trim(), wixMediaUrl(media?.image?.url ?? media?.thumbnail?.url ?? ""));
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function visible(rows: StoreCategoryCard[]): StoreCategoryCard[] {
|
|
38
|
+
function visible(rows: StoreCategoryCard[], includeAllProducts = false): StoreCategoryCard[] {
|
|
38
39
|
const seen = new Set<string>();
|
|
39
40
|
return rows.filter((r) => {
|
|
40
|
-
if (!r.id || !r.name || SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()) || seen.has(r.id)) return false;
|
|
41
|
+
if (!r.id || !r.name || (!includeAllProducts && (SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()))) || seen.has(r.id)) return false;
|
|
41
42
|
seen.add(r.id);
|
|
42
43
|
return true;
|
|
44
|
+
}).map(row => {
|
|
45
|
+
// Counts belong to the directory opt-in; Home rails and Shop chips retain their
|
|
46
|
+
// established five-field card contract.
|
|
47
|
+
const { subtitle, ...card } = row;
|
|
48
|
+
return includeAllProducts ? row : card;
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
const PAGE_SIZE = 50;
|
|
47
53
|
const MAX_PAGES = 100;
|
|
48
54
|
|
|
49
|
-
async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
55
|
+
async function loadLegacyCategories(includeAllProducts: boolean): Promise<StoreCategoryCard[]> {
|
|
50
56
|
const cards: StoreCategoryCard[] = [];
|
|
51
57
|
const seen = new Set<string>();
|
|
52
58
|
let offset = 0;
|
|
@@ -58,7 +64,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
58
64
|
if (reportedTotal != null) total = Math.max(total ?? 0, reportedTotal);
|
|
59
65
|
if (!rows.length) {
|
|
60
66
|
if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
|
|
61
|
-
return visible(cards);
|
|
67
|
+
return visible(cards, includeAllProducts);
|
|
62
68
|
}
|
|
63
69
|
const priorCount = seen.size;
|
|
64
70
|
rows.forEach(row => { if (row.id) seen.add(row.id); });
|
|
@@ -67,7 +73,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
67
73
|
offset += rows.length;
|
|
68
74
|
if (total != null ? offset >= total : rows.length < PAGE_SIZE) {
|
|
69
75
|
if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
|
|
70
|
-
return visible(cards);
|
|
76
|
+
return visible(cards, includeAllProducts);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
throw new Error("Category pagination exceeded the page limit");
|
|
@@ -75,11 +81,11 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
75
81
|
|
|
76
82
|
/** The store's browsable categories in site order. Rejects incomplete reads so callers
|
|
77
83
|
* can retain their last good catalog instead of invalidating a later category. */
|
|
78
|
-
export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
|
|
84
|
+
export async function loadStoreCategories({ includeAllProducts = false }: { includeAllProducts?: boolean } = {}): Promise<StoreCategoryCard[]> {
|
|
79
85
|
let r: StoreCategoriesResponse;
|
|
80
86
|
try {
|
|
81
87
|
r = await queryStoreCategories(PAGE_SIZE);
|
|
82
|
-
} catch { return loadLegacyCategories(); /* Only the initial failure can identify a legacy V1 site. */ }
|
|
88
|
+
} catch { return loadLegacyCategories(includeAllProducts); /* Only the initial failure can identify a legacy V1 site. */ }
|
|
83
89
|
const cards: StoreCategoryCard[] = [];
|
|
84
90
|
const cursors = new Set<string>();
|
|
85
91
|
const seen = new Set<string>();
|
|
@@ -92,7 +98,7 @@ export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
|
|
|
92
98
|
const next = r.pagingMetadata?.cursors?.next;
|
|
93
99
|
if (!next) {
|
|
94
100
|
if (seen.size < total) throw new Error("Category pagination is incomplete");
|
|
95
|
-
return visible(cards);
|
|
101
|
+
return visible(cards, includeAllProducts);
|
|
96
102
|
}
|
|
97
103
|
if (cursors.has(next)) throw new Error("Category pagination repeated a cursor");
|
|
98
104
|
cursors.add(next);
|
|
@@ -118,7 +118,7 @@ export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): st
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
|
|
121
|
+
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20" / "$19.50".
|
|
122
122
|
*
|
|
123
123
|
* Reads, in order of preference:
|
|
124
124
|
* 1. Bookings v2 fixed price — `payment.fixed.price.{value,currency}` (the
|
|
@@ -137,7 +137,9 @@ export function formatServicePrice(service: unknown): string {
|
|
|
137
137
|
if (amount == null) return "";
|
|
138
138
|
const currency = serviceCurrency(service);
|
|
139
139
|
try {
|
|
140
|
-
|
|
140
|
+
// A whole amount drops its cents ("₪70", not "₪70.00"), as formatServiceMoney and the service
|
|
141
|
+
// screen show it, so the same service reads the same price on every screen.
|
|
142
|
+
return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
|
|
141
143
|
} catch {
|
|
142
144
|
return `${amount}`;
|
|
143
145
|
}
|