@wix/himalaya-cli 0.1578.0 → 0.1580.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/calendar/src/index.d.ts +42 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/motion/src/index.d.ts +51 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/media/src/index.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/media/src/rendition.d.ts +42 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +46 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +23 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-media.mjs +78 -0
- package/dist/cli.mjs +9027 -7450
- package/dist/preview-assets.generated.bin +0 -0
- package/dist/quickjs-intl.js +462 -0
- package/dist/quickjs-web-globals.js +540 -0
- package/dist/quickjs-worker-entry.js +313 -0
- package/guides/himi-json-output.md +30 -1
- package/guides/one-app-section-mappings.md +30 -5
- package/guides/one-app-to-himi-app.md +214 -6
- package/guides/one-app-viewer.md +2 -2
- package/guides/sdk-reference.json +79 -7
- package/guides/web-page-sdk-reference.md +1 -1
- package/package.json +3 -2
- package/templates/beauty-salon/dev/index.ts +14 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/branded-lite/dev/index.ts +14 -0
- package/templates/branded-lite/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +40 -2
- package/templates/brandy/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/community-club/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/gym-fitness/dev/index.ts +14 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/restaurant/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/retail-store/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/yoga-studio/dev/index.ts +14 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-bookings.ts +21 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +8 -3
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +36 -12
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +40 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +8 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
11
11
|
import { COLLECTION_CHROME_MIN_ITEMS } from "../_shared/collection-contract.js";
|
|
12
12
|
import { queryServiceCategories, queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
13
|
-
import { categoryName, courseDatesText, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
13
|
+
import { categoryName, courseDatesText, courseHasEnded, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
14
14
|
import { formatServicePrice } from "../_shared/wix-service-price.js";
|
|
15
15
|
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
16
16
|
import { initials } from "../_shared/himi-text.js";
|
|
@@ -60,6 +60,9 @@ interface Row {
|
|
|
60
60
|
/** "Class" or "Course"; "" for an appointment. */
|
|
61
61
|
typeTag: string;
|
|
62
62
|
noTypeTag: boolean;
|
|
63
|
+
endedText: string;
|
|
64
|
+
/** Retain course dates so a cached catalog can refresh its status after the course ends. */
|
|
65
|
+
courseSchedule?: { firstSessionStart?: string; lastSessionEnd?: string };
|
|
63
66
|
/** "1 hr · ₪70"; a course: "Nov 3 – Dec 15, 2026 · ₪1,200". "" when the record has neither. */
|
|
64
67
|
meta: string;
|
|
65
68
|
/** Online booking by time is possible: online booking is on and the service is not a course. */
|
|
@@ -186,8 +189,8 @@ function typeTagOf(type: unknown): string {
|
|
|
186
189
|
return "";
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta">): string {
|
|
190
|
-
return [row.name, row.typeTag, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
192
|
+
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta" | "endedText">): string {
|
|
193
|
+
return [row.name, row.typeTag, row.endedText, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
function bookCtaFor(request: boolean): string {
|
|
@@ -212,6 +215,11 @@ function norm(service: WixService): Row {
|
|
|
212
215
|
].filter(Boolean);
|
|
213
216
|
const image = imageUrl(service);
|
|
214
217
|
const isCourse = service.type === "COURSE";
|
|
218
|
+
const courseSchedule = isCourse ? {
|
|
219
|
+
firstSessionStart: service.schedule?.firstSessionStart,
|
|
220
|
+
lastSessionEnd: service.schedule?.lastSessionEnd,
|
|
221
|
+
} : undefined;
|
|
222
|
+
const endedText = isCourse && courseHasEnded(service, Date.now()) ? copyParam("services.ended", "Ended") : "";
|
|
215
223
|
const price = servicePriceLine(service, listLabel).text;
|
|
216
224
|
const meta = [isCourse ? courseDatesText(service) : serviceDurationText(service, listLabel), price].filter(Boolean).join(" · ");
|
|
217
225
|
const typeTag = typeTagOf(service.type);
|
|
@@ -234,18 +242,20 @@ function norm(service: WixService): Row {
|
|
|
234
242
|
tagline,
|
|
235
243
|
typeTag,
|
|
236
244
|
noTypeTag: !typeTag,
|
|
245
|
+
endedText,
|
|
246
|
+
courseSchedule,
|
|
237
247
|
meta,
|
|
238
248
|
canBook,
|
|
239
249
|
noBook: !canBook,
|
|
240
250
|
bookCta: bookCtaFor(request),
|
|
241
251
|
bookA11y: bookLabel(name, request),
|
|
242
|
-
a11y: spokenName({ name, typeTag, tagline, meta }),
|
|
252
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
243
253
|
};
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
/** A row saved by an older worker (offline cache, restored state) has none of the new
|
|
247
257
|
* fields: it renders with no tagline, meta or tag, and no Book pill until the live read. */
|
|
248
|
-
function withRowDefaults(row: Partial<Row
|
|
258
|
+
function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
|
|
249
259
|
const name = typeof row.name === "string" && row.name ? row.name : "Service";
|
|
250
260
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
251
261
|
const image = str(row.image);
|
|
@@ -253,6 +263,9 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
253
263
|
const tagline = str(row.tagline);
|
|
254
264
|
const meta = str(row.meta);
|
|
255
265
|
const canBook = row.canBook === true;
|
|
266
|
+
const courseSchedule = row.courseSchedule;
|
|
267
|
+
const endedText = courseSchedule && courseHasEnded({ schedule: courseSchedule }, Date.now())
|
|
268
|
+
? copyParam("services.ended", "Ended", shownEnded || str(row.endedText) || "Ended") : "";
|
|
256
269
|
return {
|
|
257
270
|
...row,
|
|
258
271
|
id: str(row.id),
|
|
@@ -268,12 +281,13 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
268
281
|
tagline,
|
|
269
282
|
typeTag,
|
|
270
283
|
noTypeTag: !typeTag,
|
|
284
|
+
endedText,
|
|
271
285
|
meta,
|
|
272
286
|
canBook,
|
|
273
287
|
noBook: !canBook,
|
|
274
288
|
bookCta: str(row.bookCta) || bookCtaFor(false),
|
|
275
289
|
bookA11y: str(row.bookA11y) || bookLabel(name),
|
|
276
|
-
a11y:
|
|
290
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
277
291
|
};
|
|
278
292
|
}
|
|
279
293
|
|
|
@@ -287,6 +301,9 @@ let categoriesInFlight = false;
|
|
|
287
301
|
let categoriesFailedAt = 0;
|
|
288
302
|
const CATEGORY_RETRY_MS = 60_000;
|
|
289
303
|
const rowCategories = new Map<string, number>();
|
|
304
|
+
/** A restored Ended label bridges only the first live page after boot, because an app that sends no
|
|
305
|
+
* copy pack gives no sign that none is coming: after it, current copy (pack or stock) decides. */
|
|
306
|
+
let restoredEndedBridge = true;
|
|
290
307
|
|
|
291
308
|
/** Repaint the chips only, on the state already shown. Re-deriving from the query controller
|
|
292
309
|
* would publish its working state, which a revalidation deliberately holds back (a restored list
|
|
@@ -302,7 +319,7 @@ function withChips(s: S): S {
|
|
|
302
319
|
// rows of the whole list (no category, no query), or the stand-in would be one category.
|
|
303
320
|
const whole = filter === "all" && !(typeof collection?.query === "string" && collection.query.trim());
|
|
304
321
|
// Not while a load is out: the rows on screen may still be a previous search's.
|
|
305
|
-
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map(withRowDefaults));
|
|
322
|
+
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map((row) => withRowDefaults(row)));
|
|
306
323
|
const chips = categoryChips(filter);
|
|
307
324
|
return { ...s, categoryChips: chips, hideCategories: collection?.hideChrome === true || (chips.length < 3 && !filter.startsWith("value:")) };
|
|
308
325
|
}
|
|
@@ -345,6 +362,11 @@ function categoryChips(filter: string): CategoryChip[] {
|
|
|
345
362
|
|
|
346
363
|
const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
347
364
|
restorePagesOnLoad: true,
|
|
365
|
+
onLoad() {
|
|
366
|
+
// Copy is stashed before this hook. Repaint restored labels immediately, even
|
|
367
|
+
// when the background query holds the old page through a slow or failed read.
|
|
368
|
+
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
369
|
+
},
|
|
348
370
|
state: { status: "loading", query: "", all: [], results: [], emptyText: "", countLabel: "", errored: false,
|
|
349
371
|
notErrored: true, isLoading: true, notLoading: false, hasResults: false, noEmpty: true,
|
|
350
372
|
categoryChips: [], hideCategories: true, hideRows: false, noMatchText: "", hasQuery: false, noQuery: true, categoriesLabel: "" },
|
|
@@ -377,11 +399,10 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
377
399
|
},
|
|
378
400
|
actions: {
|
|
379
401
|
categoriesLanded: (s) => withChips(s),
|
|
380
|
-
//
|
|
381
|
-
// `noBook`
|
|
402
|
+
// Normalize old snapshots and refresh Ended/copy before revalidation settles.
|
|
403
|
+
// A missing `noBook` must never show Book on a restored course.
|
|
382
404
|
normalizeRestored: (s) => {
|
|
383
405
|
const rows = Array.isArray(s.results) ? s.results : [];
|
|
384
|
-
if (rows.every((row) => typeof (row as Partial<Row>).noBook === "boolean")) return s;
|
|
385
406
|
const fixed = rows.map((row) => withRowDefaults(row as Partial<Row>));
|
|
386
407
|
return { ...s, results: fixed, all: fixed };
|
|
387
408
|
},
|
|
@@ -389,7 +410,11 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
389
410
|
apply(s, page) {
|
|
390
411
|
const loading = page.status === "loading";
|
|
391
412
|
const errored = page.status === "error";
|
|
392
|
-
|
|
413
|
+
// A live row is built before a cold boot's copy pack arrives: the first live page keeps the
|
|
414
|
+
// Ended label the restored row for the same course showed, until a current pack replaces it.
|
|
415
|
+
const shown = new Map((restoredEndedBridge && Array.isArray(s.results) ? s.results : []).map((row) => [row.id, row.endedText]));
|
|
416
|
+
const rows = page.items.map((item) => withRowDefaults(item, shown.get(item.id)));
|
|
417
|
+
if (page.metadata?.fromCache === false && !loading && !errored) restoredEndedBridge = false;
|
|
393
418
|
// The stand-in categories come from the accepted rows of the whole list (a superseded
|
|
394
419
|
// response never reaches apply): a live page replaces them, the device's copy only fills a gap.
|
|
395
420
|
if (!page.criteria.query && page.criteria.filter === "all" && rows.length > 0) {
|
|
@@ -427,7 +452,6 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
427
452
|
// (there is no Refine now): both go back to the business's own view before the first read.
|
|
428
453
|
type Retired = { getState?: () => { collection?: { filter?: unknown; sort?: unknown } };
|
|
429
454
|
collectionFilter?: (arg: unknown) => unknown; collectionSort?: (arg: unknown) => unknown };
|
|
430
|
-
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
431
455
|
const retired = svc as unknown as Retired;
|
|
432
456
|
const persisted = retired.getState?.()?.collection;
|
|
433
457
|
if (persisted?.filter === "saved") void retired.collectionFilter?.({ selectedIds: ["all"] });
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -22326,6 +22326,20 @@ const SCREENS = runtimeSetting({
|
|
|
22326
22326
|
"hiddenPath": "$item.noTypeTag"
|
|
22327
22327
|
}
|
|
22328
22328
|
},
|
|
22329
|
+
{
|
|
22330
|
+
"id": "himi_services.row.$item.id/ended",
|
|
22331
|
+
"body": {
|
|
22332
|
+
"package": "himalaya",
|
|
22333
|
+
"type": "Text",
|
|
22334
|
+
"props": {
|
|
22335
|
+
"text": "$item.endedText",
|
|
22336
|
+
"style": "caption",
|
|
22337
|
+
"color": "textSecondary",
|
|
22338
|
+
"maxLines": 1,
|
|
22339
|
+
"collapseWhenEmpty": true
|
|
22340
|
+
}
|
|
22341
|
+
}
|
|
22342
|
+
},
|
|
22329
22343
|
{
|
|
22330
22344
|
"id": "",
|
|
22331
22345
|
"body": {
|
|
@@ -80,6 +80,19 @@ type CoursePolicyShape = {
|
|
|
80
80
|
};
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/** A completed course, independent of whether bookings are currently allowed. Missing,
|
|
84
|
+
* invalid or reversed dates are unknown, never evidence that a course has ended. */
|
|
85
|
+
export function courseHasEnded(service: CoursePolicyShape, now: number): boolean {
|
|
86
|
+
const start = service.schedule?.firstSessionStart ?? "";
|
|
87
|
+
const end = service.schedule?.lastSessionEnd ?? "";
|
|
88
|
+
// The schedule has no zone beside these fields. A local wall time must not be interpreted
|
|
89
|
+
// in the device's zone and presented as a confirmed business end instant.
|
|
90
|
+
if (!HAS_OFFSET.test(start) || !HAS_OFFSET.test(end)) return false;
|
|
91
|
+
const first = validDate(start)?.getTime() ?? NaN;
|
|
92
|
+
const last = validDate(end)?.getTime() ?? NaN;
|
|
93
|
+
return Number.isFinite(now) && Number.isFinite(first) && Number.isFinite(last) && first <= last && last <= now;
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
/** Whether the business takes course bookings at `now` (ms): the course has not ended, has not
|
|
84
97
|
* started (unless the policy lets people join late), and `now` is inside its booking window —
|
|
85
98
|
* which the early and late limits count from the first session. */
|
|
@@ -238,6 +251,14 @@ export function serviceDurationText(service: PricedServiceShape, label: BookingL
|
|
|
238
251
|
|
|
239
252
|
function validDate(value: string | undefined): Date | undefined {
|
|
240
253
|
if (!value) return undefined;
|
|
254
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/i.exec(value);
|
|
255
|
+
if (!parts) return undefined;
|
|
256
|
+
const year = Number(parts[1]), month = Number(parts[2]) - 1, day = Number(parts[3]);
|
|
257
|
+
// Date accepts February 30 by rolling into March. Check the stated calendar day first;
|
|
258
|
+
// setUTCFullYear also preserves years 0–99, which Date.UTC would reinterpret as 1900–1999.
|
|
259
|
+
const calendar = new Date(0);
|
|
260
|
+
calendar.setUTCFullYear(year, month, day);
|
|
261
|
+
if (calendar.getUTCFullYear() !== year || calendar.getUTCMonth() !== month || calendar.getUTCDate() !== day) return undefined;
|
|
241
262
|
const date = new Date(value);
|
|
242
263
|
return Number.isFinite(date.getTime()) ? date : undefined;
|
|
243
264
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
11
11
|
import { COLLECTION_CHROME_MIN_ITEMS } from "../_shared/collection-contract.js";
|
|
12
12
|
import { queryServiceCategories, queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
13
|
-
import { categoryName, courseDatesText, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
13
|
+
import { categoryName, courseDatesText, courseHasEnded, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
14
14
|
import { formatServicePrice } from "../_shared/wix-service-price.js";
|
|
15
15
|
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
16
16
|
import { initials } from "../_shared/himi-text.js";
|
|
@@ -60,6 +60,9 @@ interface Row {
|
|
|
60
60
|
/** "Class" or "Course"; "" for an appointment. */
|
|
61
61
|
typeTag: string;
|
|
62
62
|
noTypeTag: boolean;
|
|
63
|
+
endedText: string;
|
|
64
|
+
/** Retain course dates so a cached catalog can refresh its status after the course ends. */
|
|
65
|
+
courseSchedule?: { firstSessionStart?: string; lastSessionEnd?: string };
|
|
63
66
|
/** "1 hr · ₪70"; a course: "Nov 3 – Dec 15, 2026 · ₪1,200". "" when the record has neither. */
|
|
64
67
|
meta: string;
|
|
65
68
|
/** Online booking by time is possible: online booking is on and the service is not a course. */
|
|
@@ -186,8 +189,8 @@ function typeTagOf(type: unknown): string {
|
|
|
186
189
|
return "";
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta">): string {
|
|
190
|
-
return [row.name, row.typeTag, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
192
|
+
function spokenName(row: Pick<Row, "name" | "typeTag" | "tagline" | "meta" | "endedText">): string {
|
|
193
|
+
return [row.name, row.typeTag, row.endedText, row.tagline, row.meta].filter(Boolean).join(", ");
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
function bookCtaFor(request: boolean): string {
|
|
@@ -212,6 +215,11 @@ function norm(service: WixService): Row {
|
|
|
212
215
|
].filter(Boolean);
|
|
213
216
|
const image = imageUrl(service);
|
|
214
217
|
const isCourse = service.type === "COURSE";
|
|
218
|
+
const courseSchedule = isCourse ? {
|
|
219
|
+
firstSessionStart: service.schedule?.firstSessionStart,
|
|
220
|
+
lastSessionEnd: service.schedule?.lastSessionEnd,
|
|
221
|
+
} : undefined;
|
|
222
|
+
const endedText = isCourse && courseHasEnded(service, Date.now()) ? copyParam("services.ended", "Ended") : "";
|
|
215
223
|
const price = servicePriceLine(service, listLabel).text;
|
|
216
224
|
const meta = [isCourse ? courseDatesText(service) : serviceDurationText(service, listLabel), price].filter(Boolean).join(" · ");
|
|
217
225
|
const typeTag = typeTagOf(service.type);
|
|
@@ -234,18 +242,20 @@ function norm(service: WixService): Row {
|
|
|
234
242
|
tagline,
|
|
235
243
|
typeTag,
|
|
236
244
|
noTypeTag: !typeTag,
|
|
245
|
+
endedText,
|
|
246
|
+
courseSchedule,
|
|
237
247
|
meta,
|
|
238
248
|
canBook,
|
|
239
249
|
noBook: !canBook,
|
|
240
250
|
bookCta: bookCtaFor(request),
|
|
241
251
|
bookA11y: bookLabel(name, request),
|
|
242
|
-
a11y: spokenName({ name, typeTag, tagline, meta }),
|
|
252
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
243
253
|
};
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
/** A row saved by an older worker (offline cache, restored state) has none of the new
|
|
247
257
|
* fields: it renders with no tagline, meta or tag, and no Book pill until the live read. */
|
|
248
|
-
function withRowDefaults(row: Partial<Row
|
|
258
|
+
function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
|
|
249
259
|
const name = typeof row.name === "string" && row.name ? row.name : "Service";
|
|
250
260
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
251
261
|
const image = str(row.image);
|
|
@@ -253,6 +263,9 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
253
263
|
const tagline = str(row.tagline);
|
|
254
264
|
const meta = str(row.meta);
|
|
255
265
|
const canBook = row.canBook === true;
|
|
266
|
+
const courseSchedule = row.courseSchedule;
|
|
267
|
+
const endedText = courseSchedule && courseHasEnded({ schedule: courseSchedule }, Date.now())
|
|
268
|
+
? copyParam("services.ended", "Ended", shownEnded || str(row.endedText) || "Ended") : "";
|
|
256
269
|
return {
|
|
257
270
|
...row,
|
|
258
271
|
id: str(row.id),
|
|
@@ -268,12 +281,13 @@ function withRowDefaults(row: Partial<Row>): Row {
|
|
|
268
281
|
tagline,
|
|
269
282
|
typeTag,
|
|
270
283
|
noTypeTag: !typeTag,
|
|
284
|
+
endedText,
|
|
271
285
|
meta,
|
|
272
286
|
canBook,
|
|
273
287
|
noBook: !canBook,
|
|
274
288
|
bookCta: str(row.bookCta) || bookCtaFor(false),
|
|
275
289
|
bookA11y: str(row.bookA11y) || bookLabel(name),
|
|
276
|
-
a11y:
|
|
290
|
+
a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
|
|
277
291
|
};
|
|
278
292
|
}
|
|
279
293
|
|
|
@@ -287,6 +301,9 @@ let categoriesInFlight = false;
|
|
|
287
301
|
let categoriesFailedAt = 0;
|
|
288
302
|
const CATEGORY_RETRY_MS = 60_000;
|
|
289
303
|
const rowCategories = new Map<string, number>();
|
|
304
|
+
/** A restored Ended label bridges only the first live page after boot, because an app that sends no
|
|
305
|
+
* copy pack gives no sign that none is coming: after it, current copy (pack or stock) decides. */
|
|
306
|
+
let restoredEndedBridge = true;
|
|
290
307
|
|
|
291
308
|
/** Repaint the chips only, on the state already shown. Re-deriving from the query controller
|
|
292
309
|
* would publish its working state, which a revalidation deliberately holds back (a restored list
|
|
@@ -302,7 +319,7 @@ function withChips(s: S): S {
|
|
|
302
319
|
// rows of the whole list (no category, no query), or the stand-in would be one category.
|
|
303
320
|
const whole = filter === "all" && !(typeof collection?.query === "string" && collection.query.trim());
|
|
304
321
|
// Not while a load is out: the rows on screen may still be a previous search's.
|
|
305
|
-
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map(withRowDefaults));
|
|
322
|
+
if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map((row) => withRowDefaults(row)));
|
|
306
323
|
const chips = categoryChips(filter);
|
|
307
324
|
return { ...s, categoryChips: chips, hideCategories: collection?.hideChrome === true || (chips.length < 3 && !filter.startsWith("value:")) };
|
|
308
325
|
}
|
|
@@ -345,6 +362,11 @@ function categoryChips(filter: string): CategoryChip[] {
|
|
|
345
362
|
|
|
346
363
|
const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
347
364
|
restorePagesOnLoad: true,
|
|
365
|
+
onLoad() {
|
|
366
|
+
// Copy is stashed before this hook. Repaint restored labels immediately, even
|
|
367
|
+
// when the background query holds the old page through a slow or failed read.
|
|
368
|
+
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
369
|
+
},
|
|
348
370
|
state: { status: "loading", query: "", all: [], results: [], emptyText: "", countLabel: "", errored: false,
|
|
349
371
|
notErrored: true, isLoading: true, notLoading: false, hasResults: false, noEmpty: true,
|
|
350
372
|
categoryChips: [], hideCategories: true, hideRows: false, noMatchText: "", hasQuery: false, noQuery: true, categoriesLabel: "" },
|
|
@@ -377,11 +399,10 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
377
399
|
},
|
|
378
400
|
actions: {
|
|
379
401
|
categoriesLanded: (s) => withChips(s),
|
|
380
|
-
//
|
|
381
|
-
// `noBook`
|
|
402
|
+
// Normalize old snapshots and refresh Ended/copy before revalidation settles.
|
|
403
|
+
// A missing `noBook` must never show Book on a restored course.
|
|
382
404
|
normalizeRestored: (s) => {
|
|
383
405
|
const rows = Array.isArray(s.results) ? s.results : [];
|
|
384
|
-
if (rows.every((row) => typeof (row as Partial<Row>).noBook === "boolean")) return s;
|
|
385
406
|
const fixed = rows.map((row) => withRowDefaults(row as Partial<Row>));
|
|
386
407
|
return { ...s, results: fixed, all: fixed };
|
|
387
408
|
},
|
|
@@ -389,7 +410,11 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
389
410
|
apply(s, page) {
|
|
390
411
|
const loading = page.status === "loading";
|
|
391
412
|
const errored = page.status === "error";
|
|
392
|
-
|
|
413
|
+
// A live row is built before a cold boot's copy pack arrives: the first live page keeps the
|
|
414
|
+
// Ended label the restored row for the same course showed, until a current pack replaces it.
|
|
415
|
+
const shown = new Map((restoredEndedBridge && Array.isArray(s.results) ? s.results : []).map((row) => [row.id, row.endedText]));
|
|
416
|
+
const rows = page.items.map((item) => withRowDefaults(item, shown.get(item.id)));
|
|
417
|
+
if (page.metadata?.fromCache === false && !loading && !errored) restoredEndedBridge = false;
|
|
393
418
|
// The stand-in categories come from the accepted rows of the whole list (a superseded
|
|
394
419
|
// response never reaches apply): a live page replaces them, the device's copy only fills a gap.
|
|
395
420
|
if (!page.criteria.query && page.criteria.filter === "all" && rows.length > 0) {
|
|
@@ -427,7 +452,6 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
|
|
|
427
452
|
// (there is no Refine now): both go back to the business's own view before the first read.
|
|
428
453
|
type Retired = { getState?: () => { collection?: { filter?: unknown; sort?: unknown } };
|
|
429
454
|
collectionFilter?: (arg: unknown) => unknown; collectionSort?: (arg: unknown) => unknown };
|
|
430
|
-
(svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
|
|
431
455
|
const retired = svc as unknown as Retired;
|
|
432
456
|
const persisted = retired.getState?.()?.collection;
|
|
433
457
|
if (persisted?.filter === "saved") void retired.collectionFilter?.({ selectedIds: ["all"] });
|
|
@@ -193,9 +193,47 @@ const PRICING_NOT_HYDRATED = ["rows", "collection", "hidePriceRetry"] as const;
|
|
|
193
193
|
let content: { status: State["status"]; programs: ProgramContent[] } = { status: "loading", programs: [] };
|
|
194
194
|
let scopeIncomplete = false;
|
|
195
195
|
let loadSeq = 0;
|
|
196
|
+
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
197
|
+
function sectionOrder(value: unknown): string[] {
|
|
198
|
+
try { if (typeof value === "string") value = JSON.parse(value); } catch { return []; }
|
|
199
|
+
const settings = object(object(value).settings);
|
|
200
|
+
const sorting = object(settings.sortingSettings);
|
|
201
|
+
return sorting.ordering === "MANUAL" && Array.isArray(sorting.challengesOrder)
|
|
202
|
+
? sorting.challengesOrder.filter((id): id is string => typeof id === "string" && !!id)
|
|
203
|
+
: [];
|
|
204
|
+
}
|
|
205
|
+
const routeInput = () => (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
|
|
206
|
+
/** The whole route input identifies the route, so a change to any field ends the previous section's order. */
|
|
207
|
+
const inputKey = () => { try { return JSON.stringify(routeInput() ?? {}); } catch { return ""; } };
|
|
208
|
+
let lastInputKey = inputKey();
|
|
209
|
+
let manualOrder = sectionOrder(routeInput()?.programsSection);
|
|
210
|
+
|
|
211
|
+
/** Source ordering promotes configured IDs; it never selects a subset of the catalog. */
|
|
212
|
+
function orderRows(rows: ProgramRow[]): ProgramRow[] {
|
|
213
|
+
if (!manualOrder.length) return rows;
|
|
214
|
+
const remaining = new Map(rows.map(row => [row.id, row]));
|
|
215
|
+
const ordered: ProgramRow[] = [];
|
|
216
|
+
for (const id of manualOrder) {
|
|
217
|
+
const row = remaining.get(id);
|
|
218
|
+
if (row) { ordered.push(row); remaining.delete(id); }
|
|
219
|
+
}
|
|
220
|
+
return [...ordered, ...remaining.values()];
|
|
221
|
+
}
|
|
196
222
|
|
|
197
223
|
async function load(_state: State, arg?: unknown): Promise<undefined> {
|
|
198
224
|
stashCopy(arg);
|
|
225
|
+
const params = object(arg);
|
|
226
|
+
const hasSection = Object.prototype.hasOwnProperty.call(params, "programsSection");
|
|
227
|
+
const nextInputKey = inputKey();
|
|
228
|
+
const inputChanged = nextInputKey !== lastInputKey;
|
|
229
|
+
if (inputChanged) {
|
|
230
|
+
lastInputKey = nextInputKey;
|
|
231
|
+
manualOrder = sectionOrder(routeInput()?.programsSection);
|
|
232
|
+
}
|
|
233
|
+
if (hasSection) {
|
|
234
|
+
manualOrder = sectionOrder(params.programsSection);
|
|
235
|
+
}
|
|
236
|
+
if (inputChanged || hasSection) void svc.pricingPublish();
|
|
199
237
|
const seq = ++loadSeq;
|
|
200
238
|
const programs = await fetchPrograms(() => seq === loadSeq).catch(() => undefined);
|
|
201
239
|
if (seq !== loadSeq) return undefined;
|
|
@@ -220,8 +258,8 @@ function pricedState(state: State): State {
|
|
|
220
258
|
const item = onlineProgramItem({ id: program.wixId });
|
|
221
259
|
return item ? pricing.view(item.reference) : undefined;
|
|
222
260
|
});
|
|
223
|
-
const rows = content.programs.map(({ wixId: _wixId, ...row }, i) =>
|
|
224
|
-
({ ...row, priceLine: views[i] ? iapPriceFields(views[i]!, copy).priceText : "" }));
|
|
261
|
+
const rows = orderRows(content.programs.map(({ wixId: _wixId, ...row }, i) =>
|
|
262
|
+
({ ...row, priceLine: views[i] ? iapPriceFields(views[i]!, copy).priceText : "" })));
|
|
225
263
|
const shown = content.status === "ready" ? ready(rows) : content.status === "error" ? errored({ ...state, rows }) : INITIAL_STATE;
|
|
226
264
|
return {
|
|
227
265
|
...state, ...shown, programs: content.programs,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
22
22
|
|
|
23
23
|
let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
|
|
24
|
+
let copyReceived = false;
|
|
24
25
|
|
|
25
26
|
/** Absorb the `copy` onAppear param (customer strings or stock translation records)
|
|
26
27
|
* from an action arg. Call first in every action that can receive onAppear
|
|
@@ -44,15 +45,19 @@ export function stashCopy(arg: unknown): void {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
stashedCopy = next;
|
|
48
|
+
copyReceived = true;
|
|
47
49
|
} catch {
|
|
48
50
|
// Malformed pack param — keep the stock copy rather than dying mid-action.
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** The provisioned override for `key`, or `fallback` (the stock copy)
|
|
53
|
-
*
|
|
54
|
-
export function copyParam(key: string, fallback: string): string {
|
|
54
|
+
/** The provisioned override for `key`, or `fallback` (the stock copy). An optional
|
|
55
|
+
* restored label survives only until a current copy pack arrives. */
|
|
56
|
+
export function copyParam(key: string, fallback: string, restoredFallback = fallback): string {
|
|
55
57
|
const value = stashedCopy[key];
|
|
58
|
+
// A disk-restored row may paint before onAppear supplies its copy pack. Keep its
|
|
59
|
+
// last label only before a pack arrives; even an empty current pack selects stock copy.
|
|
60
|
+
if (value === undefined) return copyReceived ? fallback : restoredFallback;
|
|
56
61
|
if (typeof value === "string") return value !== "" ? value : fallback;
|
|
57
62
|
return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
|
|
58
63
|
}
|