@wix/himalaya-cli 0.1475.0 → 0.1476.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 +170 -63
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +3 -56
- package/guides/component-reference.md +5 -15
- package/guides/designing-himi-screens.md +9 -6
- package/guides/index.json +1 -1
- package/guides/qa-builds.md +47 -18
- package/guides/recipe-dynamic-list.md +10 -9
- package/guides/recipe-form-submit.md +12 -13
- package/guides/recipe-four-state.md +61 -21
- package/guides/recipe-search.md +11 -7
- package/guides/tier-5-add-a-screen.md +8 -2
- package/package.json +2 -2
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
**The rule every recipe follows:** a screen that loads or writes data shows a
|
|
4
4
|
**loading state**, a **classified error state**, and a **recovery action where the
|
|
5
5
|
user can fix the failure** — retry for an offline or a server failure, and for a failed
|
|
6
|
-
write; Sign in when the user is signed out;
|
|
7
|
-
|
|
6
|
+
write; Sign in when the user is signed out; the site's join flow for a non-member when
|
|
7
|
+
available; nothing for a permission failure, an empty result or a field error, where the same
|
|
8
|
+
request gives the same answer. This recipe is the shared
|
|
8
9
|
idiom; the other recipes link here for it. Design guidance for the states:
|
|
9
10
|
`himi docs designing-himi-screens` §5 ("Loading states", "Error states").
|
|
10
11
|
|
|
@@ -22,7 +23,7 @@ more than half model error, and the reference apps (`apps/himi-owner-lite`,
|
|
|
22
23
|
## 1. One status field, a classified error, derived flags
|
|
23
24
|
|
|
24
25
|
```ts
|
|
25
|
-
type ErrorKind = "" | "offline" | "server" | "signIn" | "permission";
|
|
26
|
+
type ErrorKind = "" | "offline" | "server" | "signIn" | "membership" | "permission";
|
|
26
27
|
|
|
27
28
|
interface State {
|
|
28
29
|
status: "loading" | "ready" | "error";
|
|
@@ -34,6 +35,7 @@ interface State {
|
|
|
34
35
|
hideOfflineError: boolean; // one EmptyState per error class …
|
|
35
36
|
hideServerError: boolean;
|
|
36
37
|
hideSignInError: boolean;
|
|
38
|
+
hideMembershipError: boolean;
|
|
37
39
|
hidePermissionError: boolean;
|
|
38
40
|
hideRefreshError: boolean; // … and a Banner when a refresh fails over rows we kept
|
|
39
41
|
}
|
|
@@ -50,6 +52,7 @@ function shape(s: Partial<State>): State {
|
|
|
50
52
|
hideOfflineError: !(blank && errorKind === "offline"),
|
|
51
53
|
hideServerError: !(blank && errorKind === "server"),
|
|
52
54
|
hideSignInError: !(blank && errorKind === "signIn"),
|
|
55
|
+
hideMembershipError: !(blank && errorKind === "membership"),
|
|
53
56
|
hidePermissionError: !(blank && errorKind === "permission"),
|
|
54
57
|
// Only a failure a retry can fix gets the banner over kept rows.
|
|
55
58
|
hideRefreshError: !(!blank && (errorKind === "offline" || errorKind === "server")),
|
|
@@ -66,31 +69,49 @@ const TRANSPORT = /NSURLErrorDomain|(?:UnknownHost|Connect|SocketTimeout|NoRoute
|
|
|
66
69
|
// A pinned site with no member session: iOS `siteNotPermitted(requested: "account", …)`,
|
|
67
70
|
// Android `site account is not the bound site …`.
|
|
68
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;
|
|
72
|
+
// The account is not a member of this site: iOS `NotSiteMember(metaSiteId: …)`,
|
|
73
|
+
// Android `not a member of site …`. NOT `WDE0027` — that is Wix Data's generic
|
|
74
|
+
// permission refusal, which a signed-in member and a visitor both get.
|
|
75
|
+
const NOT_MEMBER = /\bNotSiteMember\b|\bnot a member of site\b/i;
|
|
69
76
|
|
|
70
|
-
function classifyError(e: unknown): ErrorKind {
|
|
77
|
+
function classifyError(e: unknown, authState: "signedIn" | "signedOut" | "unknown" = "unknown"): ErrorKind {
|
|
71
78
|
const msg = String((e as Error)?.message ?? e ?? "");
|
|
72
|
-
|
|
79
|
+
// Some bridges put the Wix code in the response body rather than Error.code.
|
|
80
|
+
const code = String((e as { code?: unknown })?.code ?? /"(?:code|error)"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? "");
|
|
73
81
|
const status = Number(STATUS.exec(msg)?.[1] ?? 0);
|
|
74
|
-
if (
|
|
75
|
-
if (
|
|
76
|
-
|
|
82
|
+
if (code === "AccountSessionExpired" || code === "webwix_account_signed_out" || SIGNED_OUT.test(msg)) return "signIn";
|
|
83
|
+
if (code === "NotSiteMember" || NOT_MEMBER.test(msg)) return "membership";
|
|
84
|
+
// A 401 is a missing or expired session — unless the host knows the user is signed
|
|
85
|
+
// in, when it is a policy refusal. Never "server": a retry sends the same 401 back.
|
|
86
|
+
if (status === 401) return authState === "signedIn" ? "permission" : "signIn";
|
|
87
|
+
// A 403 is a missing permission — but a visitor the host knows is signed out gets
|
|
88
|
+
// `403 WDE0027` on member-only data too, and signing in is what fixes that.
|
|
89
|
+
if (status === 403) return authState === "signedOut" ? "signIn" : "permission";
|
|
77
90
|
if (code === "offline_write_blocked") return "offline"; // a write refused before it left
|
|
78
91
|
// A web call that never reached Wix arrives as
|
|
79
92
|
// `HTTP 502: {"error":"webwix_direct_failed","detail":"Failed to fetch"}` — judge its detail.
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// The top-level 502 is the bridge wrapper, not the underlying transport result.
|
|
94
|
+
const detail = msg.includes("webwix_direct_failed")
|
|
95
|
+
? /"detail"\s*:\s*"([^"]+)"/.exec(msg)?.[1] ?? msg
|
|
96
|
+
: msg;
|
|
97
|
+
if (status && detail === msg) return "server";
|
|
98
|
+
if (TRANSPORT.test(detail)) return "offline";
|
|
82
99
|
return "server"; // anything else — a bug, a malformed row — is not the user's connection
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
// App-specific host integration. If the host does not expose auth state here,
|
|
103
|
+
// keep "unknown": a 401 then shows Sign in, which is what a session that ended needs.
|
|
104
|
+
function currentAuthState(): "signedIn" | "signedOut" | "unknown" { return "unknown"; }
|
|
105
|
+
|
|
85
106
|
async function load(s: State): Promise<State> {
|
|
86
107
|
try {
|
|
87
108
|
const items = (await fetchRows()).map(normalize);
|
|
88
109
|
return shape({ status: "ready", items });
|
|
89
110
|
} catch (e) {
|
|
90
|
-
const errorKind = classifyError(e);
|
|
111
|
+
const errorKind = classifyError(e, currentAuthState()); // use host/session state when available
|
|
91
112
|
// Keep the rows: a refresh that fails must not blank a screen that had content —
|
|
92
113
|
// unless access is gone, when the rows must not stay on screen either.
|
|
93
|
-
const lostAccess = errorKind === "signIn" || errorKind === "permission";
|
|
114
|
+
const lostAccess = errorKind === "signIn" || errorKind === "membership" || errorKind === "permission";
|
|
94
115
|
return shape({ ...s, items: lostAccess ? [] : s.items, status: "error", errorKind });
|
|
95
116
|
}
|
|
96
117
|
}
|
|
@@ -140,9 +161,16 @@ only when the screen shows the user something different for it.
|
|
|
140
161
|
"body": { "package": "himalaya", "type": "EmptyState",
|
|
141
162
|
"props": { "title": "Sign in to view products",
|
|
142
163
|
"subtitle": "Your session has ended.",
|
|
143
|
-
"action": { "actionId": "tier5.products.
|
|
164
|
+
"action": { "actionId": "tier5.products.openSignIn" },
|
|
144
165
|
"actionLabel": "Sign in" } },
|
|
145
166
|
"modifiers": { "hiddenPath": "$service:products.hideSignInError" } },
|
|
167
|
+
{ "id": "error.membership",
|
|
168
|
+
"body": { "package": "himalaya", "type": "EmptyState",
|
|
169
|
+
"props": { "title": "Join this site to view products",
|
|
170
|
+
"subtitle": "Use this site's join flow, or ask the owner for access.",
|
|
171
|
+
"action": { "actionId": "tier5.products.openJoin" },
|
|
172
|
+
"actionLabel": "Join this site" } },
|
|
173
|
+
"modifiers": { "hiddenPath": "$service:products.hideMembershipError" } },
|
|
146
174
|
{ "id": "error.permission", // no action: retrying gives the same answer
|
|
147
175
|
"body": { "package": "himalaya", "type": "EmptyState",
|
|
148
176
|
"props": { "title": "You need permission to view this screen",
|
|
@@ -164,10 +192,21 @@ only when the screen shows the user something different for it.
|
|
|
164
192
|
the binding reads without negation.
|
|
165
193
|
- Ready state is simply the data itself (the list rows, the detail fields) — it
|
|
166
194
|
needs no dedicated marker.
|
|
167
|
-
- **Sign in
|
|
168
|
-
sign-in `EmptyState` calls
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
- **Sign in uses a real auth flow.** A renderer cannot dispatch `himi.wix.*`, so
|
|
196
|
+
the sign-in `EmptyState` calls an app action that opens its configured auth gate
|
|
197
|
+
or sign-in screen, then reloads after authentication. Do not dispatch
|
|
198
|
+
`himi.wix.signInDirect` from this button: it requires the email and password
|
|
199
|
+
collected by a credential form (`himi docs wix-login-integration`).
|
|
200
|
+
- **Membership is not sign-in.** `NotSiteMember` (Android: `not a member of
|
|
201
|
+
site`) means the account must join this site. Route it to the membership state;
|
|
202
|
+
wire its action to the app's join/registration UI, or omit the action when no
|
|
203
|
+
such flow exists. A `403 WDE0027` is NOT membership: Wix Data returns it to a
|
|
204
|
+
signed-in member without access too, so it stays a permission error.
|
|
205
|
+
- **A 403 is a permission error** — the user lacks access and signing in again
|
|
206
|
+
changes nothing — unless the host's auth state says the user is signed out: a
|
|
207
|
+
visitor gets `403 WDE0027` on member-only data, and signing in fixes it. A 401 is
|
|
208
|
+
the session that ended — unless the host says the user is signed in, when it is a
|
|
209
|
+
permission refusal.
|
|
171
210
|
- **Loading is a `Skeleton` shaped like the loaded content** when you know the
|
|
172
211
|
layout — the same rows at about the same size. `StatefulSection` or
|
|
173
212
|
`ActivityIndicator` are fine where they fit better. A plain `"Loading…"` `Text`
|
|
@@ -194,7 +233,7 @@ only when the screen shows the user something different for it.
|
|
|
194
233
|
| loading | initial state (`status: "loading"`) | content-shaped skeleton; NO retry |
|
|
195
234
|
| error, nothing on screen | `catch` in the first load | the class's `EmptyState`: user-facing copy + a recovery action where one exists |
|
|
196
235
|
| error, rows on screen | `catch` in a refresh (offline or server) | rows stay; a `Banner` with **Try again** above them |
|
|
197
|
-
| sign-in or permission | `catch` in any load | rows go; the class's `EmptyState` (
|
|
236
|
+
| sign-in, membership or permission | `catch` in any load | rows go; the class's `EmptyState` (auth flow, join flow, or no action) |
|
|
198
237
|
| empty | derived: ready + 0 rows | say what *would* fill it, and how |
|
|
199
238
|
| ready | successful fetch | the content |
|
|
200
239
|
|
|
@@ -227,9 +266,10 @@ only when the screen shows the user something different for it.
|
|
|
227
266
|
(`dev/net-mocks.json`, CMS seed rows). `himi render <screenId> --no-mocks` shows
|
|
228
267
|
the initial frame — the loading state — and
|
|
229
268
|
`--state '{"products":{"status":"error","errorKind":"offline","hideOfflineError":false,…}}'`
|
|
230
|
-
forces an error class without the network. A `dev/net-mocks.json` rule
|
|
231
|
-
|
|
232
|
-
|
|
269
|
+
forces an error class without the network. A `dev/net-mocks.json` rule reaches
|
|
270
|
+
the worker only as `Wix <status> for …` — the hermetic crawl drops the response
|
|
271
|
+
body — so a 401, 403 or 500 rule previews the sign-in, permission and server
|
|
272
|
+
classes. Preview the membership class with `--state`.
|
|
233
273
|
|
|
234
274
|
## See also
|
|
235
275
|
|
package/guides/recipe-search.md
CHANGED
|
@@ -66,7 +66,7 @@ not say why there are no rows.
|
|
|
66
66
|
| Results | Matching rows, result count, and the available sort/filter controls. |
|
|
67
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 | 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,
|
|
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, the app's auth flow for sign-in, the site's join flow for `NotSiteMember`, and 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
|
|
|
@@ -172,9 +172,9 @@ interface PostsState {
|
|
|
172
172
|
query: string;
|
|
173
173
|
hideNoPosts: boolean; // the EmptyState for "loaded, and there is nothing at all"
|
|
174
174
|
hideNoMatches: boolean; // the EmptyState for "there are posts, none match the query"
|
|
175
|
-
errorKind: "" | "offline" | "server" | "signIn" | "permission"; // classifyError(e)
|
|
175
|
+
errorKind: "" | "offline" | "server" | "signIn" | "membership" | "permission"; // classifyError(e, currentAuthState())
|
|
176
176
|
hideLoading: boolean; hideOfflineError: boolean; hideServerError: boolean;
|
|
177
|
-
hideSignInError: boolean; hidePermissionError: boolean;
|
|
177
|
+
hideSignInError: boolean; hideMembershipError: boolean; hidePermissionError: boolean;
|
|
178
178
|
hideRefreshError: boolean; // hiddenPath flags, as in recipe-four-state
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -219,7 +219,7 @@ clear: (s) => { currentQuery = ""; return shape({ ...s, query: "", items: s.all
|
|
|
219
219
|
```
|
|
220
220
|
|
|
221
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`** —
|
|
222
|
+
On a `signIn`, `membership` or `permission` failure it clears **`all` as well as `items`** —
|
|
223
223
|
`search`, `clear` and `retry` rebuild rows from `all`, so rows left there come
|
|
224
224
|
back on the next keystroke.
|
|
225
225
|
- The shipped Blog v3 `WixPost` type includes `categoryIds` and both Wix-image and
|
|
@@ -261,8 +261,11 @@ A searchable list loads data, so it carries the four states of
|
|
|
261
261
|
while the first `load()` runs. The search field is usable at once.
|
|
262
262
|
- **Error, nothing loaded** — one `EmptyState` per class `classifyError` can
|
|
263
263
|
return, **no icon**, user-facing copy; offline and server get **Try again** →
|
|
264
|
-
`tier5.posts.retry`, sign-in
|
|
265
|
-
|
|
264
|
+
`tier5.posts.retry`, sign-in opens the app's configured auth flow, `NotSiteMember`
|
|
265
|
+
opens the site's join flow when the app provides one, and permission gets no
|
|
266
|
+
action. The `catch` sets `errorKind` from `classifyError(e, currentAuthState())`
|
|
267
|
+
(both from `himi docs recipe-four-state`); it never
|
|
268
|
+
shows `e.message`.
|
|
266
269
|
- **Retry keeps the query.** `retry` re-runs `load()` and then re-applies
|
|
267
270
|
`currentQuery` — not `s.query`, which is the query from when the retry
|
|
268
271
|
STARTED — so what the user typed during the fetch is not lost.
|
|
@@ -273,7 +276,8 @@ A searchable list loads data, so it carries the four states of
|
|
|
273
276
|
retry: async (s) => {
|
|
274
277
|
const next = await load(s); // classified on failure
|
|
275
278
|
// Lost access clears the rows; any other failure keeps them for the Banner.
|
|
276
|
-
const lostAccess = next.errorKind === "signIn" || next.errorKind === "
|
|
279
|
+
const lostAccess = next.errorKind === "signIn" || next.errorKind === "membership"
|
|
280
|
+
|| next.errorKind === "permission";
|
|
277
281
|
const all = lostAccess ? [] : next.all;
|
|
278
282
|
return shape({ ...next, all, query: currentQuery, items: visible(all, currentQuery) });
|
|
279
283
|
},
|
|
@@ -112,10 +112,16 @@ The first arg after `state` is the action's payload object: the descriptor's `Ac
|
|
|
112
112
|
| `Rating` | `onChange` | `{ value }` — the 1-based star index |
|
|
113
113
|
| `ColorPicker` | `onChange` | `{ value }` — the hex string |
|
|
114
114
|
| `DatePicker` | `onChange` | `{ value }` — unix **seconds**, string-encoded |
|
|
115
|
-
| `OtpInput` | `onChange`, `onComplete` | `{ value }` — the assembled code
|
|
116
|
-
| `ChipsInput` | `onRemove` | `{ value }` — the removed chip |
|
|
115
|
+
| `OtpInput` | `onChange`, `onComplete` | `{ value }` — the assembled code: `onChange` on every edit, `onComplete` once, on the edit that fills the last box |
|
|
116
|
+
| `ChipsInput` | `onAdd`, `onRemove` | `{ value }` — the new token (trimmed; committed by Return or a comma, and the field clears) / the removed chip. Append the token to `valuesPath` to show it |
|
|
117
117
|
| `Button` / `ListItem` | `action` / `onTap` | exactly the descriptor's `params` (no injected value) |
|
|
118
118
|
|
|
119
|
+
For a controlled `OtpInput`, bind `valuePath` and have `onChange` store `payload.value` in that
|
|
120
|
+
bound field. An `onComplete` reducer can then clear it to empty the boxes. Service subscriptions
|
|
121
|
+
report **changes**: returning an already-empty bound field without first storing the user's edit
|
|
122
|
+
is a no-op and does not reset the local input. A completion-only input can omit `valuePath` and
|
|
123
|
+
submit the assembled code directly from `payload.value`.
|
|
124
|
+
|
|
119
125
|
The payload rides in **`args`**. Some arms also mirror it into `params`; that is permitted only
|
|
120
126
|
for string-encoded values, because `Action.params` is `map<string,string>` in the proto. Every
|
|
121
127
|
host merges `{...params, ...args}` (args winning) before invoking the reducer, so a worker sees
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1476.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": "ca87854bfed096bc30428a79c429e7d947beb42046c8ac0d46dd613b"
|
|
75
75
|
}
|