@wix/himalaya-cli 0.1464.0 → 0.1466.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 +3 -2
- 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/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
package/guides/recipe-search.md
CHANGED
|
@@ -64,9 +64,9 @@ not say why there are no rows.
|
|
|
64
64
|
| Initial, no query | Search prompt plus useful recent queries, categories, or suggestions. Avoid presenting this as a failed search. |
|
|
65
65
|
| Loading | First load: a skeleton or progress state. Refresh: keep current results visible and show a compact progress indicator when possible. |
|
|
66
66
|
| Results | Matching rows, result count, and the available sort/filter controls. |
|
|
67
|
-
| No matches |
|
|
67
|
+
| No matches | An `EmptyState` explaining that the current query or filters found nothing; offer a way to adjust them, and Clear only where the field is controlled (the native slot) — the inline `SearchField` is uncontrolled, so Clear there empties the query but not the field. |
|
|
68
68
|
| Empty catalog | Explain that the catalog itself has no items yet, independent of the query; provide a useful browse/back action. |
|
|
69
|
-
| Error |
|
|
69
|
+
| Error | Classified as in `himi docs recipe-four-state`: an icon-less `EmptyState` per class with user-facing copy, Try again only for offline and server, Sign in for sign-in, no action for permission. Retain the query and controls, and keep it distinct from no matches. |
|
|
70
70
|
|
|
71
71
|
### Native slot and old-client fallback
|
|
72
72
|
|
|
@@ -170,7 +170,12 @@ interface PostsState {
|
|
|
170
170
|
all: PostRow[]; // the full fetched set — NOT bound by the descriptor
|
|
171
171
|
items: PostRow[]; // ← itemsPath binds here; always derived from all + query
|
|
172
172
|
query: string;
|
|
173
|
-
|
|
173
|
+
hideNoPosts: boolean; // the EmptyState for "loaded, and there is nothing at all"
|
|
174
|
+
hideNoMatches: boolean; // the EmptyState for "there are posts, none match the query"
|
|
175
|
+
errorKind: "" | "offline" | "server" | "signIn" | "permission"; // classifyError(e)
|
|
176
|
+
hideLoading: boolean; hideOfflineError: boolean; hideServerError: boolean;
|
|
177
|
+
hideSignInError: boolean; hidePermissionError: boolean;
|
|
178
|
+
hideRefreshError: boolean; // hiddenPath flags, as in recipe-four-state
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
function visible(all: PostRow[], query: string): PostRow[] {
|
|
@@ -186,31 +191,103 @@ function queryFrom(arg: unknown): string {
|
|
|
186
191
|
return String(a?.value ?? a?.text ?? a?.query ?? "");
|
|
187
192
|
}
|
|
188
193
|
|
|
194
|
+
// Actions are not serialized: the user can type while load() is in flight, and the
|
|
195
|
+
// action that finishes last writes the whole state. So the latest query lives outside
|
|
196
|
+
// the state, and every fetch applies it when it returns.
|
|
197
|
+
let currentQuery = "";
|
|
198
|
+
|
|
199
|
+
// The SearchField is uncontrolled: it opens empty, so the query must start over too.
|
|
200
|
+
// `load` (opening the screen) resets it; a query typed while that fetch is in flight
|
|
201
|
+
// still wins. `retry` keeps it.
|
|
202
|
+
load: async (s) => { currentQuery = ""; return loadList(s); },
|
|
203
|
+
|
|
204
|
+
// After defineService: a cold boot restores the persisted `query` AND the `items` and
|
|
205
|
+
// empty flags derived from it. Re-derive them for an empty query before anything paints.
|
|
206
|
+
const booted = (svc as unknown as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
207
|
+
if (booted && typeof booted === "object") {
|
|
208
|
+
Object.assign(booted, shape({ ...(booted as Partial<PostsState>), query: "", items: undefined }));
|
|
209
|
+
}
|
|
210
|
+
|
|
189
211
|
// actions:
|
|
190
212
|
search: (s, arg) => {
|
|
191
213
|
const query = queryFrom(arg);
|
|
214
|
+
currentQuery = query;
|
|
192
215
|
const items = visible(s.all, query);
|
|
193
|
-
return shape({ ...s, query, items }); // shape() recomputes
|
|
216
|
+
return shape({ ...s, query, items }); // shape() recomputes the empty flags from all+items+query
|
|
194
217
|
},
|
|
195
|
-
clear: (s) => shape({ ...s, query: "", items: s.all }),
|
|
218
|
+
clear: (s) => { currentQuery = ""; return shape({ ...s, query: "", items: s.all }); },
|
|
196
219
|
```
|
|
197
220
|
|
|
198
|
-
- `load()` fetches once, fills `all`, and sets `items = visible(all,
|
|
221
|
+
- `load()` fetches once, fills `all`, and sets `items = visible(all, currentQuery)`.
|
|
222
|
+
On a `signIn` or `permission` failure it clears **`all` as well as `items`** —
|
|
223
|
+
`search`, `clear` and `retry` rebuild rows from `all`, so rows left there come
|
|
224
|
+
back on the next keystroke.
|
|
199
225
|
- The shipped Blog v3 `WixPost` type includes `categoryIds` and both Wix-image and
|
|
200
226
|
embed-thumbnail media projections. Normalize them without a local cast:
|
|
201
227
|
`coverUrl: post.media?.wixMedia?.image?.url ?? post.media?.embedMedia?.thumbnail?.url ?? ""`
|
|
202
228
|
and `categoryIds: post.categoryIds ?? []`.
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
`
|
|
229
|
+
- **Two empty states, both `EmptyState`**: "no data at all" and "no matches" are
|
|
230
|
+
different screens. Search's `shape()` takes four-state's flags with one change:
|
|
231
|
+
`blank` is `all.length === 0`, not `items.length === 0` — `items` is the filtered
|
|
232
|
+
view, so a query with no matches over rows kept after a failed refresh must not
|
|
233
|
+
read as "nothing loaded" and show the full-screen error. In `shape()`, wherever
|
|
234
|
+
items change, recompute
|
|
235
|
+
`hideNoPosts = !(status === "ready" && all.length === 0)` and
|
|
236
|
+
`hideNoMatches = !(all.length > 0 && items.length === 0)` — no `status` check,
|
|
237
|
+
because the user can still search the rows kept after a failed refresh. Their
|
|
238
|
+
copy is static — an `EmptyState` cannot bind its text on native — and the
|
|
239
|
+
no-matches one has no action: the `SearchField` is uncontrolled, so a "Clear
|
|
240
|
+
search" button would empty the worker's query while the field still shows it:
|
|
241
|
+
|
|
242
|
+
```jsonc
|
|
243
|
+
{ "id": "empty.noPosts",
|
|
244
|
+
"body": { "package": "himalaya", "type": "EmptyState",
|
|
245
|
+
"props": { "icon": "inbox", "title": "No posts yet",
|
|
246
|
+
"subtitle": "New posts will show up here." } },
|
|
247
|
+
"modifiers": { "hiddenPath": "$service:posts.hideNoPosts" } },
|
|
248
|
+
{ "id": "empty.noMatches",
|
|
249
|
+
"body": { "package": "himalaya", "type": "EmptyState",
|
|
250
|
+
"props": { "icon": "search", "title": "No posts match your search",
|
|
251
|
+
"subtitle": "Try a different word." } },
|
|
252
|
+
"modifiers": { "hiddenPath": "$service:posts.hideNoMatches" } }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 3. Loading, error and retry
|
|
256
|
+
|
|
257
|
+
A searchable list loads data, so it carries the four states of
|
|
258
|
+
`himi docs recipe-four-state`:
|
|
259
|
+
|
|
260
|
+
- **Loading** — a `Skeleton` shaped like the result rows, gated by `hideLoading`,
|
|
261
|
+
while the first `load()` runs. The search field is usable at once.
|
|
262
|
+
- **Error, nothing loaded** — one `EmptyState` per class `classifyError` can
|
|
263
|
+
return, **no icon**, user-facing copy; offline and server get **Try again** →
|
|
264
|
+
`tier5.posts.retry`, sign-in gets **Sign in**, permission gets no action. The
|
|
265
|
+
`catch` sets `errorKind` from `classifyError(e)`; it never shows `e.message`.
|
|
266
|
+
- **Retry keeps the query.** `retry` re-runs `load()` and then re-applies
|
|
267
|
+
`currentQuery` — not `s.query`, which is the query from when the retry
|
|
268
|
+
STARTED — so what the user typed during the fetch is not lost.
|
|
269
|
+
- **No matches is not an error.** A successful search with zero results shows the
|
|
270
|
+
no-matches `EmptyState` ("No posts match your search"), with no action and no retry.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
retry: async (s) => {
|
|
274
|
+
const next = await load(s); // classified on failure
|
|
275
|
+
// Lost access clears the rows; any other failure keeps them for the Banner.
|
|
276
|
+
const lostAccess = next.errorKind === "signIn" || next.errorKind === "permission";
|
|
277
|
+
const all = lostAccess ? [] : next.all;
|
|
278
|
+
return shape({ ...next, all, query: currentQuery, items: visible(all, currentQuery) });
|
|
279
|
+
},
|
|
280
|
+
```
|
|
206
281
|
|
|
207
|
-
##
|
|
282
|
+
## 4. Server-side search (large datasets)
|
|
208
283
|
|
|
209
284
|
In-worker filtering is right up to a few hundred rows. Past that, make `search`
|
|
210
285
|
re-query the API with the term (most Wix domain query endpoints take a filter,
|
|
211
286
|
e.g. blog `queryPosts` — check `himi sdk wix-<domain>`), keep `status: "loading"`
|
|
212
287
|
during the round-trip, and debounce higher (300–400ms). The descriptor does not
|
|
213
|
-
change — only the action's body.
|
|
288
|
+
change — only the action's body. A failed server-side search **keeps the query and
|
|
289
|
+
the results already on screen** and shows the refresh `Banner` with **Try again**,
|
|
290
|
+
which runs the same query again.
|
|
214
291
|
|
|
215
292
|
## Gotchas
|
|
216
293
|
|
|
@@ -231,6 +308,6 @@ change — only the action's body.
|
|
|
231
308
|
## See also
|
|
232
309
|
|
|
233
310
|
- `himi docs recipe-dynamic-list` — the list this recipe filters.
|
|
234
|
-
- `himi docs recipe-four-state` — the
|
|
311
|
+
- `himi docs recipe-four-state` — the loading, classified error and retry idiom.
|
|
235
312
|
- `himi components SearchField` / `FilterChips` / `Tabs`.
|
|
236
313
|
- `.agents/skills/himalaya/examples/search-list.*` (written into the app by `himi init` / `himi agent-init`) — this recipe, runnable.
|
|
@@ -36,6 +36,21 @@ export async function finishVoiceJournal(sessionId: string) {
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
**States.** Recording reads no remote data, so the journal screen needs no network
|
|
40
|
+
loading or error states — but it does have states the user must see:
|
|
41
|
+
|
|
42
|
+
- **Unavailable** (`status: "unavailable"` — no microphone, permission denied, or a
|
|
43
|
+
host without capture): an icon-less `EmptyState` that says recording is not
|
|
44
|
+
available and gives the next step the platform offers (for example, how to allow
|
|
45
|
+
the microphone). No **Try again**: the same call fails the same way until the
|
|
46
|
+
user changes something.
|
|
47
|
+
- **Processing** (transcription, diarization): a progress or a placeholder line on
|
|
48
|
+
the entry, never a blocking spinner; a processing failure keeps the audio and
|
|
49
|
+
shows the entry without a transcript.
|
|
50
|
+
- **Saved entries list**: reading `local-db` is local and quick, so it needs no
|
|
51
|
+
skeleton; an empty journal is an `EmptyState` with a "Record your first entry"
|
|
52
|
+
action.
|
|
53
|
+
|
|
39
54
|
The SDUI layer can use the existing list, text, button, progress, audio/media, and state-binding
|
|
40
55
|
components for recording, transcript detail, search, deferred processing, summaries, categories,
|
|
41
56
|
delete, and export. Generate summaries and structured categories with `@himalaya/on-device-ai`
|
|
@@ -33,54 +33,115 @@
|
|
|
33
33
|
"id": "loading",
|
|
34
34
|
"body": {
|
|
35
35
|
"package": "himalaya",
|
|
36
|
-
"type": "
|
|
36
|
+
"type": "Skeleton",
|
|
37
37
|
"props": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"collapseWhenEmpty": true
|
|
38
|
+
"height": 56,
|
|
39
|
+
"cornerRadius": 12,
|
|
40
|
+
"count": 5
|
|
42
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"modifiers": {
|
|
44
|
+
"hiddenPath": "$service:posts.hideLoading"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "empty.no_posts",
|
|
49
|
+
"body": {
|
|
50
|
+
"package": "himalaya",
|
|
51
|
+
"type": "EmptyState",
|
|
52
|
+
"props": {
|
|
53
|
+
"icon": "doc",
|
|
54
|
+
"title": "No posts yet",
|
|
55
|
+
"subtitle": "Posts you publish show up here."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"modifiers": {
|
|
59
|
+
"hiddenPath": "$service:posts.hideNoPosts"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "empty.no_matches",
|
|
64
|
+
"body": {
|
|
65
|
+
"package": "himalaya",
|
|
66
|
+
"type": "EmptyState",
|
|
67
|
+
"props": {
|
|
68
|
+
"icon": "search",
|
|
69
|
+
"title": "No posts match your search",
|
|
70
|
+
"subtitle": "Try a different word."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"modifiers": {
|
|
74
|
+
"hiddenPath": "$service:posts.hideNoMatches"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "error.offline",
|
|
79
|
+
"body": {
|
|
80
|
+
"package": "himalaya",
|
|
81
|
+
"type": "EmptyState",
|
|
82
|
+
"props": {
|
|
83
|
+
"title": "Reconnect to the internet",
|
|
84
|
+
"subtitle": "Connect to the internet, then try again.",
|
|
85
|
+
"action": {
|
|
86
|
+
"actionId": "tier5.posts.retry"
|
|
87
|
+
},
|
|
88
|
+
"actionLabel": "Try again"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"modifiers": {
|
|
92
|
+
"hiddenPath": "$service:posts.hideOfflineError"
|
|
43
93
|
}
|
|
44
94
|
},
|
|
45
95
|
{
|
|
46
|
-
"id": "
|
|
96
|
+
"id": "error.server",
|
|
47
97
|
"body": {
|
|
48
98
|
"package": "himalaya",
|
|
49
|
-
"type": "
|
|
99
|
+
"type": "EmptyState",
|
|
50
100
|
"props": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
101
|
+
"title": "Unable to load posts",
|
|
102
|
+
"subtitle": "There was a technical issue on our end. Please wait and try again later.",
|
|
103
|
+
"action": {
|
|
104
|
+
"actionId": "tier5.posts.retry"
|
|
105
|
+
},
|
|
106
|
+
"actionLabel": "Try again"
|
|
55
107
|
}
|
|
108
|
+
},
|
|
109
|
+
"modifiers": {
|
|
110
|
+
"hiddenPath": "$service:posts.hideServerError"
|
|
56
111
|
}
|
|
57
112
|
},
|
|
58
113
|
{
|
|
59
|
-
"id": "error",
|
|
114
|
+
"id": "error.permission",
|
|
60
115
|
"body": {
|
|
61
116
|
"package": "himalaya",
|
|
62
|
-
"type": "
|
|
117
|
+
"type": "EmptyState",
|
|
63
118
|
"props": {
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"color": "danger",
|
|
67
|
-
"collapseWhenEmpty": true
|
|
119
|
+
"title": "You need permission to view this screen",
|
|
120
|
+
"subtitle": "Ask the owner of this blog for access."
|
|
68
121
|
}
|
|
122
|
+
},
|
|
123
|
+
"modifiers": {
|
|
124
|
+
"hiddenPath": "$service:posts.hidePermissionError"
|
|
69
125
|
}
|
|
70
126
|
},
|
|
71
127
|
{
|
|
72
|
-
"id": "
|
|
128
|
+
"id": "error.refresh",
|
|
73
129
|
"body": {
|
|
74
130
|
"package": "himalaya",
|
|
75
|
-
"type": "
|
|
131
|
+
"type": "Banner",
|
|
76
132
|
"props": {
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"action": {
|
|
133
|
+
"severity": "warning",
|
|
134
|
+
"title": "Couldn't refresh",
|
|
135
|
+
"message": "Showing what was loaded before.",
|
|
136
|
+
"action": {
|
|
137
|
+
"actionId": "tier5.posts.retry"
|
|
138
|
+
},
|
|
139
|
+
"actionLabel": "Try again"
|
|
81
140
|
}
|
|
82
141
|
},
|
|
83
|
-
"modifiers": {
|
|
142
|
+
"modifiers": {
|
|
143
|
+
"hiddenPath": "$service:posts.hideRefreshError"
|
|
144
|
+
}
|
|
84
145
|
},
|
|
85
146
|
{
|
|
86
147
|
"id": "posts_list",
|
|
@@ -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.1466.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": "45c573391083ede76066e2c3b621e23db07135b7ad8781d653f59629"
|
|
75
75
|
}
|