@wix/himalaya-cli 0.1483.0 → 0.1485.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 +27 -28
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/dynamic-list-t5.descriptor.json +14 -0
- package/guides/dynamic-list.service.ts +48 -18
- package/guides/form-submit-t5.descriptor.json +1 -2
- package/guides/form-submit.service.ts +42 -22
- package/guides/recipe-four-state.md +2 -2
- package/guides/search-list-t5.descriptor.json +14 -0
- package/guides/search-list.service.ts +48 -17
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/beauty-salon-owner/dev/index.ts +58 -19
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/branded-lite/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/community-club/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/community-club/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/community-club-owner/dev/index.ts +58 -19
- package/templates/community-club-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/events-meetups/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/events-meetups-owner/dev/index.ts +46 -16
- package/templates/events-meetups-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/gym-fitness/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/gym-fitness-owner/dev/index.ts +62 -20
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/nonprofit-cause/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +50 -17
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/owner-lite/dev/index.ts +62 -20
- package/templates/owner-lite/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/podcast-creator/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/podcast-creator-owner/dev/index.ts +58 -19
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/portfolio-creative/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/portfolio-creative-owner/dev/index.ts +58 -19
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/restaurant/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/restaurant-owner/dev/index.ts +62 -20
- package/templates/restaurant-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/retail-store/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/retail-store-owner/dev/index.ts +54 -18
- package/templates/retail-store-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
- package/templates/yoga-studio/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/yoga-studio-owner/dev/index.ts +50 -17
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-query.ts +13 -4
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +2 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +5 -3
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/logic.ts +30 -0
package/dist/cli.mjs
CHANGED
|
@@ -22144,7 +22144,6 @@ function resolveIdiom(tokens2, ctx = {}) {
|
|
|
22144
22144
|
var IDIOM_CONTEXTS, UNSAFE_KEYS;
|
|
22145
22145
|
var init_idiom = __esm({
|
|
22146
22146
|
"../../core/ts/src/idiom.ts"() {
|
|
22147
|
-
"use strict";
|
|
22148
22147
|
IDIOM_CONTEXTS = {
|
|
22149
22148
|
ios: { native: ["ios"] },
|
|
22150
22149
|
android: { native: ["material"] },
|
|
@@ -164028,33 +164027,33 @@ var init_preview_assets_generated = __esm({
|
|
|
164028
164027
|
"../../packages/serve/src/preview-assets.generated.ts"() {
|
|
164029
164028
|
init_preview_assets_format();
|
|
164030
164029
|
PREVIEW_ASSET_MANIFEST = [
|
|
164031
|
-
{ key: "assets/DashboardHost-
|
|
164032
|
-
{ key: "assets/DevDrawer-
|
|
164033
|
-
{ key: "assets/IdiomCompare-
|
|
164034
|
-
{ key: "assets/LightboxViewer-
|
|
164035
|
-
{ key: "assets/PhonePreviewControl-
|
|
164036
|
-
{ key: "assets/SchemeCompare-
|
|
164037
|
-
{ key: "assets/index-
|
|
164038
|
-
{ key: "assets/pdf-
|
|
164039
|
-
{ key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset:
|
|
164040
|
-
{ key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset:
|
|
164041
|
-
{ key: "assets/pdfDocument-
|
|
164042
|
-
{ key: "assets/pdfFetch-Cc8i1I1h.js", contentType: "application/javascript; charset=utf-8", offset:
|
|
164043
|
-
{ key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset:
|
|
164044
|
-
{ key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset:
|
|
164045
|
-
{ key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset:
|
|
164046
|
-
{ key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
164047
|
-
{ key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset:
|
|
164048
|
-
{ key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset:
|
|
164049
|
-
{ key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
164050
|
-
{ key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset:
|
|
164051
|
-
{ key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
164052
|
-
{ key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset:
|
|
164053
|
-
{ key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset:
|
|
164054
|
-
{ key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
164055
|
-
{ key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset:
|
|
164056
|
-
{ key: "index.html", contentType: "text/html; charset=utf-8", offset:
|
|
164057
|
-
{ key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset:
|
|
164030
|
+
{ key: "assets/DashboardHost-BfevkfEz.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 2916, sha256: "3135cfb42f0e017ea81ac5af459e6ddcd7d7acdf1a9252728753236da4b3e1ab" },
|
|
164031
|
+
{ key: "assets/DevDrawer-DqyYJOb7.js", contentType: "application/javascript; charset=utf-8", offset: 2924, length: 16744, sha256: "9376e38332ec3295aee67daaf6ece7db9374f551f67066ad34748f6ec783c10d" },
|
|
164032
|
+
{ key: "assets/IdiomCompare-CpSHyXxC.js", contentType: "application/javascript; charset=utf-8", offset: 19668, length: 1892, sha256: "1dd97adb953b1ce57ebd61c2ce925ef4a218897da3e795b469f667c4278b44c9" },
|
|
164033
|
+
{ key: "assets/LightboxViewer-C6ZoGE02.js", contentType: "application/javascript; charset=utf-8", offset: 21560, length: 8132, sha256: "c11138b57bf2636daf98529d47875b59ed5accb8e22515fb12bc30c6ba891aa8" },
|
|
164034
|
+
{ key: "assets/PhonePreviewControl-DPGmBrY5.js", contentType: "application/javascript; charset=utf-8", offset: 29692, length: 37729, sha256: "97447fd0ef1a22bfd03d222f41e9bf7ff8fc60ea9261af2839a94ec35ad80f1e" },
|
|
164035
|
+
{ key: "assets/SchemeCompare-AHp-TFH2.js", contentType: "application/javascript; charset=utf-8", offset: 67421, length: 1824, sha256: "a62ddf48c0aa0ae42fea27f843d07309cf0bb3c44577e9e587dcae44056f7e9a" },
|
|
164036
|
+
{ key: "assets/index-B3HKN-kN.js", contentType: "application/javascript; charset=utf-8", offset: 69245, length: 1306186, sha256: "95f0ef9be576d2d31acccdfe85ec4edd60a8cf16cb36c1b7036655f0fafc1c6a" },
|
|
164037
|
+
{ key: "assets/pdf-CG4Vrwhx.js", contentType: "application/javascript; charset=utf-8", offset: 1375431, length: 488199, sha256: "d6149c33d78779cd89c8d71390bd46411e0dbe890514d82014d11724c3ebd8e6" },
|
|
164038
|
+
{ key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 1863630, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
|
|
164039
|
+
{ key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 1863703, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
|
|
164040
|
+
{ key: "assets/pdfDocument-CCd9DiA-.js", contentType: "application/javascript; charset=utf-8", offset: 3180737, length: 3379, sha256: "eb252efa4427144dec62e092acfee92390ef0d895d60a283bcc892e52822de75" },
|
|
164041
|
+
{ key: "assets/pdfFetch-Cc8i1I1h.js", contentType: "application/javascript; charset=utf-8", offset: 3184116, length: 2266, sha256: "1d21ad008921942217c9eac0c35b1713773b9d89c71e3ab4412781f7bb88f463" },
|
|
164042
|
+
{ key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3186382, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
|
|
164043
|
+
{ key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3190992, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
|
|
164044
|
+
{ key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3679576, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
|
|
164045
|
+
{ key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3770500, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
|
|
164046
|
+
{ key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3861484, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
|
|
164047
|
+
{ key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3952464, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
|
|
164048
|
+
{ key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 4043336, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
|
|
164049
|
+
{ key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4134316, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
|
|
164050
|
+
{ key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4242952, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
|
|
164051
|
+
{ key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4352036, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
|
|
164052
|
+
{ key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4461084, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
|
|
164053
|
+
{ key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4568784, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
|
|
164054
|
+
{ key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset: 4656892, length: 6748, sha256: "4a92459977d7cefa1a6a7e020c3a63509737f083b3f2773557c1a5771665811a" },
|
|
164055
|
+
{ key: "index.html", contentType: "text/html; charset=utf-8", offset: 4663640, length: 4947, sha256: "6805e797b2310186da4e8e2a785fe72899be298d586bda3165b2ff1ce5adcba8" },
|
|
164056
|
+
{ key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4668587, length: 17786, sha256: "084107ca997628c248f1b0ba3a7494744850df39fad813546ab8bdb82a6058da" }
|
|
164058
164057
|
];
|
|
164059
164058
|
}
|
|
164060
164059
|
});
|
|
Binary file
|
|
@@ -83,6 +83,20 @@
|
|
|
83
83
|
"hiddenPath": "$service:products.hideServerError"
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"id": "error.membership",
|
|
88
|
+
"body": {
|
|
89
|
+
"package": "himalaya",
|
|
90
|
+
"type": "EmptyState",
|
|
91
|
+
"props": {
|
|
92
|
+
"title": "Join this site to view products",
|
|
93
|
+
"subtitle": "Ask the owner of this store to add you as a member."
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"modifiers": {
|
|
97
|
+
"hiddenPath": "$service:products.hideMembershipError"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
86
100
|
{
|
|
87
101
|
"id": "error.permission",
|
|
88
102
|
"body": {
|
|
@@ -24,19 +24,21 @@ interface ProductRow {
|
|
|
24
24
|
initial: string; // derived presentation field (avatar letter)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Every class the recipe's classifier returns …
|
|
28
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
29
|
+
// … and the ones this screen shows: it has no sign-in flow (see shownKind()).
|
|
30
|
+
type ShownKind = Exclude<ErrorKind, "" | "signIn">;
|
|
30
31
|
|
|
31
32
|
interface ProductsState {
|
|
32
33
|
status: "loading" | "ready" | "error";
|
|
33
34
|
items: ProductRow[]; // ← itemsPath binds here; MUST initialize as an array
|
|
34
|
-
errorKind:
|
|
35
|
+
errorKind: "" | ShownKind; // why the last load failed; "" unless status is "error"
|
|
35
36
|
// hiddenPath hides a node when its flag is TRUE.
|
|
36
37
|
hideLoading: boolean; // skeleton: first load, nothing on screen
|
|
37
38
|
notEmpty: boolean; // EmptyState: ready with zero rows
|
|
38
39
|
hideOfflineError: boolean; // one EmptyState per error class …
|
|
39
40
|
hideServerError: boolean;
|
|
41
|
+
hideMembershipError: boolean;
|
|
40
42
|
hidePermissionError: boolean;
|
|
41
43
|
hideRefreshError: boolean; // … and a Banner when a refresh fails over kept rows
|
|
42
44
|
selectedId: string; // row identity from onTap params — drive a detail view from it
|
|
@@ -62,7 +64,7 @@ function normalize(raw: StoreProduct): ProductRow {
|
|
|
62
64
|
function shape(s: Partial<ProductsState>): ProductsState {
|
|
63
65
|
const status = s.status ?? "ready";
|
|
64
66
|
const items = s.items ?? [];
|
|
65
|
-
const errorKind:
|
|
67
|
+
const errorKind: "" | ShownKind = status === "error" ? (s.errorKind || "server") : "";
|
|
66
68
|
const blank = items.length === 0; // nothing on screen to keep
|
|
67
69
|
return {
|
|
68
70
|
status,
|
|
@@ -72,14 +74,17 @@ function shape(s: Partial<ProductsState>): ProductsState {
|
|
|
72
74
|
notEmpty: !(status === "ready" && blank),
|
|
73
75
|
hideOfflineError: !(blank && errorKind === "offline"),
|
|
74
76
|
hideServerError: !(blank && errorKind === "server"),
|
|
77
|
+
hideMembershipError: !(blank && errorKind === "membership"),
|
|
75
78
|
hidePermissionError: !(blank && errorKind === "permission"),
|
|
76
79
|
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
77
80
|
selectedId: s.selectedId ?? "",
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
85
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
81
86
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
82
|
-
// under `himi test` / `himi render`.
|
|
87
|
+
// under `himi test` / `himi render`.
|
|
83
88
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
84
89
|
// No answer at all — what a dropped connection says on every platform.
|
|
85
90
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -88,21 +93,45 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
88
93
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
89
94
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
90
95
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
96
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
97
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
98
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
99
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
function classifyError(e: unknown): ErrorKind {
|
|
101
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
94
102
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
95
|
-
|
|
103
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
104
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
96
105
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
107
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
108
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
109
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
110
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
111
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
112
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
113
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
114
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
101
115
|
// A web call that never reached Wix arrives as
|
|
102
116
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
118
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
119
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
120
|
+
: msg;
|
|
121
|
+
if (status && detail === msg) return "server";
|
|
122
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
123
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
127
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
128
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
129
|
+
|
|
130
|
+
// This screen has no sign-in flow, so a sign-in failure shows the permission state —
|
|
131
|
+
// every class it can show has a node in the descriptor.
|
|
132
|
+
function shownKind(e: unknown): ShownKind {
|
|
133
|
+
const kind = classifyError(e, currentAuthState());
|
|
134
|
+
return kind === "signIn" ? "permission" : kind || "server";
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
async function loadList(s: Partial<ProductsState>): Promise<ProductsState> {
|
|
@@ -111,9 +140,10 @@ async function loadList(s: Partial<ProductsState>): Promise<ProductsState> {
|
|
|
111
140
|
const items = (res.products ?? []).filter((p) => p?.id).map(normalize);
|
|
112
141
|
return shape({ status: "ready", items, selectedId: s.selectedId });
|
|
113
142
|
} catch (e) {
|
|
114
|
-
const errorKind =
|
|
143
|
+
const errorKind = shownKind(e);
|
|
115
144
|
// Keep the rows on a refresh failure; clear them when access is gone.
|
|
116
|
-
const
|
|
145
|
+
const lostAccess = errorKind === "membership" || errorKind === "permission";
|
|
146
|
+
const items = lostAccess ? [] : s.items ?? [];
|
|
117
147
|
return shape({ ...s, status: "error", items, errorKind });
|
|
118
148
|
}
|
|
119
149
|
}
|
|
@@ -25,7 +25,6 @@ interface PostFormState {
|
|
|
25
25
|
savedMessage: string; // "" unless saved → collapseWhenEmpty
|
|
26
26
|
hideSavedLink: boolean; // gates the "Open post" button via hiddenPath
|
|
27
27
|
savedScreenId: string; // deep-link target for the created entity
|
|
28
|
-
cannotSave: boolean; // true after a permission failure → the submit's disabledPath
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function shape(s: Partial<PostFormState>): PostFormState {
|
|
@@ -39,22 +38,26 @@ function shape(s: Partial<PostFormState>): PostFormState {
|
|
|
39
38
|
savedMessage: s.savedMessage ?? "",
|
|
40
39
|
savedScreenId,
|
|
41
40
|
hideSavedLink: status !== "saved" || !savedScreenId,
|
|
42
|
-
cannotSave: s.cannotSave ?? false,
|
|
43
41
|
};
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
// A failed save is an ACTION error: one user-facing sentence per class, shown next to
|
|
47
45
|
// the submit button, which is the retry (`himi docs recipe-four-state`). Never e.message.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// Submit stays available after every class: the user retries once they reconnect,
|
|
47
|
+
// sign in, join the site or are granted access — without restarting the form.
|
|
48
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
49
|
+
const SAVE_FAILED: Record<Exclude<ErrorKind, "">, string> = {
|
|
50
50
|
offline: "You're offline, so nothing was saved. Try again when you reconnect.",
|
|
51
51
|
server: "Couldn't save your changes. Try again.",
|
|
52
52
|
signIn: "Your session has ended. Sign in, then save again.",
|
|
53
|
+
membership: "Join this site before saving.",
|
|
53
54
|
permission: "You don't have permission to save this.",
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
58
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
56
59
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
57
|
-
// under `himi test` / `himi render`.
|
|
60
|
+
// under `himi test` / `himi render`.
|
|
58
61
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
59
62
|
// No answer at all — what a dropped connection says on every platform.
|
|
60
63
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -63,37 +66,54 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
63
66
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
64
67
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
65
68
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
69
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
70
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
71
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
72
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
function classifyError(e: unknown): ErrorKind {
|
|
74
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
69
75
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
70
|
-
|
|
76
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
77
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
71
78
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
80
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
81
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
82
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
83
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
84
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
85
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
86
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
87
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
76
88
|
// A web call that never reached Wix arrives as
|
|
77
89
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
91
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
92
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
93
|
+
: msg;
|
|
94
|
+
if (status && detail === msg) return "server";
|
|
95
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
96
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
100
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
101
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
102
|
+
|
|
83
103
|
// Blog draft bodies are rich content — wrap the plain text minimally.
|
|
84
104
|
function toRichContent(text: string): WixRichContent {
|
|
85
105
|
return { nodes: [{ type: "PARAGRAPH", nodes: [{ type: "TEXT", textData: { text, decorations: [] } }] }] };
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
// Every key that describes the LAST attempt, not this run: a cold boot starts from the declared values.
|
|
89
|
-
const OUTCOME_KEYS = ["status", "error", "savedMessage", "savedScreenId", "hideSavedLink"
|
|
109
|
+
const OUTCOME_KEYS = ["status", "error", "savedMessage", "savedScreenId", "hideSavedLink"] as const;
|
|
90
110
|
|
|
91
111
|
const svc = defineService<PostFormState, {
|
|
92
112
|
submit: (s: PostFormState, values?: Record<string, unknown>) => Promise<PostFormState>;
|
|
93
113
|
}>("postForm", {
|
|
94
114
|
state: shape({}),
|
|
95
|
-
// Outcome keys are per-attempt: a cold boot must not restore a stale error
|
|
96
|
-
//
|
|
115
|
+
// Outcome keys are per-attempt: a cold boot must not restore a stale error or a
|
|
116
|
+
// "Draft created." line from an earlier run.
|
|
97
117
|
notHydrated: OUTCOME_KEYS,
|
|
98
118
|
actions: {
|
|
99
119
|
// `values` is the ONLY channel for what the user typed — keyed by FormField id.
|
|
@@ -115,18 +135,18 @@ const svc = defineService<PostFormState, {
|
|
|
115
135
|
savedScreenId: id ? `my_post_${id}` : "", // wire a templated detail route per the tier-5 guide
|
|
116
136
|
});
|
|
117
137
|
} catch (e) {
|
|
118
|
-
const kind = classifyError(e);
|
|
138
|
+
const kind = classifyError(e, currentAuthState()) || "server";
|
|
119
139
|
// Echo what the user typed back into the prefill, so a failed save loses nothing.
|
|
120
140
|
// Clear the last success line: a failed attempt must not also read "Draft created.".
|
|
121
|
-
return shape({ ...s, title, body, status: "error", error: SAVE_FAILED[kind], savedMessage: ""
|
|
141
|
+
return shape({ ...s, title, body, status: "error", error: SAVE_FAILED[kind], savedMessage: "" });
|
|
122
142
|
}
|
|
123
143
|
},
|
|
124
144
|
},
|
|
125
145
|
});
|
|
126
146
|
|
|
127
147
|
// An older runtime ignores `notHydrated` (it does not set `himalaya.hydrationOptOut`) and restores the
|
|
128
|
-
// whole snapshot: reset the outcome keys in place before any action runs, or
|
|
129
|
-
// would come back on every launch
|
|
148
|
+
// whole snapshot: reset the outcome keys in place before any action runs, or last run's error line
|
|
149
|
+
// would come back on every launch.
|
|
130
150
|
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
131
151
|
const booted = (svc as unknown as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
132
152
|
const fresh = shape({}) as unknown as Record<string, unknown>;
|
|
@@ -67,8 +67,8 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
67
67
|
// Android reports an unknown host as the bare hostname (`users.wix.com`) — hence the last form.
|
|
68
68
|
// No session on the device: the native bridge refuses before any request leaves.
|
|
69
69
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
70
|
-
// Android `site account is not the bound site
|
|
71
|
-
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site/i;
|
|
70
|
+
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
71
|
+
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
72
72
|
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
73
73
|
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
74
74
|
// permission refusal, which a signed-in member and a visitor both get.
|
|
@@ -110,6 +110,20 @@
|
|
|
110
110
|
"hiddenPath": "$service:posts.hideServerError"
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
"id": "error.membership",
|
|
115
|
+
"body": {
|
|
116
|
+
"package": "himalaya",
|
|
117
|
+
"type": "EmptyState",
|
|
118
|
+
"props": {
|
|
119
|
+
"title": "Join this site to view posts",
|
|
120
|
+
"subtitle": "Ask the owner of this blog to add you as a member."
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"modifiers": {
|
|
124
|
+
"hiddenPath": "$service:posts.hideMembershipError"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
113
127
|
{
|
|
114
128
|
"id": "error.permission",
|
|
115
129
|
"body": {
|
|
@@ -21,15 +21,17 @@ interface PostRow {
|
|
|
21
21
|
categoryIds: string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
type ErrorKind = "" | "offline" | "server" | "permission";
|
|
24
|
+
// Every class the recipe's classifier returns …
|
|
25
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
26
|
+
// … and the ones this screen shows: it has no sign-in flow (see shownKind()).
|
|
27
|
+
type ShownKind = Exclude<ErrorKind, "" | "signIn">;
|
|
26
28
|
|
|
27
29
|
interface PostsState {
|
|
28
30
|
status: "loading" | "ready" | "error";
|
|
29
31
|
all: PostRow[]; // the full fetched set — NOT bound by the descriptor
|
|
30
32
|
items: PostRow[]; // ← itemsPath binds here; ALWAYS derived from all + query
|
|
31
33
|
query: string;
|
|
32
|
-
errorKind:
|
|
34
|
+
errorKind: "" | ShownKind;
|
|
33
35
|
// hiddenPath flags (hide when TRUE) — see `himi docs recipe-four-state`
|
|
34
36
|
hideLoading: boolean;
|
|
35
37
|
hideNoPosts: boolean; // EmptyState: the blog has no posts at all
|
|
@@ -37,6 +39,7 @@ interface PostsState {
|
|
|
37
39
|
notEmpty: boolean; // false while either EmptyState shows
|
|
38
40
|
hideOfflineError: boolean;
|
|
39
41
|
hideServerError: boolean;
|
|
42
|
+
hideMembershipError: boolean;
|
|
40
43
|
hidePermissionError: boolean;
|
|
41
44
|
hideRefreshError: boolean;
|
|
42
45
|
}
|
|
@@ -64,7 +67,7 @@ function shape(s: Partial<PostsState>): PostsState {
|
|
|
64
67
|
const all = s.all ?? [];
|
|
65
68
|
const query = s.query ?? "";
|
|
66
69
|
const items = s.items ?? visible(all, query);
|
|
67
|
-
const errorKind:
|
|
70
|
+
const errorKind: "" | ShownKind = status === "error" ? (s.errorKind || "server") : "";
|
|
68
71
|
const blank = all.length === 0; // nothing loaded to keep
|
|
69
72
|
return {
|
|
70
73
|
status,
|
|
@@ -80,13 +83,16 @@ function shape(s: Partial<PostsState>): PostsState {
|
|
|
80
83
|
hideLoading: !(status === "loading" && blank),
|
|
81
84
|
hideOfflineError: !(blank && errorKind === "offline"),
|
|
82
85
|
hideServerError: !(blank && errorKind === "server"),
|
|
86
|
+
hideMembershipError: !(blank && errorKind === "membership"),
|
|
83
87
|
hidePermissionError: !(blank && errorKind === "permission"),
|
|
84
88
|
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
// The classifier below is `himi docs recipe-four-state`'s, VERBATIM — a test holds
|
|
93
|
+
// the two byte-identical, so the rules an agent copies from here are the documented ones.
|
|
88
94
|
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
89
|
-
// under `himi test` / `himi render`.
|
|
95
|
+
// under `himi test` / `himi render`.
|
|
90
96
|
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
91
97
|
// No answer at all — what a dropped connection says on every platform.
|
|
92
98
|
const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRouteToHost|SSLHandshake)Exception|Failed to connect|Unable to resolve host|Connection (?:refused|reset)|Network is unreachable|Software caused connection abort|ECONNRESET|ETIMEDOUT|EPIPE|Broken pipe|stream was reset|unexpected end of stream|timed out|^timeout$|Failed to fetch|NetworkError|^(?:TypeError: )?Load failed$|net::ERR_|^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i;
|
|
@@ -95,21 +101,45 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
95
101
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
96
102
|
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
97
103
|
const SIGNED_OUT = /wixApi not enabled|session unavailable|PRINCIPAL_CHANGED|not signed in|siteNotPermitted\(requested: "account"|site account is not the bound site|webwix_owner_no_session/i;
|
|
104
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
105
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
106
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
107
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
function classifyError(e: unknown): ErrorKind {
|
|
109
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
101
110
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
102
|
-
|
|
111
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
112
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
103
113
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
115
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
116
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
117
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
118
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
119
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
120
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
121
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
122
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
108
123
|
// A web call that never reached Wix arrives as
|
|
109
124
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
126
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
127
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
128
|
+
: msg;
|
|
129
|
+
if (status && detail === msg) return "server";
|
|
130
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
131
|
+
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// App-specific host integration. If the host does not expose auth state here, keep
|
|
135
|
+
// "unknown": a 401 then reads as a session that ended.
|
|
136
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
137
|
+
|
|
138
|
+
// This screen has no sign-in flow, so a sign-in failure shows the permission state —
|
|
139
|
+
// every class it can show has a node in the descriptor.
|
|
140
|
+
function shownKind(e: unknown): ShownKind {
|
|
141
|
+
const kind = classifyError(e, currentAuthState());
|
|
142
|
+
return kind === "signIn" ? "permission" : kind || "server";
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
// The SearchField onChange arg shape varies by surface — read it defensively.
|
|
@@ -132,9 +162,10 @@ async function loadList(s: Partial<PostsState>): Promise<PostsState> {
|
|
|
132
162
|
.filter((r) => r.id);
|
|
133
163
|
return shape({ status: "ready", all, query: currentQuery });
|
|
134
164
|
} catch (e) {
|
|
135
|
-
const errorKind =
|
|
165
|
+
const errorKind = shownKind(e);
|
|
136
166
|
// Keep what was loaded on a refresh failure; clear it when access is gone.
|
|
137
|
-
const
|
|
167
|
+
const lostAccess = errorKind === "membership" || errorKind === "permission";
|
|
168
|
+
const all = lostAccess ? [] : s.all ?? [];
|
|
138
169
|
return shape({ status: "error", all, query: currentQuery, errorKind });
|
|
139
170
|
}
|
|
140
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1485.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"artifactId": "himalaya-cli"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
"falconPackageHash": "
|
|
74
|
+
"falconPackageHash": "b348966433fe41b3836cc5030d8aac7869b50966ab9ac20254d62046"
|
|
75
75
|
}
|
|
@@ -84,14 +84,17 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
84
84
|
}
|
|
85
85
|
return result;
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
/** [keepRows]: a search typed over the rows on screen leaves them there until its answer arrives (status stays
|
|
88
|
+
* `loading`, and nothing pages under the old answer), so the list is not blanked on each keystroke. */
|
|
89
|
+
const fetch = async (append: boolean, keepRows = false): Promise<void> => {
|
|
88
90
|
if (disposed || (append && (state.loadingMore || state.status === 'loading' || !state.nextCursor))) return;
|
|
89
91
|
const revision = append ? generation : ++generation;
|
|
90
92
|
const cursor = append ? state.nextCursor : '';
|
|
91
93
|
const criteria = copyQuery(state.criteria);
|
|
92
94
|
if (!append) loadedPages = 0;
|
|
93
95
|
state = { ...state, status: append ? 'ready' : 'loading', loadingMore: append, pageError: false, error: '',
|
|
94
|
-
...(append ? {} :
|
|
96
|
+
...(append ? {} : keepRows ? { nextCursor: '', complete: false, provisional: false }
|
|
97
|
+
: { items: [], nextCursor: '', total: undefined, metadata: undefined, complete: false, provisional: false }) };
|
|
95
98
|
emit();
|
|
96
99
|
try {
|
|
97
100
|
if (!append && source.cached) {
|
|
@@ -117,7 +120,9 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
117
120
|
clearedByError = clear || /HTTP 40[13]\b/.test(String(error));
|
|
118
121
|
if (clearedByError) lastShown = undefined;
|
|
119
122
|
if (clear) loadedPages = 0;
|
|
120
|
-
|
|
123
|
+
// The rows a typed search kept on screen while it ran were never its answer: a failed one shows none of them.
|
|
124
|
+
const drop = clear || keepRows;
|
|
125
|
+
state = { ...state, ...(drop ? { items: [], nextCursor: '', total: undefined, metadata: undefined } : {}),
|
|
121
126
|
status: append && !clear ? 'ready' : 'error', loadingMore: false, pageError: append && !clear, complete: false,
|
|
122
127
|
provisional: false, error: source.errorMessage?.(error) ?? '' };
|
|
123
128
|
}
|
|
@@ -134,9 +139,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
134
139
|
const count = restorePages && same ? Math.max(loadedPages, restoringPages, 1) : 1;
|
|
135
140
|
const showing = lastShown && sameCriteria(lastShown.state.criteria, next) ? lastShown : undefined;
|
|
136
141
|
restoringPages = count;
|
|
142
|
+
// Only the search text changed: the user is typing over the rows on screen, also while the last keystroke's
|
|
143
|
+
// search is still out.
|
|
144
|
+
const typing = !same && (state.status === 'ready' || state.status === 'loading')
|
|
145
|
+
&& sameCriteria({ ...state.criteria, query: '' }, { ...next, query: '' });
|
|
137
146
|
state = { ...state, criteria: next };
|
|
138
147
|
if (revalidate && showing) { revalidating = generation + 1; clearedByError = false; }
|
|
139
|
-
const pending = fetch(false);
|
|
148
|
+
const pending = fetch(false, typing);
|
|
140
149
|
const revision = generation;
|
|
141
150
|
await pending;
|
|
142
151
|
// Re-appearance revalidates every previously opened page using fresh
|
|
@@ -194,7 +194,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
194
194
|
noSignIn: !signInRequired,
|
|
195
195
|
errorTitle: signInRequired ? copyParam('collection.signInTitle', 'Sign in to see this') : copyParam('collection.loadErrorTitle', 'This collection could not load'),
|
|
196
196
|
errorMessage,
|
|
197
|
-
|
|
197
|
+
// A skeleton only with nothing to show: a search re-running over the rows on screen keeps them.
|
|
198
|
+
noLoading: !loading || rows.length > 0,
|
|
198
199
|
// De-dup against a domain that paints its OWN empty block (screen-kit emptyBlock is
|
|
199
200
|
// gated on the domain's top-level `noEmpty`): only one empty state ever shows.
|
|
200
201
|
noEmptySource: !sourceEmpty || s.noEmpty === false,
|