@wix/himalaya-cli 0.1321.0 → 0.1322.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/index.d.ts +16 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +21 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview-page.mjs +78 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview.mjs +16 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +13 -4
- package/dist/cli.mjs +25 -0
- package/guides/sdk-reference.json +33 -3
- package/guides/web-page-native-sdk.md +154 -33
- package/guides/webview-page-sdk.md +11 -0
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +492 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/branded-lite/dev/index.ts +492 -0
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/gym-fitness/dev/index.ts +492 -0
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/portfolio-creative/dev/index.ts +492 -0
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/retail-store/dev/index.ts +492 -0
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Collection detail (ON-DEVICE, site-scoped).
|
|
2
2
|
// Prefer Catalog V3 `getStoreCategory`; V1 `getCollection` 428s on CATALOG_V3 sites.
|
|
3
|
+
//
|
|
4
|
+
// #4155 — the screen showed the category header ("N products") and never the products.
|
|
5
|
+
// Once the category resolves, its products load through `searchStoreProducts({ categoryId })`
|
|
6
|
+
// with the same row shape as `himi-store-shop`, so a category reads as a filtered Shop.
|
|
7
|
+
//
|
|
8
|
+
// Two lifecycles, deliberately kept apart:
|
|
9
|
+
// • the CATEGORY — presence flags (loading / found / missing / failed), as every detail screen;
|
|
10
|
+
// • its PRODUCTS — a cursor-paged list (`createCollectionQuery`) with its own loading, failed
|
|
11
|
+
// and empty states. A product read that fails leaves a FOUND category found: the header
|
|
12
|
+
// stays and the list offers a retry. It never turns into "Category not found".
|
|
3
13
|
import { defineService } from "@himalaya/state";
|
|
4
14
|
import {
|
|
5
15
|
categoryCount,
|
|
@@ -8,46 +18,152 @@ import {
|
|
|
8
18
|
type WixCollection,
|
|
9
19
|
} from "@himalaya/wix-stores-collections";
|
|
10
20
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
21
|
+
import { createCollectionQuery, type CollectionQueryState } from "../_shared/collection-query.js";
|
|
22
|
+
import { productImage, productPrice, searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
23
|
+
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
11
24
|
|
|
12
|
-
interface
|
|
25
|
+
interface Row {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
image: string;
|
|
30
|
+
initial: string;
|
|
31
|
+
hasImage: boolean;
|
|
32
|
+
noImage: boolean;
|
|
33
|
+
detailScreenId: string;
|
|
34
|
+
}
|
|
13
35
|
|
|
36
|
+
interface S extends PresenceFlags {
|
|
37
|
+
status: "loading" | "ready";
|
|
38
|
+
name: string;
|
|
39
|
+
subtitle: string;
|
|
40
|
+
description: string;
|
|
41
|
+
/** Which `load` the committed state belongs to — a products snapshot is only ever
|
|
42
|
+
* projected onto the category it was fetched for. */
|
|
43
|
+
loadId: number;
|
|
44
|
+
products: Row[];
|
|
45
|
+
/** Load-more cursor — non-empty while another page exists and none is in flight. */
|
|
46
|
+
productsCursor: string;
|
|
47
|
+
hideProducts: boolean;
|
|
48
|
+
hideProductsLoading: boolean;
|
|
49
|
+
/** Gates the products retry block (the category's own failure is `hideLoadError`). */
|
|
50
|
+
productsNotErrored: boolean;
|
|
51
|
+
/** Gates the "next page failed" retry under the list. A failed load-more keeps the rows
|
|
52
|
+
* and clears the cursor, so without this the list would just stop and read as complete. */
|
|
53
|
+
productsNoPageError: boolean;
|
|
54
|
+
/** One plain line for a screen that has no dedicated empty / error blocks ("" otherwise). */
|
|
55
|
+
productsMessage: string;
|
|
56
|
+
/** Gates the "no products in this category" block (screen-kit `emptyBlock` reads `noEmpty`). */
|
|
57
|
+
noEmpty: boolean;
|
|
58
|
+
}
|
|
14
59
|
|
|
15
|
-
|
|
16
|
-
|
|
60
|
+
/** The same row `himi-store-shop` emits, so the screen reuses the Shop tile. */
|
|
61
|
+
function row(p: StoreProduct): Row {
|
|
62
|
+
const name = cleanTitle(p.name) || "Product";
|
|
63
|
+
const image = productImage(p);
|
|
64
|
+
return {
|
|
65
|
+
id: p.id ?? "",
|
|
66
|
+
name,
|
|
67
|
+
subtitle: productPrice(p),
|
|
68
|
+
image,
|
|
69
|
+
initial: initials(name),
|
|
70
|
+
hasImage: !!image,
|
|
71
|
+
noImage: !image,
|
|
72
|
+
detailScreenId: "himi_product",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let categoryId = "";
|
|
77
|
+
let loadId = 0;
|
|
78
|
+
const products = createCollectionQuery<Row>({
|
|
79
|
+
id: (item) => item.id,
|
|
80
|
+
async fetch(_criteria, cursor) {
|
|
81
|
+
const response = await searchStoreProducts({ categoryId, limit: 50, cursor: cursor || undefined });
|
|
82
|
+
return {
|
|
83
|
+
items: (response.products ?? []).map(row).filter((item) => !!item.id),
|
|
84
|
+
nextCursor: response.pagingMetadata?.cursors?.next ?? "",
|
|
85
|
+
total: response.pagingMetadata?.total,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
}, () => { void svc.productsChanged(); });
|
|
89
|
+
|
|
90
|
+
/** Project the products snapshot onto a state whose category is FOUND. Anything else
|
|
91
|
+
* (loading, missing, failed) shows no product UI at all. */
|
|
92
|
+
function withProducts(s: S, page: CollectionQueryState<Row> = products.getState()): S {
|
|
93
|
+
const found = s.found;
|
|
94
|
+
const items = found ? page.items : [];
|
|
95
|
+
const loading = page.status === "idle" || page.status === "loading";
|
|
96
|
+
return {
|
|
97
|
+
...s,
|
|
98
|
+
products: items,
|
|
99
|
+
productsCursor: found && !page.pageError && !page.loadingMore ? page.nextCursor : "",
|
|
100
|
+
hideProducts: items.length === 0,
|
|
101
|
+
hideProductsLoading: !(found && loading && items.length === 0),
|
|
102
|
+
productsNotErrored: !(found && page.status === "error"),
|
|
103
|
+
productsNoPageError: !(found && page.pageError),
|
|
104
|
+
noEmpty: !(found && page.status === "ready" && items.length === 0),
|
|
105
|
+
productsMessage: !found ? ""
|
|
106
|
+
: page.status === "error" || page.pageError ? "Products couldn't load. Pull to refresh to try again."
|
|
107
|
+
: page.status === "ready" && items.length === 0 ? "No products in this category yet." : "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const NO_PRODUCTS = { products: [], productsCursor: "", hideProducts: true, hideProductsLoading: true,
|
|
112
|
+
productsNotErrored: true, productsNoPageError: true, noEmpty: true, productsMessage: "" };
|
|
113
|
+
|
|
114
|
+
const initial: S = { status: "loading", name: "", subtitle: "", description: "", loadId: 0, ...NO_PRODUCTS, ...presence("loading") };
|
|
115
|
+
|
|
116
|
+
/** The category header for `id`: V3 category first, V1 collection as the fallback. */
|
|
117
|
+
async function readCategory(id: string): Promise<Pick<S, "name" | "subtitle" | "description">> {
|
|
118
|
+
try {
|
|
119
|
+
const c = (await getStoreCategory(id)).category;
|
|
120
|
+
if (c?.id || c?.name) return { name: c.name || "Category", subtitle: categoryCount(c), description: c.description || "" };
|
|
121
|
+
} catch { /* fall through to V1 */ }
|
|
122
|
+
const res = await getCollection(id);
|
|
123
|
+
const c = (((res as unknown as { collection?: WixCollection }).collection) ?? (res as unknown as WixCollection)) as unknown as {
|
|
124
|
+
name?: string; numberOfProducts?: number; description?: string;
|
|
125
|
+
};
|
|
126
|
+
const n = c.numberOfProducts;
|
|
127
|
+
return {
|
|
128
|
+
name: c.name || "Collection",
|
|
129
|
+
subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "",
|
|
130
|
+
description: c.description || "",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const svc = defineService<S, {
|
|
135
|
+
load: (s: S) => Promise<S | undefined>;
|
|
136
|
+
productsChanged: (s: S) => S | undefined;
|
|
137
|
+
loadMoreProducts: (s: S) => Promise<undefined>;
|
|
138
|
+
retryProducts: (s: S) => Promise<undefined>;
|
|
139
|
+
}>("spacesCollectionDetail", {
|
|
140
|
+
state: initial,
|
|
141
|
+
// A cold boot must not paint another category's products from last run's snapshot.
|
|
142
|
+
notHydrated: ["products", "productsCursor", "hideProducts", "hideProductsLoading", "productsNotErrored", "productsNoPageError", "noEmpty", "productsMessage", "loadId"],
|
|
17
143
|
actions: {
|
|
18
144
|
load: async (s) => {
|
|
145
|
+
const run = ++loadId;
|
|
19
146
|
const id = detailInputId();
|
|
20
|
-
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
21
|
-
|
|
22
|
-
const res = await getStoreCategory(id);
|
|
23
|
-
const c = res.category;
|
|
24
|
-
if (c?.id || c?.name) {
|
|
25
|
-
return {
|
|
26
|
-
status: "ready",
|
|
27
|
-
name: c.name || "Category",
|
|
28
|
-
subtitle: categoryCount(c),
|
|
29
|
-
description: c.description || "",
|
|
30
|
-
...presence("found"),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
} catch { /* fall through to V1 */ }
|
|
147
|
+
if (!id) return { ...s, ...NO_PRODUCTS, loadId: run, status: "ready", ...presence("missing") };
|
|
148
|
+
let header: Pick<S, "name" | "subtitle" | "description">;
|
|
34
149
|
try {
|
|
35
|
-
|
|
36
|
-
const c = (((res as unknown as { collection?: WixCollection }).collection) ?? (res as unknown as WixCollection)) as unknown as {
|
|
37
|
-
name?: string; numberOfProducts?: number; description?: string;
|
|
38
|
-
};
|
|
39
|
-
const n = c.numberOfProducts;
|
|
40
|
-
return {
|
|
41
|
-
status: "ready",
|
|
42
|
-
name: c.name || "Collection",
|
|
43
|
-
subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "",
|
|
44
|
-
description: c.description || "",
|
|
45
|
-
...presence("found"),
|
|
46
|
-
};
|
|
150
|
+
header = await readCategory(id);
|
|
47
151
|
} catch (e) {
|
|
48
|
-
|
|
152
|
+
if (run !== loadId) return undefined;
|
|
153
|
+
return { ...s, ...NO_PRODUCTS, loadId: run, status: "ready", ...presenceAfterError(e) };
|
|
49
154
|
}
|
|
155
|
+
// A newer load (onAppear after the boot load, pull-to-refresh) owns the screen now.
|
|
156
|
+
if (run !== loadId) return undefined;
|
|
157
|
+
categoryId = id;
|
|
158
|
+
// Starts the first page; its snapshots land through `productsChanged`, which ignores
|
|
159
|
+
// them until THIS state (stamped `run`) is committed. The "loading" snapshot is read
|
|
160
|
+
// synchronously below, so the header paints with the products skeleton under it.
|
|
161
|
+
void products.query({});
|
|
162
|
+
return withProducts({ ...s, ...header, loadId: run, status: "ready", ...presence("found") });
|
|
50
163
|
},
|
|
164
|
+
productsChanged: (s) => (s.loadId === loadId && s.found ? withProducts(s) : undefined),
|
|
165
|
+
loadMoreProducts: async () => { await products.loadMore(); return undefined; },
|
|
166
|
+
retryProducts: async () => { await products.retry(); return undefined; },
|
|
51
167
|
},
|
|
52
168
|
});
|
|
53
169
|
svc.load();
|