@wix/himalaya-cli 0.1465.0 → 0.1467.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 +24 -7
- package/guides/designing-himi-screens.md +58 -3
- package/guides/dynamic-list-t5.descriptor.json +69 -23
- package/guides/dynamic-list.service.ts +60 -15
- package/guides/form-submit-t5.descriptor.json +28 -27
- package/guides/form-submit.service.ts +60 -2
- package/guides/getting-started.md +2 -1
- package/guides/himi-skill.md +2 -1
- package/guides/index.json +1 -0
- package/guides/qa-builds.md +78 -0
- package/guides/recipe-dynamic-list.md +57 -22
- package/guides/recipe-form-submit.md +39 -7
- package/guides/recipe-four-state.md +178 -54
- package/guides/recipe-onboarding.md +2 -1
- package/guides/recipe-search.md +89 -12
- package/guides/recipe-voice-journal.md +15 -0
- package/guides/search-list-t5.descriptor.json +85 -24
- package/guides/search-list.service.ts +83 -21
- package/package.json +2 -2
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// the current query, and let the SearchField's debounced onChange action swap
|
|
7
7
|
// what the single bound path holds. The descriptor never rebinds anything.
|
|
8
8
|
//
|
|
9
|
-
// Builds on the dynamic-list recipe (worker owns the array,
|
|
10
|
-
//
|
|
9
|
+
// Builds on the dynamic-list recipe (worker owns the array, one hiddenPath flag per
|
|
10
|
+
// state, classified errors) — see examples/dynamic-list.* for that base. A retry
|
|
11
|
+
// keeps the user's query. Declares "wix_api".
|
|
11
12
|
|
|
12
13
|
import { defineService } from "@wix/himalaya/state";
|
|
13
14
|
import { queryPosts, type WixPost } from "@wix/himalaya/wix-blog";
|
|
@@ -20,15 +21,24 @@ interface PostRow {
|
|
|
20
21
|
categoryIds: string[];
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// A public blog has no sign-in flow, so a 401 reads as "permission" here.
|
|
25
|
+
type ErrorKind = "" | "offline" | "server" | "permission";
|
|
26
|
+
|
|
23
27
|
interface PostsState {
|
|
24
28
|
status: "loading" | "ready" | "error";
|
|
25
29
|
all: PostRow[]; // the full fetched set — NOT bound by the descriptor
|
|
26
30
|
items: PostRow[]; // ← itemsPath binds here; ALWAYS derived from all + query
|
|
27
31
|
query: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
errorKind: ErrorKind;
|
|
33
|
+
// hiddenPath flags (hide when TRUE) — see `himi docs recipe-four-state`
|
|
34
|
+
hideLoading: boolean;
|
|
35
|
+
hideNoPosts: boolean; // EmptyState: the blog has no posts at all
|
|
36
|
+
hideNoMatches: boolean; // EmptyState: posts exist, none match the query (not an error)
|
|
37
|
+
notEmpty: boolean; // false while either EmptyState shows
|
|
38
|
+
hideOfflineError: boolean;
|
|
39
|
+
hideServerError: boolean;
|
|
40
|
+
hidePermissionError: boolean;
|
|
41
|
+
hideRefreshError: boolean;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
function normalize(raw: WixPost): PostRow {
|
|
@@ -54,38 +64,78 @@ function shape(s: Partial<PostsState>): PostsState {
|
|
|
54
64
|
const all = s.all ?? [];
|
|
55
65
|
const query = s.query ?? "";
|
|
56
66
|
const items = s.items ?? visible(all, query);
|
|
67
|
+
const errorKind: ErrorKind = status === "error" ? (s.errorKind || "server") : "";
|
|
68
|
+
const blank = all.length === 0; // nothing loaded to keep
|
|
57
69
|
return {
|
|
58
70
|
status,
|
|
59
71
|
all,
|
|
60
72
|
items,
|
|
61
73
|
query,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
errorKind,
|
|
75
|
+
// Two EmptyStates, not one bound Text: iOS/Android do not bind EmptyState text.
|
|
76
|
+
hideNoPosts: !(status === "ready" && blank),
|
|
77
|
+
// Over rows kept after a failed refresh too: no matches is still empty, not an error.
|
|
78
|
+
hideNoMatches: !(!blank && items.length === 0),
|
|
79
|
+
notEmpty: !((status === "ready" && blank) || (!blank && items.length === 0)),
|
|
80
|
+
hideLoading: !(status === "loading" && blank),
|
|
81
|
+
hideOfflineError: !(blank && errorKind === "offline"),
|
|
82
|
+
hideServerError: !(blank && errorKind === "server"),
|
|
83
|
+
hidePermissionError: !(blank && errorKind === "permission"),
|
|
84
|
+
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
71
85
|
};
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
// The HTTP status rides in the message: `HTTP 503: …` on a device, `Wix 503 for …`
|
|
89
|
+
// under `himi test` / `himi render`. Only a transport failure is "offline".
|
|
90
|
+
const STATUS = /\b(?:HTTP|Wix) (\d{3})\b/;
|
|
91
|
+
// No answer at all — what a dropped connection says on every platform.
|
|
92
|
+
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;
|
|
93
|
+
// Android reports an unknown host as the bare hostname (`users.wix.com`) — hence the last form.
|
|
94
|
+
// No session on the device: the native bridge refuses before any request leaves.
|
|
95
|
+
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
96
|
+
// Android `site account is not the bound site …`; the web host `HTTP 501: {"error":"webwix_owner_no_session"}`.
|
|
97
|
+
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;
|
|
98
|
+
|
|
99
|
+
// The same classifier as `himi docs recipe-four-state`, except that a public screen with no sign-in flow shows the permission state for a sign-in failure.
|
|
100
|
+
function classifyError(e: unknown): ErrorKind {
|
|
101
|
+
const msg = String((e as Error)?.message ?? e ?? "");
|
|
102
|
+
const code = String((e as { code?: unknown })?.code ?? "");
|
|
103
|
+
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
104
|
+
if (status === 401) return "permission";
|
|
105
|
+
if (status === 403) return "permission";
|
|
106
|
+
if (SIGNED_OUT.test(msg)) return "permission";
|
|
107
|
+
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
108
|
+
// A web call that never reached Wix arrives as
|
|
109
|
+
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
110
|
+
if (status && !msg.includes("webwix_direct_failed")) return "server";
|
|
111
|
+
if (TRANSPORT.test(msg)) return "offline";
|
|
112
|
+
return "server"; // a bug or a malformed row is not the user's connection
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
// The SearchField onChange arg shape varies by surface — read it defensively.
|
|
75
116
|
function queryFrom(arg: unknown): string {
|
|
76
117
|
const a = arg as { value?: string; text?: string; query?: string } | undefined;
|
|
77
118
|
return String(a?.value ?? a?.text ?? a?.query ?? "");
|
|
78
119
|
}
|
|
79
120
|
|
|
80
|
-
|
|
121
|
+
// Actions are not serialized: the user can type while load() is in flight, and the
|
|
122
|
+
// action that finishes last writes the whole state. So the latest query lives outside
|
|
123
|
+
// the state, and the fetch applies it when it returns — a retry or a slow first load
|
|
124
|
+
// never throws away what the user typed.
|
|
125
|
+
let currentQuery = "";
|
|
126
|
+
|
|
127
|
+
async function loadList(s: Partial<PostsState>): Promise<PostsState> {
|
|
81
128
|
try {
|
|
82
129
|
const res = await queryPosts({ query: { paging: { limit: 100 } } });
|
|
83
130
|
const all = ((res as { posts?: WixPost[] }).posts ?? [])
|
|
84
131
|
.map(normalize)
|
|
85
132
|
.filter((r) => r.id);
|
|
86
|
-
return shape({ status: "ready", all, query:
|
|
133
|
+
return shape({ status: "ready", all, query: currentQuery });
|
|
87
134
|
} catch (e) {
|
|
88
|
-
|
|
135
|
+
const errorKind = classifyError(e);
|
|
136
|
+
// Keep what was loaded on a refresh failure; clear it when access is gone.
|
|
137
|
+
const all = errorKind === "permission" ? [] : s.all ?? [];
|
|
138
|
+
return shape({ status: "error", all, query: currentQuery, errorKind });
|
|
89
139
|
}
|
|
90
140
|
}
|
|
91
141
|
|
|
@@ -97,17 +147,29 @@ const svc = defineService<PostsState, {
|
|
|
97
147
|
}>("posts", {
|
|
98
148
|
state: shape({ status: "loading" }),
|
|
99
149
|
actions: {
|
|
100
|
-
load
|
|
101
|
-
|
|
150
|
+
// `load` opens the screen, and the uncontrolled SearchField opens empty: start the
|
|
151
|
+
// query over too. A query typed while this fetch is in flight still wins.
|
|
152
|
+
load: async (s) => { currentQuery = ""; return loadList(s); },
|
|
153
|
+
retry: async (s) => loadList(s),
|
|
102
154
|
// In-worker filter: swap what the bound `items` path holds. For datasets past
|
|
103
155
|
// a few hundred rows, make this re-query the API instead (same descriptor).
|
|
104
156
|
search: (s, arg) => {
|
|
105
157
|
const query = queryFrom(arg);
|
|
158
|
+
currentQuery = query;
|
|
106
159
|
return shape({ ...s, query, items: visible(s.all, query) });
|
|
107
160
|
},
|
|
108
|
-
clear: (s) => shape({ ...s, query: "", items: s.all }),
|
|
161
|
+
clear: (s) => { currentQuery = ""; return shape({ ...s, query: "", items: s.all }); },
|
|
109
162
|
},
|
|
110
163
|
});
|
|
111
164
|
|
|
165
|
+
// The SearchField is uncontrolled and shows empty on a cold boot, but the runtime restores the
|
|
166
|
+
// persisted snapshot: last run's `query`, and the `items` and empty flags derived from it. Re-derive
|
|
167
|
+
// them in place for an empty query before anything paints, or the list stays filtered by text the
|
|
168
|
+
// field does not show until the first fetch settles.
|
|
169
|
+
const booted = (svc as unknown as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
170
|
+
if (booted && typeof booted === "object") {
|
|
171
|
+
Object.assign(booted, shape({ ...(booted as Partial<PostsState>), query: "", items: undefined }));
|
|
172
|
+
}
|
|
173
|
+
|
|
112
174
|
svc.load(); // fetch once at boot; searching never re-fetches
|
|
113
175
|
export { svc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1467.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": "7882aee1b35bad5d711c75125239b98cc49d21ee40dff33a6fda8b6b"
|
|
75
75
|
}
|