@wix/himalaya-cli 0.1210.0 → 0.1212.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/cli.mjs +91 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/beauty-salon-owner/dev/index.ts +280 -37
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club-owner/dev/index.ts +389 -37
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/events-meetups-owner/dev/index.ts +280 -37
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/gym-fitness-owner/dev/index.ts +389 -37
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/nonprofit-cause-owner/dev/index.ts +280 -37
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/owner-lite/dev/index.ts +389 -37
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq/index.ts +8 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/podcast-creator-owner/dev/index.ts +389 -37
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/portfolio-creative-owner/dev/index.ts +280 -37
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/restaurant-owner/dev/index.ts +389 -37
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/retail-store-owner/dev/index.ts +280 -37
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/yoga-studio-owner/dev/index.ts +280 -37
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
|
@@ -39,6 +39,8 @@ interface State {
|
|
|
39
39
|
noEmpty: boolean;
|
|
40
40
|
hasRecents: boolean;
|
|
41
41
|
noRecents: boolean;
|
|
42
|
+
/** True while `sections` is empty — gates the list so an empty one costs no gap. */
|
|
43
|
+
noSections: boolean;
|
|
42
44
|
isLoading: boolean;
|
|
43
45
|
notLoading: boolean;
|
|
44
46
|
summaryNote: string;
|
|
@@ -200,7 +202,7 @@ function derive(state: State, areas = state.areas): State {
|
|
|
200
202
|
// areas arrive — or when this app provisions none at all — there is nothing a
|
|
201
203
|
// filter could have excluded, so "Clear filters" would be a lie.
|
|
202
204
|
const noEmpty = next.noEmpty || state.status !== 'ready' || areas.length === 0;
|
|
203
|
-
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0, businessName: currentBusinessName };
|
|
205
|
+
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0, noSections: next.sections.length === 0, businessName: currentBusinessName };
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
// Loads are not serialized: on a warm reappearance the host's bare refresh and
|
|
@@ -276,7 +278,7 @@ const svc = defineService<State, {
|
|
|
276
278
|
// 'All' is always a valid filter, so the chip row paints from the first frame
|
|
277
279
|
// instead of appearing (and shifting the layout) when the areas arrive.
|
|
278
280
|
groups: [{ id: 'all', label: allLabel() }], summary: '', noEmpty: true,
|
|
279
|
-
hasRecents: false, noRecents: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
281
|
+
hasRecents: false, noRecents: true, noSections: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
280
282
|
},
|
|
281
283
|
actions: {
|
|
282
284
|
load: async (state, arg) => {
|
|
@@ -80,6 +80,9 @@ interface PostState {
|
|
|
80
80
|
/** Non-empty when showing owner-preview-demo sample data (web preview, no owner bridge). */
|
|
81
81
|
demoNote: string;
|
|
82
82
|
notDemo: boolean;
|
|
83
|
+
/** #3158 — Delete ARMS a confirmation; only confirmDelete reaches the API. */
|
|
84
|
+
deletePending: boolean;
|
|
85
|
+
noDeleteConfirmation: boolean;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -159,6 +162,9 @@ function shape(s: Partial<PostState>): PostState {
|
|
|
159
162
|
hideUnpublish: !published || status !== "ready",
|
|
160
163
|
demoNote: s.demoNote ?? "",
|
|
161
164
|
notDemo: !s.demoNote,
|
|
165
|
+
// Only a loaded entity can be pending deletion; every other action disarms it (`disarm`).
|
|
166
|
+
deletePending: !!s.deletePending && hasEntity,
|
|
167
|
+
noDeleteConfirmation: !(s.deletePending && hasEntity),
|
|
162
168
|
};
|
|
163
169
|
}
|
|
164
170
|
|
|
@@ -276,18 +282,35 @@ async function refresh(prev: PostState, id: string, actionMessage = ""): Promise
|
|
|
276
282
|
});
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
/** #3158 — a confirmed delete in flight. Synchronous and module-level on purpose: two
|
|
286
|
+
* confirmDelete dispatches (a double tap) both start from a state that says "armed", and only a
|
|
287
|
+
* flag set BEFORE the first await stops the second from issuing its own DELETE — whose late 404
|
|
288
|
+
* would otherwise land after, and overwrite, the first one's "deleted". */
|
|
289
|
+
let deleteInFlight = false;
|
|
290
|
+
|
|
291
|
+
/** #3158 — the confirmation describes THIS visit. Never take it from a persisted snapshot, or a
|
|
292
|
+
* reopened record comes back with "Delete this …?" already armed. */
|
|
293
|
+
const DELETE_RUN_KEYS = ["deletePending", "noDeleteConfirmation"] as const;
|
|
294
|
+
|
|
295
|
+
/** #3158 — clear an armed delete confirmation. */
|
|
296
|
+
const disarm = <T extends { deletePending: boolean }>(s: T): T => ({ ...s, deletePending: false });
|
|
297
|
+
|
|
279
298
|
const svc = defineService<PostState, {
|
|
280
299
|
load: (s: PostState, arg?: unknown) => Promise<PostState>;
|
|
281
300
|
retry: (s: PostState) => Promise<PostState>;
|
|
282
301
|
publish: (s: PostState) => Promise<PostState>;
|
|
283
302
|
unpublish: (s: PostState) => Promise<PostState>;
|
|
284
303
|
delete: (s: PostState) => Promise<PostState>;
|
|
304
|
+
cancelDelete: (s: PostState) => Promise<PostState>;
|
|
305
|
+
confirmDelete: (s: PostState) => Promise<PostState | undefined>;
|
|
285
306
|
/** [rico] #3370 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
286
307
|
openLink: (s: PostState, arg?: unknown) => Promise<PostState>;
|
|
287
308
|
}>("himiOwnerLitePost", {
|
|
288
309
|
state: shape({ status: "loading" }),
|
|
310
|
+
notHydrated: [...DELETE_RUN_KEYS],
|
|
289
311
|
actions: {
|
|
290
|
-
load: async (
|
|
312
|
+
load: async (s0, arg) => {
|
|
313
|
+
const s = disarm(s0);
|
|
291
314
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
292
315
|
const id = idFrom(arg, s.id) || inputId();
|
|
293
316
|
if (!id) return shape({ ...s, status: "error", error: copyParam("ownerPost.errorNoId", "no post id") });
|
|
@@ -312,7 +335,8 @@ const svc = defineService<PostState, {
|
|
|
312
335
|
return shape({ ...s, id, status: "error", error: ownerLoadError(e) });
|
|
313
336
|
}
|
|
314
337
|
},
|
|
315
|
-
retry: async (
|
|
338
|
+
retry: async (s0) => {
|
|
339
|
+
const s = disarm(s0);
|
|
316
340
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerPost.errorNoId", "no post id") });
|
|
317
341
|
if (isOwnerPreviewDemo()) {
|
|
318
342
|
const demo = demoDetail(s, s.id);
|
|
@@ -334,7 +358,8 @@ const svc = defineService<PostState, {
|
|
|
334
358
|
return shape({ ...s, status: "error", error: ownerLoadError(e) });
|
|
335
359
|
}
|
|
336
360
|
},
|
|
337
|
-
publish: async (
|
|
361
|
+
publish: async (s0) => {
|
|
362
|
+
const s = disarm(s0);
|
|
338
363
|
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
339
364
|
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
340
365
|
if (s.postStatusRaw === "PUBLISHED") return shape({ ...s, actionMessage: copyParam("ownerPost.alreadyPublished", "Post is already published.") });
|
|
@@ -352,7 +377,8 @@ const svc = defineService<PostState, {
|
|
|
352
377
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
353
378
|
}
|
|
354
379
|
},
|
|
355
|
-
unpublish: async (
|
|
380
|
+
unpublish: async (s0) => {
|
|
381
|
+
const s = disarm(s0);
|
|
356
382
|
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
357
383
|
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
358
384
|
if (s.postStatusRaw !== "PUBLISHED" && s.postStatusRaw !== "SCHEDULED") {
|
|
@@ -372,20 +398,35 @@ const svc = defineService<PostState, {
|
|
|
372
398
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
373
399
|
}
|
|
374
400
|
},
|
|
375
|
-
delete
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
401
|
+
// #3158 — destructive, so two steps. `delete` (the Manage card's Delete button)
|
|
402
|
+
// only ARMS the inline confirmation, which names the record and says it cannot
|
|
403
|
+
// be undone; `cancelDelete` disarms it; only `confirmDelete` deletes. Anything
|
|
404
|
+
// that dispatches `delete` directly therefore still asks first.
|
|
405
|
+
delete: async (s) => shape({ ...s, deletePending: true, actionMessage: "" }),
|
|
406
|
+
cancelDelete: async (s) => shape({ ...s, deletePending: false }),
|
|
407
|
+
confirmDelete: async (s0) => {
|
|
408
|
+
// Nothing armed (a stray dispatch, a rehydrated snapshot) or a delete already running
|
|
409
|
+
// (a second tap): ignore it — `undefined` leaves the state exactly as it is.
|
|
410
|
+
if (!s0.deletePending || deleteInFlight) return undefined;
|
|
411
|
+
deleteInFlight = true;
|
|
380
412
|
try {
|
|
381
|
-
|
|
382
|
-
return shape({ ...s,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
const s = disarm(s0);
|
|
414
|
+
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
415
|
+
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
416
|
+
// Optimistic local delete — the list drops the row immediately.
|
|
417
|
+
await cacheDrop(s.id);
|
|
418
|
+
try {
|
|
419
|
+
await deleteDraftPost(s.id);
|
|
420
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerPost.deletedEmpty", "Post deleted."), actionMessage: "" });
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (shouldQueueOwnerWrite(e)) {
|
|
423
|
+
await queueWrite("tier5.himiOwnerLitePost.delete", { id: s.id });
|
|
424
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerPost.deletedEmpty", "Post deleted."), actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
425
|
+
}
|
|
426
|
+
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
387
427
|
}
|
|
388
|
-
|
|
428
|
+
} finally {
|
|
429
|
+
deleteInFlight = false;
|
|
389
430
|
}
|
|
390
431
|
},
|
|
391
432
|
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and
|
|
@@ -397,4 +438,10 @@ const svc = defineService<PostState, {
|
|
|
397
438
|
},
|
|
398
439
|
});
|
|
399
440
|
|
|
441
|
+
// The same reset on a runtime from before `notHydrated` (an older binary on a new bundle).
|
|
442
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
443
|
+
const booted = svc.getState?.() as unknown as Record<string, unknown> | undefined;
|
|
444
|
+
if (booted && typeof booted === "object") Object.assign(booted, { deletePending: false, noDeleteConfirmation: true });
|
|
445
|
+
}
|
|
446
|
+
|
|
400
447
|
export { svc };
|
|
@@ -59,6 +59,9 @@ interface ProductState {
|
|
|
59
59
|
/** Non-empty when showing owner-preview-demo sample data (web preview, no owner bridge). */
|
|
60
60
|
demoNote: string;
|
|
61
61
|
notDemo: boolean;
|
|
62
|
+
/** #3158 — Delete ARMS a confirmation; only confirmDelete reaches the API. */
|
|
63
|
+
deletePending: boolean;
|
|
64
|
+
noDeleteConfirmation: boolean;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -143,6 +146,9 @@ function shape(s: Partial<ProductState>): ProductState {
|
|
|
143
146
|
hideHide: !visible || status !== "ready",
|
|
144
147
|
demoNote: s.demoNote ?? "",
|
|
145
148
|
notDemo: !s.demoNote,
|
|
149
|
+
// Only a loaded entity can be pending deletion; every other action disarms it (`disarm`).
|
|
150
|
+
deletePending: !!s.deletePending && hasEntity,
|
|
151
|
+
noDeleteConfirmation: !(s.deletePending && hasEntity),
|
|
146
152
|
};
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -265,16 +271,33 @@ async function setVisibility(s: ProductState, visible: boolean, msg: string): Pr
|
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
|
|
274
|
+
/** #3158 — a confirmed delete in flight. Synchronous and module-level on purpose: two
|
|
275
|
+
* confirmDelete dispatches (a double tap) both start from a state that says "armed", and only a
|
|
276
|
+
* flag set BEFORE the first await stops the second from issuing its own DELETE — whose late 404
|
|
277
|
+
* would otherwise land after, and overwrite, the first one's "deleted". */
|
|
278
|
+
let deleteInFlight = false;
|
|
279
|
+
|
|
280
|
+
/** #3158 — the confirmation describes THIS visit. Never take it from a persisted snapshot, or a
|
|
281
|
+
* reopened record comes back with "Delete this …?" already armed. */
|
|
282
|
+
const DELETE_RUN_KEYS = ["deletePending", "noDeleteConfirmation"] as const;
|
|
283
|
+
|
|
284
|
+
/** #3158 — clear an armed delete confirmation. */
|
|
285
|
+
const disarm = <T extends { deletePending: boolean }>(s: T): T => ({ ...s, deletePending: false });
|
|
286
|
+
|
|
268
287
|
const svc = defineService<ProductState, {
|
|
269
288
|
load: (s: ProductState, arg?: unknown) => Promise<ProductState>;
|
|
270
289
|
retry: (s: ProductState) => Promise<ProductState>;
|
|
271
290
|
show: (s: ProductState) => Promise<ProductState>;
|
|
272
291
|
hide: (s: ProductState) => Promise<ProductState>;
|
|
273
292
|
delete: (s: ProductState) => Promise<ProductState>;
|
|
293
|
+
cancelDelete: (s: ProductState) => Promise<ProductState>;
|
|
294
|
+
confirmDelete: (s: ProductState) => Promise<ProductState | undefined>;
|
|
274
295
|
}>("himiOwnerLiteProduct", {
|
|
275
296
|
state: shape({ status: "loading" }),
|
|
297
|
+
notHydrated: [...DELETE_RUN_KEYS],
|
|
276
298
|
actions: {
|
|
277
|
-
load: async (
|
|
299
|
+
load: async (s0, arg) => {
|
|
300
|
+
const s = disarm(s0);
|
|
278
301
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
279
302
|
const id = idFrom(arg, s.id) || inputId();
|
|
280
303
|
if (!id) return shape({ ...s, status: "error", error: copyParam("ownerProduct.errorNoId", "no product id") });
|
|
@@ -297,7 +320,8 @@ const svc = defineService<ProductState, {
|
|
|
297
320
|
return shape({ ...s, id, status: "error", error: ownerLoadError(e) });
|
|
298
321
|
}
|
|
299
322
|
},
|
|
300
|
-
retry: async (
|
|
323
|
+
retry: async (s0) => {
|
|
324
|
+
const s = disarm(s0);
|
|
301
325
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerProduct.errorNoId", "no product id") });
|
|
302
326
|
if (isOwnerPreviewDemo()) {
|
|
303
327
|
const demo = demoDetail(s, s.id);
|
|
@@ -317,25 +341,46 @@ const svc = defineService<ProductState, {
|
|
|
317
341
|
return shape({ ...s, status: "error", error: ownerLoadError(e) });
|
|
318
342
|
}
|
|
319
343
|
},
|
|
320
|
-
show: async (
|
|
321
|
-
hide: async (
|
|
322
|
-
delete
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
344
|
+
show: async (s0) => { const s = disarm(s0); return setVisibility(s, true, copyParam("ownerProduct.shownMessage", "Product is now visible in your store.")); },
|
|
345
|
+
hide: async (s0) => { const s = disarm(s0); return setVisibility(s, false, copyParam("ownerProduct.hiddenMessage", "Product hidden from your store.")); },
|
|
346
|
+
// #3158 — destructive, so two steps. `delete` (the Manage card's Delete button)
|
|
347
|
+
// only ARMS the inline confirmation, which names the record and says it cannot
|
|
348
|
+
// be undone; `cancelDelete` disarms it; only `confirmDelete` deletes. Anything
|
|
349
|
+
// that dispatches `delete` directly therefore still asks first.
|
|
350
|
+
delete: async (s) => shape({ ...s, deletePending: true, actionMessage: "" }),
|
|
351
|
+
cancelDelete: async (s) => shape({ ...s, deletePending: false }),
|
|
352
|
+
confirmDelete: async (s0) => {
|
|
353
|
+
// Nothing armed (a stray dispatch, a rehydrated snapshot) or a delete already running
|
|
354
|
+
// (a second tap): ignore it — `undefined` leaves the state exactly as it is.
|
|
355
|
+
if (!s0.deletePending || deleteInFlight) return undefined;
|
|
356
|
+
deleteInFlight = true;
|
|
327
357
|
try {
|
|
328
|
-
|
|
329
|
-
return shape({ ...s,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
358
|
+
const s = disarm(s0);
|
|
359
|
+
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
360
|
+
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerProduct.noIdMessage", "No product id.") });
|
|
361
|
+
// Optimistic local delete — the list drops the row immediately.
|
|
362
|
+
await cacheDrop(s.id);
|
|
363
|
+
try {
|
|
364
|
+
await deleteProduct(s.id);
|
|
365
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerProduct.deletedEmpty", "Product deleted."), actionMessage: "" });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
if (shouldQueueOwnerWrite(e)) {
|
|
368
|
+
await queueWrite("tier5.himiOwnerLiteProduct.delete", { id: s.id });
|
|
369
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerProduct.deletedEmpty", "Product deleted."), actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
370
|
+
}
|
|
371
|
+
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
334
372
|
}
|
|
335
|
-
|
|
373
|
+
} finally {
|
|
374
|
+
deleteInFlight = false;
|
|
336
375
|
}
|
|
337
376
|
},
|
|
338
377
|
},
|
|
339
378
|
});
|
|
340
379
|
|
|
380
|
+
// The same reset on a runtime from before `notHydrated` (an older binary on a new bundle).
|
|
381
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
382
|
+
const booted = svc.getState?.() as unknown as Record<string, unknown> | undefined;
|
|
383
|
+
if (booted && typeof booted === "object") Object.assign(booted, { deletePending: false, noDeleteConfirmation: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
341
386
|
export { svc };
|
|
@@ -48,6 +48,36 @@ const MONTHS_LONG = [
|
|
|
48
48
|
"July", "August", "September", "October", "November", "December",
|
|
49
49
|
] as const;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A DateTimeFormat for `timeZone` when one is given and this engine knows it. An unknown or
|
|
53
|
+
* malformed IANA id is a RangeError in ECMA-402; these helpers never throw, so it degrades to
|
|
54
|
+
* the device zone rather than killing the screen. Without Intl there is no zone database at all,
|
|
55
|
+
* and the fallbacks below render in the device zone for the same reason.
|
|
56
|
+
*
|
|
57
|
+
* Formatters are memoised per (locale, zone, options): a management page formats several dates
|
|
58
|
+
* per row, and building a DateTimeFormat (or throwing on a bad zone) each time is not free on
|
|
59
|
+
* JSC or QuickJS. Only call it where `hasIntl()` holds — every caller does.
|
|
60
|
+
*/
|
|
61
|
+
const FORMATTERS = new Map<string, Intl.DateTimeFormat>();
|
|
62
|
+
function dateFormat(locale: string, options: Intl.DateTimeFormatOptions, timeZone?: string): Intl.DateTimeFormat {
|
|
63
|
+
const key = `${locale}|${timeZone ?? ""}|${JSON.stringify(options)}`;
|
|
64
|
+
let formatter = FORMATTERS.get(key);
|
|
65
|
+
if (!formatter) {
|
|
66
|
+
if (hasIntl()) {
|
|
67
|
+
if (timeZone) {
|
|
68
|
+
try {
|
|
69
|
+
formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone });
|
|
70
|
+
} catch {
|
|
71
|
+
/* unknown zone: device zone below, remembered under this key */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
formatter ??= new Intl.DateTimeFormat(locale, options);
|
|
75
|
+
FORMATTERS.set(key, formatter);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return formatter as Intl.DateTimeFormat;
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
// The four shapes below are not invented — they are the option bags this corpus actually uses,
|
|
52
82
|
// counted across every bundle: "Jan 5, 2026" (33 sites), "Jan 5" (11), "Mon, Jan 5, 3:07 PM" (8)
|
|
53
83
|
// and "Jan 5, 3:07 PM" (4). Naming them means a conversion is a one-line swap rather than a
|
|
@@ -55,46 +85,46 @@ const MONTHS_LONG = [
|
|
|
55
85
|
// ECMA-402 in engine-intl-fallback.test.ts.
|
|
56
86
|
|
|
57
87
|
/** `{ month: "short", day: "numeric", year: "numeric" }` — "Jan 5, 2026". */
|
|
58
|
-
export function formatDateMedium(date: Date, locale = "en-US"): string {
|
|
88
|
+
export function formatDateMedium(date: Date, locale = "en-US", timeZone?: string): string {
|
|
59
89
|
if (hasIntl()) {
|
|
60
|
-
return
|
|
90
|
+
return dateFormat(locale, { month: "short", day: "numeric", year: "numeric" }, timeZone).format(date);
|
|
61
91
|
}
|
|
62
92
|
return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
/** `{ month: "short", day: "numeric" }` — "Jan 5". */
|
|
66
|
-
export function formatMonthDay(date: Date, locale = "en-US"): string {
|
|
96
|
+
export function formatMonthDay(date: Date, locale = "en-US", timeZone?: string): string {
|
|
67
97
|
if (hasIntl()) {
|
|
68
|
-
return
|
|
98
|
+
return dateFormat(locale, { month: "short", day: "numeric" }, timeZone).format(date);
|
|
69
99
|
}
|
|
70
100
|
return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}`;
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
/** `{ month: "long", day: "numeric" }` — "January 5". */
|
|
74
|
-
export function formatMonthDayLong(date: Date, locale = "en-US"): string {
|
|
104
|
+
export function formatMonthDayLong(date: Date, locale = "en-US", timeZone?: string): string {
|
|
75
105
|
if (hasIntl()) {
|
|
76
|
-
return
|
|
106
|
+
return dateFormat(locale, { month: "long", day: "numeric" }, timeZone).format(date);
|
|
77
107
|
}
|
|
78
108
|
return `${MONTHS_LONG[date.getMonth()]} ${date.getDate()}`;
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
/** `{ weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }`
|
|
82
112
|
* — "Mon, Jan 5, 3:07 PM". */
|
|
83
|
-
export function formatWeekdayDateTime(date: Date, locale = "en-US"): string {
|
|
113
|
+
export function formatWeekdayDateTime(date: Date, locale = "en-US", timeZone?: string): string {
|
|
84
114
|
if (hasIntl()) {
|
|
85
|
-
return
|
|
115
|
+
return dateFormat(locale, {
|
|
86
116
|
weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
|
|
87
|
-
}).format(date);
|
|
117
|
+
}, timeZone).format(date);
|
|
88
118
|
}
|
|
89
119
|
return `${WEEKDAYS_SHORT[date.getDay()]}, ${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${clock12(date)}`;
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
/** `{ month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }` — "Jan 5, 3:07 PM". */
|
|
93
|
-
export function formatMonthDayTime(date: Date, locale = "en-US"): string {
|
|
123
|
+
export function formatMonthDayTime(date: Date, locale = "en-US", timeZone?: string): string {
|
|
94
124
|
if (hasIntl()) {
|
|
95
|
-
return
|
|
125
|
+
return dateFormat(locale, {
|
|
96
126
|
month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
|
|
97
|
-
}).format(date);
|
|
127
|
+
}, timeZone).format(date);
|
|
98
128
|
}
|
|
99
129
|
return `${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}, ${clock12(date)}`;
|
|
100
130
|
}
|
|
@@ -380,9 +410,9 @@ export function normalizeFormattedPrice(formatted: string): string {
|
|
|
380
410
|
return `${m[1]}${m[3]}${m[2]}`;
|
|
381
411
|
}
|
|
382
412
|
|
|
383
|
-
function clockParts(d: Date, locale: string): { time: string; meridiem: string } {
|
|
413
|
+
function clockParts(d: Date, locale: string, timeZone?: string): { time: string; meridiem: string } {
|
|
384
414
|
const full = hasIntl()
|
|
385
|
-
?
|
|
415
|
+
? dateFormat(locale, { hour: "numeric", minute: "2-digit", hour12: true }, timeZone).format(d)
|
|
386
416
|
: clock12(d);
|
|
387
417
|
// ICU puts a narrow no-break space before the meridiem; normalize so callers can split on it.
|
|
388
418
|
const clean = full.replace(/[ ]/g, " ");
|
|
@@ -391,8 +421,8 @@ function clockParts(d: Date, locale: string): { time: string; meridiem: string }
|
|
|
391
421
|
}
|
|
392
422
|
|
|
393
423
|
/** "8:00 PM" — the one clock every customer screen uses (12-hour, en-US shape). */
|
|
394
|
-
export function formatClock(date: Date, locale = "en-US"): string {
|
|
395
|
-
const { time, meridiem } = clockParts(date, locale);
|
|
424
|
+
export function formatClock(date: Date, locale = "en-US", timeZone?: string): string {
|
|
425
|
+
const { time, meridiem } = clockParts(date, locale, timeZone);
|
|
396
426
|
return meridiem ? `${time} ${meridiem}` : time;
|
|
397
427
|
}
|
|
398
428
|
|
|
@@ -403,3 +433,14 @@ export function isPastDate(iso: string | undefined | null, now: number = Date.no
|
|
|
403
433
|
const t = Date.parse(iso);
|
|
404
434
|
return !Number.isNaN(t) && t < now;
|
|
405
435
|
}
|
|
436
|
+
|
|
437
|
+
/** The calendar day `date` falls on in `timeZone` (device zone when absent, unknown, or when
|
|
438
|
+
* the engine has no Intl), as `YYYY-MM-DD`. For "is this today / this year" comparisons. */
|
|
439
|
+
export function calendarDay(date: Date, timeZone?: string): string {
|
|
440
|
+
if (hasIntl()) {
|
|
441
|
+
const parts = dateFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit" }, timeZone).formatToParts(date);
|
|
442
|
+
const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "";
|
|
443
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
444
|
+
}
|
|
445
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
446
|
+
}
|