@wix/himalaya-cli 0.1032.0 → 0.1034.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 +0 -2
- package/guides/getting-started.md +6 -2
- package/guides/wix-site-to-himi-app.md +9 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +398 -1
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/beauty-salon/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +3 -2
- package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +3 -2
- package/templates/beauty-salon/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/beauty-salon/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/beauty-salon-owner/dev/index.ts +1216 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/branded-lite/dev/index.ts +2239 -343
- package/templates/branded-lite/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/branded-lite/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-badges/index.ts +20 -16
- package/templates/branded-lite/tier5-src/himi-branded-lite-coupons/index.ts +20 -20
- package/templates/branded-lite/tier5-src/himi-branded-lite-donations/index.ts +20 -16
- package/templates/branded-lite/tier5-src/himi-branded-lite-forms/index.ts +23 -31
- package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +19 -14
- package/templates/branded-lite/tier5-src/himi-branded-lite-giftcards/index.ts +20 -16
- package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +20 -15
- package/templates/branded-lite/tier5-src/himi-branded-lite-invoices/index.ts +20 -16
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +20 -17
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +20 -18
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +23 -19
- package/templates/branded-lite/tier5-src/himi-branded-lite-podcasts/index.ts +1 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +24 -20
- package/templates/branded-lite/tier5-src/himi-branded-lite-reviews/index.ts +20 -17
- package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +19 -15
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +3 -2
- package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +3 -2
- package/templates/branded-lite/tier5-src/wix-spaces-forum/index.ts +3 -2
- package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +19 -6
- package/templates/branded-lite/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/branded-lite/tier5-src/wix-spaces-new-message/index.ts +3 -2
- package/templates/branded-lite/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/community-club/dev/index.ts +283 -2
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/community-club/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/community-club/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/community-club/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/community-club-owner/dev/index.ts +1216 -0
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/events-meetups/dev/index.ts +197 -1
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/events-meetups/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/events-meetups/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/events-meetups-owner/dev/index.ts +1088 -0
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/gym-fitness/dev/index.ts +462 -1
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/gym-fitness/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +3 -2
- package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +3 -2
- package/templates/gym-fitness/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/gym-fitness/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/gym-fitness-owner/dev/index.ts +1280 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/nonprofit-cause/dev/index.ts +264 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/nonprofit-cause/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/nonprofit-cause/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/nonprofit-cause/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/nonprofit-cause-owner/dev/index.ts +1152 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/owner-lite/dev/index.ts +1280 -0
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/podcast-creator/dev/index.ts +286 -23
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/podcast-creator/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-podcasts/index.ts +1 -1
- package/templates/podcast-creator/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/podcast-creator/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/podcast-creator-owner/dev/index.ts +1216 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/portfolio-creative/dev/index.ts +353 -23
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/portfolio-creative/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +3 -2
- package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +3 -2
- package/templates/portfolio-creative/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/portfolio-creative-owner/dev/index.ts +1216 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/restaurant/dev/index.ts +197 -1
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/restaurant/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/restaurant/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/restaurant-owner/dev/index.ts +1280 -0
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/retail-store/dev/index.ts +203 -1
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/retail-store/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +3 -2
- package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +3 -2
- package/templates/retail-store/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/retail-store-owner/dev/index.ts +1152 -0
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/yoga-studio/dev/index.ts +392 -1
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +36 -5
- package/templates/yoga-studio/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/yoga-studio/tier5-src/wix-spaces-my-subscriptions/index.ts +11 -2
- package/templates/yoga-studio/tier5-src/wix-spaces-whats-new/index.ts +3 -2
- package/templates/yoga-studio-owner/dev/index.ts +1152 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +4 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +36 -5
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Load-state honesty for the branded-lite vertical workers.
|
|
2
|
+
//
|
|
3
|
+
// These workers used to answer every failed or empty query with representative SAMPLE
|
|
4
|
+
// rows — discount codes, gift-card balances, invoices, reservations, inbox messages —
|
|
5
|
+
// under a "Sample content" banner. On a customer build that is invented data presented
|
|
6
|
+
// as the customer's own: a code that does not exist, a balance in the wrong currency, an
|
|
7
|
+
// "Overdue" invoice, a booking labelled "tap to cancel". The banner did not make it
|
|
8
|
+
// honest; the rows were still the first thing a visitor read.
|
|
9
|
+
//
|
|
10
|
+
// The rule now:
|
|
11
|
+
// • query FAILED → status "error" with a customer sentence, a Retry, and — when
|
|
12
|
+
// Wix refused an anonymous visitor (401) — a Sign in path instead;
|
|
13
|
+
// • query was EMPTY → status "ready", no rows, the vertical's own empty sentence;
|
|
14
|
+
// • sample rows → ONLY when the host explicitly asked for sample data (the web
|
|
15
|
+
// preview's sample data mode, `__himiOwnerPreview: "sample"`).
|
|
16
|
+
// Never on a device, never as a fallback.
|
|
17
|
+
//
|
|
18
|
+
// Every field here is flat and boolean-gated because descriptors can only
|
|
19
|
+
// `hiddenWhen(<path>)` on a TRUE value: `notErrored` hides the error card, `noSignIn`
|
|
20
|
+
// hides the sign-in CTA, `signInRequired` hides Retry when retrying cannot help.
|
|
21
|
+
import { copyParam } from "./himi-copy.js";
|
|
22
|
+
|
|
23
|
+
export interface LoadFlags {
|
|
24
|
+
status: "loading" | "ready" | "error";
|
|
25
|
+
/** The customer-facing failure sentence; "" unless status is "error". */
|
|
26
|
+
error: string;
|
|
27
|
+
signInRequired: boolean;
|
|
28
|
+
notErrored: boolean;
|
|
29
|
+
/** hiddenWhen gate for a loading skeleton. */
|
|
30
|
+
notLoading: boolean;
|
|
31
|
+
/** Loading or failed — gates content that has nothing to say in either. */
|
|
32
|
+
busy: boolean;
|
|
33
|
+
noSignIn: boolean;
|
|
34
|
+
isMock: boolean;
|
|
35
|
+
notMock: boolean;
|
|
36
|
+
notMockEmpty: boolean;
|
|
37
|
+
notMockUnavailable: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const LOADING: LoadFlags = {
|
|
41
|
+
status: "loading", error: "", signInRequired: false, notErrored: true, notLoading: false, busy: true, noSignIn: true,
|
|
42
|
+
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const READY: LoadFlags = { ...LOADING, status: "ready", notLoading: true, busy: false };
|
|
46
|
+
|
|
47
|
+
/** Did the HOST ask for sample data? Only the web preview's sample mode does; a native
|
|
48
|
+
* build, a visitor preview and a live owner preview never carry it. */
|
|
49
|
+
export function samplesRequested(): boolean {
|
|
50
|
+
const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
|
|
51
|
+
return input?.__himiOwnerPreview === "sample";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wix refused an ANONYMOUS visitor: signing in is the fix, not retrying. A 403 is not
|
|
55
|
+
* this — it also answers "feature not installed" and a signed-in member without the
|
|
56
|
+
* permission, where a Sign in button leads nowhere — so it stays a Retry-able failure. */
|
|
57
|
+
export function needsSignIn(err: unknown): boolean {
|
|
58
|
+
const msg = String((err as { message?: unknown })?.message ?? err ?? "");
|
|
59
|
+
return /\b401\b|unauthori[sz]ed|unauthenticated/i.test(msg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadCopy<T> {
|
|
63
|
+
/** The copy namespace (`giftCards`, `coupons`, …) — keys resolve through copyParam. */
|
|
64
|
+
prefix: string;
|
|
65
|
+
/** Shown when the site genuinely has none. */
|
|
66
|
+
empty: string;
|
|
67
|
+
/** Shown when the query failed. */
|
|
68
|
+
error: string;
|
|
69
|
+
/** Shown when Wix refused the visitor (member-only data). */
|
|
70
|
+
signIn: string;
|
|
71
|
+
/** Sample rows for the explicit sample preview ONLY. */
|
|
72
|
+
sample: () => T[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Rows<K extends string, T> = { [P in K]: T[] } & { emptyText: string } & LoadFlags;
|
|
76
|
+
|
|
77
|
+
/** A query answered. Rows win; an empty answer is a real empty state. */
|
|
78
|
+
export function loaded<K extends string, T>(key: K, rows: T[], copy: LoadCopy<T>): Rows<K, T> {
|
|
79
|
+
if (rows.length) return { ...READY, [key]: rows, emptyText: "" } as Rows<K, T>;
|
|
80
|
+
if (samplesRequested()) return sampleRows(key, copy, "empty");
|
|
81
|
+
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty) } as Rows<K, T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A query threw. Never rows the customer did not create. */
|
|
85
|
+
export function loadFailed<K extends string, T>(key: K, err: unknown, copy: LoadCopy<T>): Rows<K, T> {
|
|
86
|
+
if (samplesRequested()) return sampleRows(key, copy, "unavailable");
|
|
87
|
+
const signIn = needsSignIn(err);
|
|
88
|
+
return {
|
|
89
|
+
...READY,
|
|
90
|
+
status: "error",
|
|
91
|
+
[key]: [],
|
|
92
|
+
emptyText: "",
|
|
93
|
+
error: signIn ? copyParam(`${copy.prefix}.signInText`, copy.signIn) : copyParam(`${copy.prefix}.errorText`, copy.error),
|
|
94
|
+
signInRequired: signIn,
|
|
95
|
+
notErrored: false,
|
|
96
|
+
busy: true,
|
|
97
|
+
noSignIn: !signIn,
|
|
98
|
+
} as Rows<K, T>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sampleRows<K extends string, T>(key: K, copy: LoadCopy<T>, reason: "empty" | "unavailable"): Rows<K, T> {
|
|
102
|
+
return {
|
|
103
|
+
...READY, [key]: copy.sample(), emptyText: "",
|
|
104
|
+
isMock: true, notMock: false, notMockEmpty: reason !== "empty", notMockUnavailable: reason !== "unavailable",
|
|
105
|
+
} as Rows<K, T>;
|
|
106
|
+
}
|
|
@@ -30,6 +30,9 @@ export interface CollectionConfig {
|
|
|
30
30
|
* `screenId` opens that screen with the row id; without it the verb reuses the
|
|
31
31
|
* row's own tap action (the detail screen that owns the verb). */
|
|
32
32
|
primaryVerb?: { label: string; screenId?: string };
|
|
33
|
+
/** A state field the DOMAIN narrows `rows` by (e.g. a category picker). While it is set,
|
|
34
|
+
* zero rows is a filtered result, not an empty source. */
|
|
35
|
+
narrowedBy?: string;
|
|
33
36
|
}
|
|
34
37
|
/** At or below this many records, with no query/filter narrowing them and no further
|
|
35
38
|
* page, the list/grid toggle, Refine, chips, search and count are noise. */
|
|
@@ -41,7 +44,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
41
44
|
defaultFilter: 'value:Upcoming', searchPlaceholder: 'Exact booking ID', filterValues: ['Upcoming', 'Past', 'Cancelled'],
|
|
42
45
|
sortLabels: { default: 'Soonest', az: 'Oldest', za: 'Latest' } },
|
|
43
46
|
himiEvents: { rows: 'results', title: 'title', subtitle: 'dateLine', image: 'image', view: 'grid', backendQuery: true, searchPlaceholder: 'Exact event title', filterValues: ['Upcoming', 'Past'], sortLabels: { default: 'Date', az: 'Title A–Z', za: 'Title Z–A' }, primaryVerb: { label: 'Register' } },
|
|
44
|
-
himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid' },
|
|
47
|
+
himiBlogFeed: { rows: 'posts', title: 'title', subtitle: 'excerpt', image: 'image', filter: 'category', filterValuesFrom: 'categories', view: 'grid', narrowedBy: 'selectedCategoryId' },
|
|
45
48
|
himiOwnerLiteProducts: { rows: 'products', title: 'name', subtitle: 'price', image: 'image', view: 'grid', backendQuery: true },
|
|
46
49
|
himiOwnerModules: { instanceInput: 'module', rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', filter: 'statusLabel', view: 'list', paged: true, reviewAction: 'prepareRecord', reviewSelectedAction: 'prepareSelected' },
|
|
47
50
|
himiOwnerLiteServices: { rows: 'services', title: 'name', subtitle: 'subtitle', view: 'list', backendQuery: true, searchPlaceholder: 'Service name starts with…' },
|
|
@@ -122,14 +122,38 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
122
122
|
const errored = s.status === 'error' || s.isErrored === true || s.errored === true || backendState?.status === 'error';
|
|
123
123
|
const domainError = typeof s.error === 'string' ? s.error.trim() : '';
|
|
124
124
|
const queryError = typeof backendState?.error === 'string' ? backendState.error.trim() : '';
|
|
125
|
-
|
|
125
|
+
// Some domains carry their failure sentence in `emptyText` (portfolio, podcasts);
|
|
126
|
+
// the screen folds that node into this block, so keep the domain's own words.
|
|
127
|
+
const domainEmptyError = errored && typeof s.emptyText === 'string' ? s.emptyText.trim() : '';
|
|
128
|
+
const errorMessage = queryError || domainError || domainEmptyError || copyParam('collection.loadError', 'This collection could not load. Try again.');
|
|
126
129
|
const canRetry = !!backend || typeof def.actions?.refresh === 'function' || typeof def.actions?.load === 'function' || typeof def.actions?.collectionRetry === 'function';
|
|
130
|
+
// Wix refused the visitor: retrying cannot help, signing in can.
|
|
131
|
+
const signInRequired = errored && s.signInRequired === true;
|
|
132
|
+
// "the source has nothing" and "your filter matched nothing" are
|
|
133
|
+
// different states. Only the second may offer "Clear filters", and the list chrome
|
|
134
|
+
// (view, refine, chips, counter, search) means nothing over a source with no rows.
|
|
135
|
+
// Loaded-row collections hold the whole source in `rows`, so zero rows IS an empty
|
|
136
|
+
// source. A backend query returns only matches, so zero rows is source-empty only
|
|
137
|
+
// when nothing narrowed it.
|
|
138
|
+
// Anything the customer (or the domain) narrowed the result by. While narrowed, zero rows
|
|
139
|
+
// is "your filter matched nothing" and the controls that got them here must stay.
|
|
140
|
+
// The DOMAIN's own filter (config.narrowedBy, e.g. blog category) narrowed the rows: the
|
|
141
|
+
// domain owns that empty state and its clear action. The shared "Clear filters" block
|
|
142
|
+
// cannot reset a domain filter, so it never shows for one.
|
|
143
|
+
const domainNarrowed = !!config.narrowedBy && !!s[config.narrowedBy];
|
|
127
144
|
// A default view is what the screen opens on; anything else is the member's own
|
|
128
145
|
// narrowing, which keeps the chrome (and the Clear that undoes it) on screen.
|
|
129
146
|
// "All" and the collection's own default (e.g. Upcoming) are both un-narrowed: Clear
|
|
130
147
|
// resets to "All", so showing it on either would offer a reset that changes nothing.
|
|
131
|
-
const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter);
|
|
132
|
-
const
|
|
148
|
+
const narrowed = !!prefs.query.trim() || (prefs.filter !== 'all' && prefs.filter !== initialCollection(config).filter) || domainNarrowed;
|
|
149
|
+
const sourceEmpty = !loading && !errored && rows.length === 0 &&
|
|
150
|
+
(!(backend && config.backendQuery) || !narrowed) && !domainNarrowed;
|
|
151
|
+
const emptySourceText = (typeof s.emptyText === 'string' && s.emptyText.trim()) || copyParam('collection.emptySource', 'Nothing here yet');
|
|
152
|
+
// Chrome is noise over a small un-narrowed list (fewer than COLLECTION_CHROME_MIN_ITEMS
|
|
153
|
+
// records, no further page), over an empty source, and before the FIRST load or an
|
|
154
|
+
// un-narrowed failure. A re-query while typing keeps it (and the search field's focus).
|
|
155
|
+
const hideChrome = (!narrowed && !s.nextCursor && projected.loaded < COLLECTION_CHROME_MIN_ITEMS)
|
|
156
|
+
|| sourceEmpty || ((loading || errored) && rows.length === 0 && !narrowed);
|
|
133
157
|
return { ...s, collection: { ...prefs, ...projected,
|
|
134
158
|
hideChrome, noActiveCriteria: !narrowed,
|
|
135
159
|
viewName, viewMessage, saveMessage, noSavedViews: prefs.savedViews.length === 0,
|
|
@@ -138,8 +162,15 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
138
162
|
noPageError: s.pageError !== true,
|
|
139
163
|
noOffline: s.fromCache !== true,
|
|
140
164
|
noError: !errored,
|
|
141
|
-
noRetry: !canRetry,
|
|
165
|
+
noRetry: !canRetry || signInRequired,
|
|
166
|
+
noSignIn: !signInRequired,
|
|
167
|
+
errorTitle: signInRequired ? copyParam('collection.signInTitle', 'Sign in to see this') : copyParam('collection.loadErrorTitle', 'This collection could not load'),
|
|
142
168
|
errorMessage,
|
|
169
|
+
noLoading: !loading,
|
|
170
|
+
// De-dup against a domain that paints its OWN empty block (screen-kit emptyBlock is
|
|
171
|
+
// gated on the domain's top-level `noEmpty`): only one empty state ever shows.
|
|
172
|
+
noEmptySource: !sourceEmpty || s.noEmpty === false,
|
|
173
|
+
emptySourceText,
|
|
143
174
|
hideList: prefs.view !== 'list', hideGrid: prefs.view !== 'grid',
|
|
144
175
|
hideComfortable: prefs.density !== 'comfortable', hideCompact: prefs.density !== 'compact',
|
|
145
176
|
selectingLabel: prefs.selecting ? copyParam('collection.done', 'Done') : copyParam('collection.select', 'Select'),
|
|
@@ -147,7 +178,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
147
178
|
cannotSaveSelected: !prefs.selectedIds.length || prefs.selectedIds.some(id => !rows.some(row => collectionId(row) === id)),
|
|
148
179
|
filtersPresented, previewPresented: !!previewId, preview: projected.items.filter(row => row.collectionId === previewId),
|
|
149
180
|
persistenceLabel: persisted.available ? copyParam('collection.onDevice', 'Saved on this device') : copyParam('collection.sessionOnly', 'Choices last for this session'),
|
|
150
|
-
noEmpty: loading || errored || projected.items.length > 0,
|
|
181
|
+
noEmpty: loading || errored || sourceEmpty || domainNarrowed || projected.items.length > 0,
|
|
151
182
|
summary: backend && config.backendQuery
|
|
152
183
|
? (s.fromCache ? copyParam('collection.cached', 'Saved offline · may be outdated')
|
|
153
184
|
: typeof s.total === 'number' ? `${projected.items.length} / ${s.total} ${copyParam('collection.results', 'results')}`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Empty-collection flags for descriptors.
|
|
2
|
+
//
|
|
3
|
+
// A bound Grid/LazyList with zero rows still renders a zero-height box, and a stack
|
|
4
|
+
// allocates its `spacing` between CHILDREN, not between painted children — so every
|
|
5
|
+
// empty list left a full gap of blank on the screen (the measure pass's `dead-gap`, on
|
|
6
|
+
// iOS and Android as much as on web). `hiddenWhen` hides on a TRUE scalar only, and an
|
|
7
|
+
// empty array is not one, so a descriptor cannot gate a list on its own rows.
|
|
8
|
+
//
|
|
9
|
+
// `withEmptyFlags(def, { rows: "noRows" })` derives `noRows = rows is empty` into the
|
|
10
|
+
// initial state and after EVERY action (sync or async), so a builder can write
|
|
11
|
+
// `hiddenWhen(grid(...), "$service:svc.noRows")` without each reducer remembering to.
|
|
12
|
+
type State = Record<string, unknown>;
|
|
13
|
+
type Action = (s: State, ...args: unknown[]) => unknown;
|
|
14
|
+
|
|
15
|
+
export function withEmptyFlags<D extends { state: object; actions?: object }>(def: D, flags: Record<string, string>): D {
|
|
16
|
+
const derive = <T>(s: T): T => {
|
|
17
|
+
if (!s || typeof s !== "object") return s;
|
|
18
|
+
const out = { ...(s as State) };
|
|
19
|
+
for (const [rows, flag] of Object.entries(flags)) {
|
|
20
|
+
const v = out[rows];
|
|
21
|
+
out[flag] = !Array.isArray(v) || v.length === 0;
|
|
22
|
+
}
|
|
23
|
+
return out as T;
|
|
24
|
+
};
|
|
25
|
+
const actions: Record<string, Action> = {};
|
|
26
|
+
for (const [name, action] of Object.entries((def.actions ?? {}) as Record<string, Action>)) {
|
|
27
|
+
actions[name] = (s, ...args) => {
|
|
28
|
+
const r = action(s, ...args);
|
|
29
|
+
return r && typeof (r as Promise<unknown>).then === "function" ? (r as Promise<unknown>).then(derive) : derive(r);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { ...def, state: derive(def.state), actions } as D;
|
|
33
|
+
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
// himi-branded-lite-badges —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// labelled "Sample content" banner. Real badges always win the moment any exist.
|
|
1
|
+
// himi-branded-lite-badges — reads the site's real Wix Badges catalog (query).
|
|
2
|
+
// a failed query is an error with Retry (or Sign in when Wix refuses the
|
|
3
|
+
// visitor) and an empty one is a real empty state — never sample rows presented as
|
|
4
|
+
// the customer's own. Samples appear only in the host's explicit sample preview
|
|
5
|
+
// (_shared/branded-load-state.ts).
|
|
7
6
|
import { defineService } from "../_shared/collection-service.js";
|
|
7
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
8
8
|
// v4 (the live Badges service) has no plain GET list — only the query endpoint;
|
|
9
9
|
// GET /badges/v4/badges 404s. v1-v3 are back-compat proxies that 404 here too.
|
|
10
10
|
import { query as queryBadges, type WixBadge } from "@himalaya/wix-badges";
|
|
11
11
|
|
|
12
12
|
interface Row { id: string; title: string; subtitle: string }
|
|
13
|
-
interface S {
|
|
14
|
-
status: "loading" | "ready";
|
|
13
|
+
interface S extends LoadFlags {
|
|
15
14
|
badges: Row[]; emptyText: string;
|
|
16
15
|
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
17
16
|
* shows only when the site really has none, and the unavailable one only when the
|
|
@@ -32,21 +31,26 @@ const MOCK: Row[] = [
|
|
|
32
31
|
{ id: "sample-vip", title: "VIP", subtitle: "A valued long-standing customer." },
|
|
33
32
|
];
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
35
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
36
|
+
const COPY: LoadCopy<Row> = {
|
|
37
|
+
prefix: "badges",
|
|
38
|
+
empty: "No badges yet.",
|
|
39
|
+
error: "Badges couldn't be loaded.",
|
|
40
|
+
signIn: "Sign in to see community badges.",
|
|
41
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
42
|
+
};
|
|
38
43
|
|
|
39
44
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedBadges", {
|
|
40
|
-
state: {
|
|
45
|
+
state: { ...LOADING, badges: [], emptyText: "" },
|
|
41
46
|
actions: {
|
|
42
47
|
load: async (s) => {
|
|
43
48
|
try {
|
|
44
49
|
const r = await queryBadges({ query: { cursorPaging: { limit: 50 } } });
|
|
45
50
|
const badges = (r.badges ?? []).map(norm);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return mockState(s, "unavailable");
|
|
51
|
+
return { ...s, ...loaded("badges", badges, COPY) };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ...s, ...loadFailed("badges", err, COPY) };
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
},
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
// himi-branded-lite-coupons —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// "Sample content" banner. Real data always wins the moment coupons exist.
|
|
1
|
+
// himi-branded-lite-coupons — reads the site's real Wix Coupons (queryCoupons).
|
|
2
|
+
// a failed query is an error with Retry (or Sign in when Wix refuses the
|
|
3
|
+
// visitor) and an empty one is a real empty state — never sample rows presented as
|
|
4
|
+
// the customer's own. Samples appear only in the host's explicit sample preview
|
|
5
|
+
// (_shared/branded-load-state.ts).
|
|
7
6
|
import { defineService } from "../_shared/collection-service.js";
|
|
7
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
8
8
|
import { queryCoupons, type WixCoupon } from "@himalaya/wix-coupons";
|
|
9
9
|
|
|
10
10
|
interface Row { id: string; name: string; subtitle: string }
|
|
11
|
-
interface S {
|
|
12
|
-
status: "loading" | "ready";
|
|
11
|
+
interface S extends LoadFlags {
|
|
13
12
|
coupons: Row[]; emptyText: string;
|
|
14
13
|
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
15
14
|
* shows only when the site really has none, and the unavailable one only when the
|
|
@@ -30,25 +29,26 @@ const MOCK: Row[] = [
|
|
|
30
29
|
{ id: "sample-loyal10", name: "Loyalty reward — $10 off", subtitle: "Code: LOYAL10 · members only" },
|
|
31
30
|
];
|
|
32
31
|
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
33
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
34
|
+
const COPY: LoadCopy<Row> = {
|
|
35
|
+
prefix: "coupons",
|
|
36
|
+
empty: "No offers right now — check back soon.",
|
|
37
|
+
error: "Offers couldn't be loaded.",
|
|
38
|
+
signIn: "Sign in to see offers available to you.",
|
|
39
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
40
|
+
};
|
|
40
41
|
|
|
41
42
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedCoupons", {
|
|
42
|
-
state: {
|
|
43
|
+
state: { ...LOADING, coupons: [], emptyText: "" },
|
|
43
44
|
actions: {
|
|
44
45
|
load: async (s) => {
|
|
45
46
|
try {
|
|
46
47
|
const r = await queryCoupons({ paging: { limit: 30 } } as never);
|
|
47
48
|
const coupons = (((r as unknown as { coupons?: WixCoupon[] }).coupons) ?? []).map(norm);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return mockState(s, "unavailable");
|
|
49
|
+
return { ...s, ...loaded("coupons", coupons, COPY) };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { ...s, ...loadFailed("coupons", err, COPY) };
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
},
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
// himi-branded-lite-donations —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// labelled "Sample content" banner. Real campaigns always win the moment any exist.
|
|
1
|
+
// himi-branded-lite-donations — reads the site's real Wix Donations campaigns (queryDonationCampaigns).
|
|
2
|
+
// a failed query is an error with Retry (or Sign in when Wix refuses the
|
|
3
|
+
// visitor) and an empty one is a real empty state — never sample rows presented as
|
|
4
|
+
// the customer's own. Samples appear only in the host's explicit sample preview
|
|
5
|
+
// (_shared/branded-load-state.ts).
|
|
7
6
|
import { defineService } from "../_shared/collection-service.js";
|
|
7
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
8
8
|
import { queryDonationCampaigns, type WixDonationCampaign } from "@himalaya/wix-donations";
|
|
9
9
|
|
|
10
10
|
interface Row { id: string; title: string; subtitle: string }
|
|
11
|
-
interface S {
|
|
12
|
-
status: "loading" | "ready";
|
|
11
|
+
interface S extends LoadFlags {
|
|
13
12
|
campaigns: Row[]; emptyText: string;
|
|
14
13
|
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
15
14
|
* shows only when the site really has none, and the unavailable one only when the
|
|
@@ -40,21 +39,26 @@ const MOCK: Row[] = [
|
|
|
40
39
|
{ id: "sample-relief", title: "Winter Relief Fund", subtitle: "Goal: $5,000 · Goal reached" },
|
|
41
40
|
];
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
43
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
44
|
+
const COPY: LoadCopy<Row> = {
|
|
45
|
+
prefix: "donations",
|
|
46
|
+
empty: "No donation campaigns right now.",
|
|
47
|
+
error: "Donation campaigns couldn't be loaded.",
|
|
48
|
+
signIn: "Sign in to see donation campaigns.",
|
|
49
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
50
|
+
};
|
|
46
51
|
|
|
47
52
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedDonations", {
|
|
48
|
-
state: {
|
|
53
|
+
state: { ...LOADING, campaigns: [], emptyText: "" },
|
|
49
54
|
actions: {
|
|
50
55
|
load: async (s) => {
|
|
51
56
|
try {
|
|
52
57
|
const r = await queryDonationCampaigns({ query: { cursorPaging: { limit: 20 } } });
|
|
53
58
|
const campaigns = (r.donationCampaigns ?? []).filter((c) => c.archived !== true).map(norm);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return mockState(s, "unavailable");
|
|
59
|
+
return { ...s, ...loaded("campaigns", campaigns, COPY) };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { ...s, ...loadFailed("campaigns", err, COPY) };
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
},
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
// himi-branded-lite-forms —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// there would state "this site has none yet", which the app never established.
|
|
9
|
-
// Real forms always win the moment any exist.
|
|
1
|
+
// himi-branded-lite-forms — reads the site's real Wix Forms (queryForms, which
|
|
2
|
+
// REQUIRES a namespace filter — a bare query is answered `400 MISSING_NAMESPACE_FILTER`
|
|
3
|
+
// for every site, #2769).
|
|
4
|
+
// a failed query is an error with Retry (or Sign in when Wix refuses the
|
|
5
|
+
// visitor) and an empty one is a real empty state — never sample rows presented as
|
|
6
|
+
// the customer's own. Samples appear only in the host's explicit sample preview
|
|
7
|
+
// (_shared/branded-load-state.ts).
|
|
10
8
|
import { defineService } from "../_shared/collection-service.js";
|
|
9
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
11
10
|
import { queryForms, type WixForm } from "@himalaya/wix-forms";
|
|
12
11
|
|
|
13
12
|
interface Row { id: string; title: string; subtitle: string }
|
|
14
|
-
interface S {
|
|
15
|
-
status: "loading" | "ready" | "error";
|
|
13
|
+
interface S extends LoadFlags {
|
|
16
14
|
forms: Row[]; emptyText: string;
|
|
17
15
|
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
18
16
|
* shows only when the site really has none, and the unavailable one only when the
|
|
@@ -35,39 +33,33 @@ function norm(f: WixForm): Row {
|
|
|
35
33
|
return { id: f.id || "form", title, subtitle: parts.join(" · ") };
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
/**
|
|
39
|
-
* them is a statement about the customer's site.
|
|
40
|
-
*
|
|
41
|
-
* There used to be one banner, "this site has no forms yet", shown whenever the rows were
|
|
42
|
-
* samples — including when the query had FAILED. A site WITH forms got told it had none.
|
|
43
|
-
* The sample fallback itself is deliberate and covered by the renderer's offline test,
|
|
44
|
-
* which keeps these verticals showing labelled sample content rather than a blank screen.
|
|
45
|
-
* So what was wrong was never the samples — it was claiming to know something the app had
|
|
46
|
-
* not established. */
|
|
36
|
+
/** Preview-only sample forms (see COPY below). */
|
|
47
37
|
const MOCK: Row[] = [
|
|
48
38
|
{ id: "sample-contact", title: "Contact Us", subtitle: "4 fields · Published" },
|
|
49
39
|
{ id: "sample-subscribe", title: "Newsletter Signup", subtitle: "2 fields · Published" },
|
|
50
40
|
{ id: "sample-feedback", title: "Feedback", subtitle: "5 fields · Published" },
|
|
51
41
|
];
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
44
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
45
|
+
const COPY: LoadCopy<Row> = {
|
|
46
|
+
prefix: "forms",
|
|
47
|
+
empty: "No forms available yet.",
|
|
48
|
+
error: "Forms couldn't be loaded.",
|
|
49
|
+
signIn: "Sign in to see available forms.",
|
|
50
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
51
|
+
};
|
|
56
52
|
|
|
57
53
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedForms", {
|
|
58
|
-
state: {
|
|
54
|
+
state: { ...LOADING, forms: [], emptyText: "" },
|
|
59
55
|
actions: {
|
|
60
56
|
load: async (s) => {
|
|
61
57
|
try {
|
|
62
58
|
const r = await queryForms({ filter: { namespace: FORMS_NAMESPACE }, paging: { limit: 50 } });
|
|
63
59
|
const forms = (r.forms ?? []).map(norm);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// The samples still render — a blank screen helps nobody and this vertical
|
|
68
|
-
// deliberately ships a fallback — but the banner no longer asserts the site is
|
|
69
|
-
// empty, because a failed query established no such thing.
|
|
70
|
-
return mockState(s, "unavailable");
|
|
60
|
+
return { ...s, ...loaded("forms", forms, COPY) };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { ...s, ...loadFailed("forms", err, COPY) };
|
|
71
63
|
}
|
|
72
64
|
},
|
|
73
65
|
},
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
// codegen — the real consumer app (wix-private/wix-one-app-pro-gallery) fetches it
|
|
5
5
|
// from the photography one-app viewer REST endpoint. So this worker mirrors that: it
|
|
6
6
|
// GETs the site's galleries from that endpoint through the raw wixApi bridge (absolute
|
|
7
|
-
// URL + the app's WixApiCapability auth)
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// URL + the app's WixApiCapability auth). a failed query is an error with
|
|
8
|
+
// Retry (or Sign in), an empty one a real empty state; sample galleries appear only
|
|
9
|
+
// in the host's explicit sample preview (_shared/branded-load-state.ts).
|
|
10
10
|
//
|
|
11
11
|
// Endpoint verbatim from wix-one-app-pro-gallery/src/constants (API_BASE_URL) +
|
|
12
12
|
// api/photo-gallery-api.js (fetchGalleries).
|
|
13
13
|
import { defineService } from "../_shared/collection-service.js";
|
|
14
|
+
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
14
15
|
|
|
15
16
|
const GALLERIES_URL =
|
|
16
17
|
"https://www.wix.com/_api/photography-one-app-viewer/galleries?itemsPerGallery=1&offset=0&limit=30&withHidden=false&useNew=true";
|
|
@@ -22,8 +23,7 @@ function wixApi(): WixApi | null {
|
|
|
22
23
|
|
|
23
24
|
interface RawGallery { id?: string; name?: string; totalItemsCount?: number }
|
|
24
25
|
interface Row { id: string; title: string; subtitle: string }
|
|
25
|
-
interface S {
|
|
26
|
-
status: "loading" | "ready";
|
|
26
|
+
interface S extends LoadFlags {
|
|
27
27
|
galleries: Row[]; emptyText: string;
|
|
28
28
|
isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
|
|
29
29
|
* shows only when the site really has none, and the unavailable one only when the
|
|
@@ -45,17 +45,23 @@ const MOCK: Row[] = [
|
|
|
45
45
|
{ id: "sample-team", title: "The Team", subtitle: "9 photos" },
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
/** sample rows reach the screen ONLY in the host's explicit sample preview;
|
|
49
|
+
* a failed or empty query is an honest error / empty state (branded-load-state.ts). */
|
|
50
|
+
const COPY: LoadCopy<Row> = {
|
|
51
|
+
prefix: "galleries",
|
|
52
|
+
empty: "No galleries yet — check back soon.",
|
|
53
|
+
error: "Galleries couldn't be loaded.",
|
|
54
|
+
signIn: "Sign in to see galleries.",
|
|
55
|
+
sample: () => MOCK.map((r) => ({ ...r })),
|
|
56
|
+
};
|
|
51
57
|
|
|
52
58
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedGalleries", {
|
|
53
|
-
state: {
|
|
59
|
+
state: { ...LOADING, galleries: [], emptyText: "" },
|
|
54
60
|
actions: {
|
|
55
61
|
load: async (s) => {
|
|
56
62
|
try {
|
|
57
63
|
const api = wixApi();
|
|
58
|
-
if (!api) return
|
|
64
|
+
if (!api) return { ...s, ...loadFailed("galleries", new Error("Wix bridge unavailable"), COPY) };
|
|
59
65
|
const res = (await api.call("GET", GALLERIES_URL)) as
|
|
60
66
|
{ galleries?: RawGallery[] } | RawGallery[] | { data?: { galleries?: RawGallery[] } };
|
|
61
67
|
// Accept {galleries:[...]}, a top-level array, or a {data:{galleries}} envelope.
|
|
@@ -64,10 +70,9 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("himiBrandedGalleri
|
|
|
64
70
|
(Array.isArray(res) ? (res as RawGallery[]) : undefined) ??
|
|
65
71
|
(res as { data?: { galleries?: RawGallery[] } })?.data?.galleries ?? [];
|
|
66
72
|
const galleries = raw.map(norm);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return mockState(s, "unavailable");
|
|
73
|
+
return { ...s, ...loaded("galleries", galleries, COPY) };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { ...s, ...loadFailed("galleries", err, COPY) };
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
},
|