@wix/himalaya-cli 0.1209.0 → 0.1211.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 +112 -18
- package/dist/preview-assets.generated.bin +0 -0
- 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 +80 -50
- 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/himi-text.ts +24 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- 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 +80 -50
- 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/himi-text.ts +24 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/community-club-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/community-club-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/events-meetups-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/events-meetups-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/gym-fitness-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/nonprofit-cause-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/owner-lite/dev/index.ts +81 -51
- 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/himi-text.ts +24 -0
- package/templates/owner-lite/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/owner-lite/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/owner-lite/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-account/index.ts +12 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
- 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 +29 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/owner-lite/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/podcast-creator-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/portfolio-creative-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/restaurant-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/restaurant-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-drawer/index.ts +21 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/retail-store-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/retail-store-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/yoga-studio-owner/dev/index.ts +80 -50
- 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/himi-text.ts +24 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-account.ts +4 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-selected-site.ts +103 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-site-pick.ts +74 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-account/index.ts +12 -2
- 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-drawer/index.ts +21 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +29 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +25 -5
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-overview/index.ts +8 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-sites/index.ts +14 -31
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Switching the business an __HIMI_APP_NAME__ session manages — the one implementation
|
|
2
|
+
// behind every "pick a business" row (the Sites list and the cross-business Overview).
|
|
3
|
+
//
|
|
4
|
+
// Previously those rows were declarative `himi.open{business}` taps: the shell scoped
|
|
5
|
+
// the bridge and PUSHED Home, and nothing recorded which business was picked, so the
|
|
6
|
+
// chrome kept naming the build-time one and a `‹ Back` led to the list just left. As a
|
|
7
|
+
// worker action the row can record the name first, then hand the shell the same
|
|
8
|
+
// `business`-carrying open — now as a `replace` onto the Home root.
|
|
9
|
+
//
|
|
10
|
+
// Scoping stays with the shell's router (SP-1: a `business` nav param scopes the Wix
|
|
11
|
+
// bridge BEFORE navigating, on iOS and Android). It is deliberately not done here with
|
|
12
|
+
// `site.setActiveSite`: owner-lite's native shells do not register the worker-facing
|
|
13
|
+
// `siteSetActive` capability, so the call throws and the tap would go nowhere.
|
|
14
|
+
|
|
15
|
+
import { navigate, type NavDirective } from "@himalaya/state";
|
|
16
|
+
import { forgetCatalogVersion } from "@himalaya/wix-stores";
|
|
17
|
+
import { rememberSelectedSite } from "./owner-selected-site.js";
|
|
18
|
+
|
|
19
|
+
export const OWNER_HOME = "himi_owner_lite_home";
|
|
20
|
+
|
|
21
|
+
/** Set by a pick, cleared by `settleBusinessSwitch` once the new business is scoped. */
|
|
22
|
+
const SWITCH_PENDING_KEY = "himi.ownerLite.businessSwitchPending.v1";
|
|
23
|
+
|
|
24
|
+
type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void };
|
|
25
|
+
const storage = (): StorageLike | undefined =>
|
|
26
|
+
(globalThis as { himalaya?: { storage?: StorageLike } }).himalaya?.storage;
|
|
27
|
+
|
|
28
|
+
/** `{ id | business, name }` from a row's action params (or a bare msid string). */
|
|
29
|
+
export function pickedSite(arg: unknown): { msid: string; name: string } {
|
|
30
|
+
if (typeof arg === "string") return { msid: arg.trim(), name: "" };
|
|
31
|
+
const a = (arg ?? {}) as { business?: unknown; id?: unknown; name?: unknown };
|
|
32
|
+
const msid = typeof a.business === "string" && a.business ? a.business : typeof a.id === "string" ? a.id : "";
|
|
33
|
+
return { msid: msid.trim(), name: typeof a.name === "string" ? a.name.trim() : "" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Record `name` as the business being managed, then return `state` carrying a
|
|
38
|
+
* `replace` onto the Home root that asks the shell to scope to `msid`. Returns `state`
|
|
39
|
+
* untouched (no navigation) when there is no msid.
|
|
40
|
+
*/
|
|
41
|
+
export function pickSite<S extends object>(state: S, msid: string, name: string): S | (S & { _nav: NavDirective }) {
|
|
42
|
+
if (!msid) return state;
|
|
43
|
+
// Another business may run the other Stores catalog. The resolved version is
|
|
44
|
+
// persisted app-wide and routes WRITES, which have no 428 retry, so it is dropped
|
|
45
|
+
// before the switch rather than left to be re-used against the new site.
|
|
46
|
+
forgetCatalogVersion();
|
|
47
|
+
// ...but the shell scopes the bridge to `msid` only AFTER this returns, so a warm
|
|
48
|
+
// Stores worker resolving in that gap still talks to the OLD site and can persist
|
|
49
|
+
// its version over the tombstone above. Leave a marker so the first worker that
|
|
50
|
+
// runs after scoping drops the catalog version once more (`settleBusinessSwitch`).
|
|
51
|
+
try { storage()?.setItem(SWITCH_PENDING_KEY, msid); } catch { /* no storage: the first forget stands */ }
|
|
52
|
+
if (name) rememberSelectedSite({ id: msid, name });
|
|
53
|
+
// `replace` onto Home, the tab root the pickers are reached from: the core router
|
|
54
|
+
// lands ON the root instead of stacking a second Home above the list.
|
|
55
|
+
return navigate(state, { screenId: OWNER_HOME, presentation: "replace", args: { business: msid } });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Finish a business switch: if a pick is pending, drop the Stores catalog version
|
|
60
|
+
* again and clear the marker. Call it first in a worker that runs only after the shell
|
|
61
|
+
* has scoped the bridge to the picked business (Home's `load`: the pick `replace`s
|
|
62
|
+
* onto Home, and the router scopes before it navigates), so a version persisted from
|
|
63
|
+
* the old site during the scoping gap is discarded. Never throws.
|
|
64
|
+
*/
|
|
65
|
+
export function settleBusinessSwitch(): void {
|
|
66
|
+
try {
|
|
67
|
+
const s = storage();
|
|
68
|
+
if (!s?.getItem(SWITCH_PENDING_KEY)) return;
|
|
69
|
+
forgetCatalogVersion();
|
|
70
|
+
s.setItem(SWITCH_PENDING_KEY, "");
|
|
71
|
+
} catch {
|
|
72
|
+
// No storage, or it failed: nothing pending can be recorded either.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
// from provisioning.json `identity` — so a branded template ships ITS links,
|
|
12
12
|
// and `openLink` can only ever open one of those three URLs.
|
|
13
13
|
//
|
|
14
|
+
// • which business this is — the one the owner picked on Sites (see
|
|
15
|
+
// `_shared/owner-selected-site.ts`), not the build-time provisioning name,
|
|
16
|
+
// which arrives only as the cold-start `businessName` param. A local read,
|
|
17
|
+
// so it is set synchronously in the initial state and again on every load.
|
|
18
|
+
//
|
|
14
19
|
// Every read is best-effort: an identity or version the host cannot answer
|
|
15
20
|
// collapses its line rather than rendering an error on a settings screen.
|
|
16
21
|
|
|
@@ -20,19 +25,24 @@ import { displayVersion } from "@himalaya/app-info";
|
|
|
20
25
|
import { open } from "@himalaya/open-url";
|
|
21
26
|
import { stashCopy, copyTemplate } from "../_shared/himi-copy.js";
|
|
22
27
|
import { ownerLineOf, shapeAccount, type AccountState } from "../_shared/owner-account.js";
|
|
28
|
+
import { fallbackNameFrom, selectedSiteName } from "../_shared/owner-selected-site.js";
|
|
23
29
|
|
|
24
30
|
type LinkKind = "support" | "privacy" | "terms";
|
|
25
31
|
|
|
26
32
|
const obj = (v: unknown): Record<string, unknown> => (v && typeof v === "object" ? v as Record<string, unknown> : {});
|
|
27
33
|
|
|
34
|
+
let fallbackBusinessName = "";
|
|
35
|
+
|
|
28
36
|
const svc = defineService<AccountState, {
|
|
29
37
|
load: (s: AccountState, arg?: unknown) => Promise<AccountState>;
|
|
30
38
|
openLink: (s: AccountState, arg?: unknown) => Promise<AccountState>;
|
|
31
39
|
}>("himiOwnerLiteAccount", {
|
|
32
|
-
state: shapeAccount({}),
|
|
40
|
+
state: shapeAccount({ businessName: selectedSiteName(fallbackBusinessName) }),
|
|
33
41
|
actions: {
|
|
34
42
|
load: async (s, arg) => {
|
|
35
43
|
stashCopy(arg);
|
|
44
|
+
fallbackBusinessName = fallbackNameFrom(arg, fallbackBusinessName);
|
|
45
|
+
const businessName = selectedSiteName(fallbackBusinessName);
|
|
36
46
|
const p = obj(arg);
|
|
37
47
|
const links = { supportUrl: String(p.supportUrl ?? s.supportUrl), privacyUrl: String(p.privacyUrl ?? s.privacyUrl), termsUrl: String(p.termsUrl ?? s.termsUrl) };
|
|
38
48
|
let ownerLine = "";
|
|
@@ -54,7 +64,7 @@ const svc = defineService<AccountState, {
|
|
|
54
64
|
const v = await displayVersion();
|
|
55
65
|
versionLine = v ? copyTemplate("ownerAccount.version", "Version {version}", { version: v }) : "";
|
|
56
66
|
} catch { /* host without app_info — the row collapses */ }
|
|
57
|
-
return shapeAccount({ ...s, ...links, status: "ready", ownerLine, versionLine, error });
|
|
67
|
+
return shapeAccount({ ...s, ...links, status: "ready", businessName, ownerLine, versionLine, error });
|
|
58
68
|
},
|
|
59
69
|
openLink: async (s, arg) => {
|
|
60
70
|
const kind = String(obj(arg).kind ?? arg ?? "") as LinkKind;
|
|
@@ -26,6 +26,7 @@ const core = createAriaHistory({
|
|
|
26
26
|
last7: () => copyParam("ownerAriaHistory.last7", "Last 7 days"),
|
|
27
27
|
last30: () => copyParam("ownerAriaHistory.last30", "Last 30 days"),
|
|
28
28
|
older: () => copyParam("ownerAriaHistory.older", "Older"),
|
|
29
|
+
today: () => copyParam("ownerAriaHistory.today", "Today"),
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -20,13 +20,23 @@
|
|
|
20
20
|
import { defineService } from "@himalaya/state";
|
|
21
21
|
import { serializeForAgent } from "@himalaya/agent";
|
|
22
22
|
import { activeScreenFromInput, attentionRow, navRow, withDerived, type DrawerState } from "./logic.js";
|
|
23
|
+
import { fallbackNameFrom, selectedSiteName } from "../_shared/owner-selected-site.js";
|
|
23
24
|
|
|
24
25
|
/** The shell passes the screen it is showing as worker input, so the drawer can mark
|
|
25
26
|
* that destination selected the way the declarative drawer does. */
|
|
26
27
|
const hostActiveScreen = () => activeScreenFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input);
|
|
27
28
|
|
|
29
|
+
/** Cold-start name from the descriptor's onAppear params (provisioning identity). */
|
|
30
|
+
let fallbackBusinessName = "";
|
|
31
|
+
|
|
32
|
+
/** "Which site am I on": the business picked on Sites, else the fallback. */
|
|
33
|
+
const currentSiteName = () => selectedSiteName(fallbackBusinessName);
|
|
34
|
+
|
|
28
35
|
const drawer = defineService<DrawerState, {
|
|
29
|
-
/**
|
|
36
|
+
/** The panel opened (root onAppear): re-read the live site and the active screen.
|
|
37
|
+
* `arg.businessName` is the cold-start fallback for the site name. */
|
|
38
|
+
refresh: (s: DrawerState, arg?: unknown) => DrawerState;
|
|
39
|
+
/** Re-read the active screen from host input. An explicit
|
|
30
40
|
* `{ screenId }` wins, for a host that pushes the change instead. */
|
|
31
41
|
syncActive: (s: DrawerState, args?: { screenId?: string }) => DrawerState;
|
|
32
42
|
/** First tap on "Mark all handled": ask before hiding anything. */
|
|
@@ -37,7 +47,7 @@ const drawer = defineService<DrawerState, {
|
|
|
37
47
|
clearAttention: (s: DrawerState) => DrawerState;
|
|
38
48
|
}>("drawer", {
|
|
39
49
|
state: withDerived({
|
|
40
|
-
siteName:
|
|
50
|
+
siteName: currentSiteName(),
|
|
41
51
|
nav: [
|
|
42
52
|
navRow("drawer_home", "Home", "home", "himi_owner_lite_home"),
|
|
43
53
|
// Between Home and Manage deliberately: analytics answers "how is the
|
|
@@ -55,6 +65,15 @@ const drawer = defineService<DrawerState, {
|
|
|
55
65
|
activeScreenId: hostActiveScreen(),
|
|
56
66
|
}),
|
|
57
67
|
actions: {
|
|
68
|
+
refresh: (s, arg) => {
|
|
69
|
+
fallbackBusinessName = fallbackNameFrom(arg, fallbackBusinessName);
|
|
70
|
+
const screenId = (arg as { screenId?: unknown } | undefined)?.screenId;
|
|
71
|
+
return snap(withDerived({
|
|
72
|
+
...s,
|
|
73
|
+
siteName: currentSiteName(),
|
|
74
|
+
activeScreenId: typeof screenId === "string" ? screenId : hostActiveScreen(),
|
|
75
|
+
}));
|
|
76
|
+
},
|
|
58
77
|
syncActive: (s, args) => snap(withDerived({
|
|
59
78
|
...s,
|
|
60
79
|
activeScreenId: typeof args?.screenId === "string" ? args.screenId : hostActiveScreen(),
|
|
@@ -21,6 +21,8 @@ import { getSiteAnalytics, getSiteTimeZone, localDateRange, localDateOf, localMi
|
|
|
21
21
|
import { ownerDisplayError, isBridgeUnavailable, isOwnerPreviewDemo } from "../_shared/owner-load-error.js";
|
|
22
22
|
import { DEMO_DASHBOARD, DEMO_NOTE, DEMO_TODAY } from "../_shared/owner-preview-demo.js";
|
|
23
23
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
24
|
+
import { fallbackNameFrom, selectedSiteName } from "../_shared/owner-selected-site.js";
|
|
25
|
+
import { settleBusinessSwitch } from "../_shared/owner-site-pick.js";
|
|
24
26
|
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
25
27
|
import {
|
|
26
28
|
attentionRecords,
|
|
@@ -51,6 +53,9 @@ interface SourceState {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
interface HomeState {
|
|
56
|
+
/** The business the header names: the one picked on Sites, else the descriptor's
|
|
57
|
+
* build-time fallback. */
|
|
58
|
+
businessName: string;
|
|
54
59
|
attention: TodayRecord[];
|
|
55
60
|
schedule: TodayRecord[];
|
|
56
61
|
allSchedule: TodayRecord[];
|
|
@@ -79,6 +84,10 @@ interface HomeState {
|
|
|
79
84
|
notLoading: boolean;
|
|
80
85
|
notErrored: boolean;
|
|
81
86
|
hideContent: boolean;
|
|
87
|
+
/** The retry cards' list is hidden unless the dashboard is ready AND a source failed.
|
|
88
|
+
* An empty list still costs its parent stack a gap, which on iOS read as a blank
|
|
89
|
+
* band under the header; gating on `hideContent` alone left it mounted. */
|
|
90
|
+
hideSourceFailures: boolean;
|
|
82
91
|
demoNote: string;
|
|
83
92
|
notDemo: boolean;
|
|
84
93
|
}
|
|
@@ -101,11 +110,18 @@ const SOURCE_LABELS: Record<HomeSourceId, string> = {
|
|
|
101
110
|
reviews: "Reviews", reservations: "Reservations",
|
|
102
111
|
};
|
|
103
112
|
|
|
113
|
+
/** Cold-start name from the descriptor's onAppear params (provisioning identity). */
|
|
114
|
+
let fallbackBusinessName = "";
|
|
115
|
+
/** Resolved once per load, not per shape: a storage read is a host round-trip, and the
|
|
116
|
+
* picked business only changes between loads (the picker is another screen). */
|
|
117
|
+
let currentBusinessName = selectedSiteName("");
|
|
118
|
+
|
|
104
119
|
function shape(s: Partial<HomeState>): HomeState {
|
|
105
120
|
const status = s.status ?? "ready";
|
|
106
121
|
const isLoading = status === "loading";
|
|
107
122
|
return {
|
|
108
123
|
status,
|
|
124
|
+
businessName: currentBusinessName,
|
|
109
125
|
attention: s.attention ?? [],
|
|
110
126
|
schedule: s.schedule ?? [],
|
|
111
127
|
allSchedule: s.allSchedule ?? s.schedule ?? [],
|
|
@@ -133,6 +149,7 @@ function shape(s: Partial<HomeState>): HomeState {
|
|
|
133
149
|
notLoading: !isLoading,
|
|
134
150
|
notErrored: status !== "error",
|
|
135
151
|
hideContent: status !== "ready",
|
|
152
|
+
hideSourceFailures: status !== "ready" || !(s.sourceFailures?.length),
|
|
136
153
|
demoNote: s.demoNote ?? "",
|
|
137
154
|
notDemo: !s.demoNote,
|
|
138
155
|
};
|
|
@@ -834,8 +851,19 @@ const svc = defineService<HomeState, {
|
|
|
834
851
|
})) }),
|
|
835
852
|
actions: {
|
|
836
853
|
load: async (s, arg) => {
|
|
854
|
+
// FIRST, before anything reads the catalog: a business pick `replace`s onto
|
|
855
|
+
// Home, and the shell scopes the bridge to the picked business before it
|
|
856
|
+
// navigates, so this load is the first code that runs on the new site. A Stores
|
|
857
|
+
// catalog version persisted from the OLD site during that gap is dropped here.
|
|
858
|
+
settleBusinessSwitch();
|
|
837
859
|
stashCopy(arg);
|
|
838
|
-
|
|
860
|
+
fallbackBusinessName = fallbackNameFrom(arg, fallbackBusinessName);
|
|
861
|
+
// Re-read here: Home's root is not remounted when the owner switches business,
|
|
862
|
+
// and the picker records the selection from a different worker.
|
|
863
|
+
currentBusinessName = selectedSiteName(fallbackBusinessName);
|
|
864
|
+
// An argless re-load (the host refreshing a warm root after the owner switches
|
|
865
|
+
// business) keeps the sources the descriptor enabled rather than widening to all.
|
|
866
|
+
const sources = enabledSourcesFrom(arg) ?? activeSources;
|
|
839
867
|
return loadDashboard(shape({ ...s, status: "loading" }), sources);
|
|
840
868
|
},
|
|
841
869
|
retry: async (s) => loadDashboard(shape({ ...s, status: "loading" }), activeSources),
|
|
@@ -16,9 +16,13 @@ import { queryOwnerModule } from '../_shared/owner-module-data.js';
|
|
|
16
16
|
import { OWNER_MODULES, ownerModuleId } from '../_shared/owner-modules.js';
|
|
17
17
|
import { localPreferences } from '../_shared/local-preferences.js';
|
|
18
18
|
import { stashCopy, copyParam, copyTemplate } from '../_shared/himi-copy.js';
|
|
19
|
+
import { fallbackNameFrom, selectedSiteName } from '../_shared/owner-selected-site.js';
|
|
19
20
|
import { DASHBOARD_PATHS, deriveManage, parseAreas, recentAreas, rememberArea, withMetrics, type ManageArea, type Metric } from './logic.js';
|
|
20
21
|
|
|
21
22
|
interface State {
|
|
23
|
+
/** The business the header names: the one picked on Sites, else the descriptor's
|
|
24
|
+
* build-time fallback. */
|
|
25
|
+
businessName: string;
|
|
22
26
|
/** `loading` until the descriptor has delivered the areas, then
|
|
23
27
|
* `ready`. The descriptor keys a FragmentSlot on it whose DEFAULT case is the
|
|
24
28
|
* skeleton, so an unresolved path (worker not booted yet) is loading too —
|
|
@@ -35,6 +39,8 @@ interface State {
|
|
|
35
39
|
noEmpty: boolean;
|
|
36
40
|
hasRecents: boolean;
|
|
37
41
|
noRecents: boolean;
|
|
42
|
+
/** True while `sections` is empty — gates the list so an empty one costs no gap. */
|
|
43
|
+
noSections: boolean;
|
|
38
44
|
isLoading: boolean;
|
|
39
45
|
notLoading: boolean;
|
|
40
46
|
summaryNote: string;
|
|
@@ -178,6 +184,11 @@ async function metricFor(metricKey: string): Promise<Metric> {
|
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
/** Cold-start name from the descriptor's onAppear params (provisioning identity). */
|
|
188
|
+
let fallbackBusinessName = '';
|
|
189
|
+
/** Resolved per load/refresh, not per derive (a storage read is a host round-trip). */
|
|
190
|
+
let currentBusinessName = selectedSiteName('');
|
|
191
|
+
|
|
181
192
|
function derive(state: State, areas = state.areas): State {
|
|
182
193
|
const next = deriveManage(
|
|
183
194
|
areas,
|
|
@@ -191,7 +202,7 @@ function derive(state: State, areas = state.areas): State {
|
|
|
191
202
|
// areas arrive — or when this app provisions none at all — there is nothing a
|
|
192
203
|
// filter could have excluded, so "Clear filters" would be a lie.
|
|
193
204
|
const noEmpty = next.noEmpty || state.status !== 'ready' || areas.length === 0;
|
|
194
|
-
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0 };
|
|
205
|
+
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0, noSections: next.sections.length === 0, businessName: currentBusinessName };
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
// Loads are not serialized: on a warm reappearance the host's bare refresh and
|
|
@@ -261,16 +272,21 @@ const svc = defineService<State, {
|
|
|
261
272
|
handoff: (s: State, arg?: unknown) => Promise<State>;
|
|
262
273
|
}>('himiOwnerLiteManage', {
|
|
263
274
|
state: {
|
|
275
|
+
businessName: currentBusinessName,
|
|
264
276
|
status: 'loading',
|
|
265
277
|
areas: [], results: [], sections: [], recents: [], query: '', group: 'all',
|
|
266
278
|
// 'All' is always a valid filter, so the chip row paints from the first frame
|
|
267
279
|
// instead of appearing (and shifting the layout) when the areas arrive.
|
|
268
280
|
groups: [{ id: 'all', label: allLabel() }], summary: '', noEmpty: true,
|
|
269
|
-
hasRecents: false, noRecents: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
281
|
+
hasRecents: false, noRecents: true, noSections: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
270
282
|
},
|
|
271
283
|
actions: {
|
|
272
284
|
load: async (state, arg) => {
|
|
273
285
|
stashCopy(arg);
|
|
286
|
+
fallbackBusinessName = fallbackNameFrom(arg, fallbackBusinessName);
|
|
287
|
+
// Re-read on every load: this worker stays warm across tab switches, and the
|
|
288
|
+
// picker records the selection from another worker.
|
|
289
|
+
currentBusinessName = selectedSiteName(fallbackBusinessName);
|
|
274
290
|
// A `load` with no areas comes from the host, not the descriptor: the
|
|
275
291
|
// warm-worker refresh lifecycle calls it bare. Before the areas are known
|
|
276
292
|
// it must not settle (that derived "0 business areas" and painted the
|
|
@@ -278,7 +294,7 @@ const svc = defineService<State, {
|
|
|
278
294
|
if (!('areas' in read(arg))) {
|
|
279
295
|
return state.status === 'ready'
|
|
280
296
|
? loadMetrics(derive({ ...state, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }))
|
|
281
|
-
: state;
|
|
297
|
+
: { ...state, businessName: currentBusinessName };
|
|
282
298
|
}
|
|
283
299
|
const parsed = (() => { try { return JSON.parse(str(arg, 'areas') || '[]'); } catch { return []; } })();
|
|
284
300
|
const areas = parseAreas(parsed);
|
|
@@ -286,8 +302,12 @@ const svc = defineService<State, {
|
|
|
286
302
|
},
|
|
287
303
|
// Refresh re-reads summaries for KNOWN areas. Before the descriptor has
|
|
288
304
|
// delivered them there is nothing to refresh, and settling here would open
|
|
289
|
-
// the skeleton gate over an empty hub.
|
|
290
|
-
|
|
305
|
+
// the skeleton gate over an empty hub. The business name is a local read, so
|
|
306
|
+
// it is re-read either way.
|
|
307
|
+
refresh: async state => {
|
|
308
|
+
currentBusinessName = selectedSiteName(fallbackBusinessName);
|
|
309
|
+
return state.status !== 'ready' ? { ...state, businessName: currentBusinessName } : loadMetrics(derive({ ...state, isLoading: true, notLoading: false, summaryNote: '', handoffError: '' }));
|
|
310
|
+
},
|
|
291
311
|
search: (state, arg) => derive({ ...state, query: str(arg, 'value').slice(0, 256) }),
|
|
292
312
|
filter: (state, arg) => {
|
|
293
313
|
const ids = read(arg).selectedIds;
|
|
@@ -30,6 +30,7 @@ import { searchOrders } from "@himalaya/wix-ecom";
|
|
|
30
30
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo } from "../_shared/owner-load-error.js";
|
|
31
31
|
import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
|
|
32
32
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
33
|
+
import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
|
|
33
34
|
|
|
34
35
|
/** Mirrors the Home dashboard's "needs attention" definition, so a number here means the
|
|
35
36
|
* same thing it means after the owner drills into that business. */
|
|
@@ -205,6 +206,7 @@ async function loadRollup(): Promise<OverviewState> {
|
|
|
205
206
|
const svc = defineService<OverviewState, {
|
|
206
207
|
load: (s: OverviewState, arg?: unknown) => Promise<OverviewState>;
|
|
207
208
|
retry: (s: OverviewState) => Promise<OverviewState>;
|
|
209
|
+
pick: (s: OverviewState, arg?: unknown) => OverviewState;
|
|
208
210
|
}>("himiOwnerLiteOverview", {
|
|
209
211
|
state: shape({ status: "loading" }),
|
|
210
212
|
actions: {
|
|
@@ -213,6 +215,12 @@ const svc = defineService<OverviewState, {
|
|
|
213
215
|
return loadRollup();
|
|
214
216
|
},
|
|
215
217
|
retry: async () => loadRollup(),
|
|
218
|
+
// Drilling into one business is the ONLY thing that moves the active business —
|
|
219
|
+
// the same switch the Sites list makes (scope, name it, reset to Home).
|
|
220
|
+
pick: (s, arg) => {
|
|
221
|
+
const { msid, name } = pickedSite(arg);
|
|
222
|
+
return pickSite(s, msid, name || s.rows.find((r) => r.id === msid)?.name || "");
|
|
223
|
+
},
|
|
216
224
|
},
|
|
217
225
|
});
|
|
218
226
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// Tier-5 bundle: __HIMI_APP_NAME__ — Sites List (multi-site chooser).
|
|
2
2
|
// ACCOUNT-level: `site-list/v2/sites/query` authorizes on the owner wixSession2
|
|
3
3
|
// cookie alone (no per-site signed-instance), so this loads before any business
|
|
4
|
-
// is picked. Each row's `id` IS the site's metaSiteId — the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// is picked. Each row's `id` IS the site's metaSiteId — `pick` records the row's
|
|
5
|
+
// name for the chrome and hands the shell a `business`-carrying `replace` onto Home;
|
|
6
|
+
// the shell's Router scopes the Wix bridge to it (SiteInstanceStore mints that site's
|
|
7
|
+
// User signed-instance for the management verticals).
|
|
7
8
|
|
|
8
9
|
import { defineService } from "@himalaya/state";
|
|
9
10
|
import { querySites, type WixSite } from "@himalaya/wix-site-list";
|
|
10
|
-
import { site } from "@himalaya/wix-platform";
|
|
11
11
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo } from "../_shared/owner-load-error.js";
|
|
12
12
|
import { DEMO_SITES, DEMO_NOTE } from "../_shared/owner-preview-demo.js";
|
|
13
13
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
14
14
|
import { getAll, upsert, type DbEntity } from "../_shared/owner-local-db.js";
|
|
15
|
-
import {
|
|
15
|
+
import { pickSite, pickedSite } from "../_shared/owner-site-pick.js";
|
|
16
16
|
|
|
17
17
|
// Offline-first (data-offline): the owner's last-synced businesses are the
|
|
18
18
|
// READ-THROUGH cache for the `sites` entity — paint them instantly (works
|
|
@@ -139,17 +139,16 @@ async function loadList(s: SitesState): Promise<SitesState> {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: String((arg as { business?: string; id?: string })?.business ?? (arg as { id?: string })?.id ?? "");
|
|
142
|
+
/** The picked row's display name: the row passes it, else it is looked up in the list. */
|
|
143
|
+
function nameFor(s: SitesState, msid: string, passed: string): string {
|
|
144
|
+
return passed || s.allSites.find((r) => r.id === msid)?.name || "";
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
const svc = defineService<SitesState, {
|
|
149
148
|
load: (s: SitesState, arg?: unknown) => Promise<SitesState>;
|
|
150
149
|
retry: (s: SitesState) => Promise<SitesState>;
|
|
151
150
|
search: (s: SitesState, arg?: unknown) => Promise<SitesState>;
|
|
152
|
-
pick: (s: SitesState, arg?: unknown) =>
|
|
151
|
+
pick: (s: SitesState, arg?: unknown) => SitesState;
|
|
153
152
|
}>("himiOwnerLiteSites", {
|
|
154
153
|
state: shape({ status: "loading" }),
|
|
155
154
|
actions: {
|
|
@@ -158,27 +157,11 @@ const svc = defineService<SitesState, {
|
|
|
158
157
|
return loadList(shape({ ...s, status: "loading" }));
|
|
159
158
|
},
|
|
160
159
|
retry: async (s) => loadList(shape({ ...s, status: "loading" })),
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const msid = msidFrom(arg);
|
|
167
|
-
if (msid) {
|
|
168
|
-
// Scoping the bridge to another business points it at another SITE, which may
|
|
169
|
-
// run the other Stores catalog. The resolved version is persisted app-wide and
|
|
170
|
-
// routes WRITES, which have no 428 retry — so it has to be dropped here, not
|
|
171
|
-
// just on the consumer-side site picker.
|
|
172
|
-
//
|
|
173
|
-
// Cleared on BOTH sides of the await: before, in case native remounts us
|
|
174
|
-
// before it settles; after, because a read interleaving it would re-resolve
|
|
175
|
-
// against the bridge that has not been re-scoped yet and re-persist the value
|
|
176
|
-
// we just dropped.
|
|
177
|
-
forgetCatalogVersion();
|
|
178
|
-
await site.setActiveSite(msid);
|
|
179
|
-
forgetCatalogVersion();
|
|
180
|
-
}
|
|
181
|
-
return s;
|
|
160
|
+
// Picking a business (every Sites row dispatches this): record its name for the
|
|
161
|
+
// chrome, then reset to the Home root; the shell scopes the bridge.
|
|
162
|
+
pick: (s, arg) => {
|
|
163
|
+
const { msid, name } = pickedSite(arg);
|
|
164
|
+
return pickSite(s, msid, nameFor(s, msid, name));
|
|
182
165
|
},
|
|
183
166
|
search: async (s, arg) => {
|
|
184
167
|
const q = typeof arg === "string" ? arg : String((arg as { value?: string; query?: string })?.value ?? (arg as { query?: string })?.query ?? "");
|
|
@@ -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
|
+
}
|