@wix/himalaya-cli 0.1471.0 → 0.1473.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +6 -9
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/auth-gate-screen.d.ts +29 -0
- package/dist/cli.mjs +30 -18
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/branded-lite/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/community-club/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/community-club/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/community-club-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/community-club-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/events-meetups/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/events-meetups-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/events-meetups-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/gym-fitness/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/nonprofit-cause/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/owner-lite/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/owner-lite/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq-category-form/index.ts +3 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq-question-form/index.ts +4 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/podcast-creator/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/portfolio-creative/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/restaurant/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/restaurant-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/restaurant-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
- package/templates/retail-store/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/retail-store-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/retail-store-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +4 -4
- package/templates/yoga-studio/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-query.ts +6 -4
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +5 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-editor-value.ts +14 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +4 -4
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +4 -4
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +4 -3
|
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import type { DerivedAppMeta } from "./app-module.js";
|
|
3
3
|
import { type WixCachePolicy } from "./wix-cache-policy.js";
|
|
4
4
|
import type { NetMocksFile, WixMockRule } from "./wix-gateway.js";
|
|
5
|
-
import { gateScreenIdProblem } from "../../ts/src/auth-gate-screen.js";
|
|
5
|
+
import { gateScreenIdProblem, liftAuthBlock } from "../../ts/src/auth-gate-screen.js";
|
|
6
6
|
import { type UnmappedPushDestination } from "../../ts/src/wix-push-links.js";
|
|
7
7
|
import type { PushConfig, FlagConfig, AppControl, TabBarConfig, DrawerConfig, SidebarConfig, SplashConfig, LocaleConfig, NavKind, OrientationConfig, StreamPolicy } from "@wix/himalaya/core";
|
|
8
8
|
export type { PushConfig, FlagConfig, AppControl, TabBarConfig, DrawerConfig, SidebarConfig, SplashConfig, LocaleConfig, NavKind, OrientationConfig, StreamPolicy };
|
|
@@ -202,14 +202,11 @@ export { gateScreenIdProblem };
|
|
|
202
202
|
/** Derive the OTA authConfig block from a defineApp auth block
|
|
203
203
|
* ({ providerId, config }). `required` is read from `config.required === true`
|
|
204
204
|
* and lifted to the top level, as is `config.gateScreenId` (#4651); secret-ish
|
|
205
|
-
* keys are scrubbed
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* `himalaya-dev app validate` is where a bad id fails ([gateScreenIdProblem]). */
|
|
209
|
-
export declare function deriveAuthConfig(auth:
|
|
210
|
-
providerId: string;
|
|
211
|
-
config: Record<string, unknown>;
|
|
212
|
-
} | undefined, screenIds?: readonly string[]): AuthConfigBlock | undefined;
|
|
205
|
+
* keys are scrubbed — the same `liftAuthBlock` the auth bake runs (#4818). When
|
|
206
|
+
* the app's screens are given, a `gateScreenId` naming none of them is DROPPED
|
|
207
|
+
* (the shell falls back to the stock login) and warned about — never thrown: this
|
|
208
|
+
* runs on every app-config serve and hot reload, and `himalaya-dev app validate` is where a bad id fails ([gateScreenIdProblem]). */
|
|
209
|
+
export declare function deriveAuthConfig(auth: Parameters<typeof liftAuthBlock>[0], screenIds?: readonly string[]): AuthConfigBlock | undefined;
|
|
213
210
|
/** Derive a baseline NavigationConfig from the loaded app's derived meta. */
|
|
214
211
|
/** What `deriveNavigationConfig` needs to finish an app-level nav bundle ref.
|
|
215
212
|
*
|
|
@@ -8,3 +8,32 @@ export declare function gateScreenIdProblem(auth: {
|
|
|
8
8
|
providerId: string;
|
|
9
9
|
config: Record<string, unknown>;
|
|
10
10
|
} | undefined, screenIds: readonly string[]): string | null;
|
|
11
|
+
/** Keys never lifted into a client auth block — the baked auth.json or the OTA authConfig (defense in
|
|
12
|
+
* depth: public OAuth client ids are safe; anything secret-shaped is not). */
|
|
13
|
+
export declare const AUTH_SECRET_KEY_RE: RegExp;
|
|
14
|
+
/** The client auth block: what auth.json bakes and what the OTA `authConfig` serves. */
|
|
15
|
+
export interface LiftedAuthBlock {
|
|
16
|
+
providerId: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
/** #4651 — the app's own auth gate screen; absent ⇒ the stock login. */
|
|
19
|
+
gateScreenId?: string;
|
|
20
|
+
config: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** #4818 — the one lift from an auth block to the client shape, shared by the auth bake and the OTA
|
|
23
|
+
* authConfig so the two cannot drift. Accepts the defineApp shape (`config.required`,
|
|
24
|
+
* `config.gateScreenId`) AND an already-lifted block (top-level `required` / `gateScreenId`, as an
|
|
25
|
+
* existing auth.json or a seed's `authConfig` carries them): re-lifting a lifted block is the
|
|
26
|
+
* identity, so a re-bake never fails open. `required` is true when either place says so; a
|
|
27
|
+
* `config.gateScreenId` wins over a top-level one. Secret-ish config keys are scrubbed. Returns null
|
|
28
|
+
* when there is no usable providerId. */
|
|
29
|
+
export declare function liftAuthBlock(auth: {
|
|
30
|
+
providerId?: unknown;
|
|
31
|
+
required?: unknown;
|
|
32
|
+
gateScreenId?: unknown;
|
|
33
|
+
config?: Record<string, unknown>;
|
|
34
|
+
} | undefined | null): LiftedAuthBlock | null;
|
|
35
|
+
/** The raw gate id of either auth shape (`config.gateScreenId` first), unvalidated — for the checks. */
|
|
36
|
+
export declare function liftedGateScreenId(auth: {
|
|
37
|
+
gateScreenId?: unknown;
|
|
38
|
+
config?: Record<string, unknown>;
|
|
39
|
+
}): unknown;
|
package/dist/cli.mjs
CHANGED
|
@@ -10034,7 +10034,6 @@ function webViewOriginAllowed(url, origins) {
|
|
|
10034
10034
|
}
|
|
10035
10035
|
var init_webview_origin = __esm({
|
|
10036
10036
|
"../../core/ts/src/webview-origin.ts"() {
|
|
10037
|
-
"use strict";
|
|
10038
10037
|
}
|
|
10039
10038
|
});
|
|
10040
10039
|
|
|
@@ -31153,8 +31152,29 @@ function gateScreenIdProblem(auth, screenIds) {
|
|
|
31153
31152
|
if (screenIds.includes(gateId)) return null;
|
|
31154
31153
|
return `defineApp: auth.config.gateScreenId is "${gateId}", which is not one of the app's exact screens \u2014 a required-auth launch would have no login to show (a release serves exact screens, so a templated route 404s). Name an exact screen, or drop gateScreenId to use the stock login.`;
|
|
31155
31154
|
}
|
|
31155
|
+
function liftAuthBlock(auth) {
|
|
31156
|
+
if (!auth || typeof auth.providerId !== "string" || auth.providerId.length === 0) return null;
|
|
31157
|
+
const config = {};
|
|
31158
|
+
for (const [key3, value] of Object.entries(auth.config ?? {})) {
|
|
31159
|
+
if (key3 === "required" || key3 === "gateScreenId") continue;
|
|
31160
|
+
if (AUTH_SECRET_KEY_RE.test(key3)) continue;
|
|
31161
|
+
config[key3] = value;
|
|
31162
|
+
}
|
|
31163
|
+
const gateScreenId = liftedGateScreenId(auth);
|
|
31164
|
+
return {
|
|
31165
|
+
providerId: auth.providerId,
|
|
31166
|
+
required: auth.config?.required === true || auth.required === true,
|
|
31167
|
+
...typeof gateScreenId === "string" && gateScreenId.length > 0 ? { gateScreenId } : {},
|
|
31168
|
+
config
|
|
31169
|
+
};
|
|
31170
|
+
}
|
|
31171
|
+
function liftedGateScreenId(auth) {
|
|
31172
|
+
return auth.config?.gateScreenId ?? auth.gateScreenId;
|
|
31173
|
+
}
|
|
31174
|
+
var AUTH_SECRET_KEY_RE;
|
|
31156
31175
|
var init_auth_gate_screen = __esm({
|
|
31157
31176
|
"../../core/ts/src/auth-gate-screen.ts"() {
|
|
31177
|
+
AUTH_SECRET_KEY_RE = /secret|private/i;
|
|
31158
31178
|
}
|
|
31159
31179
|
});
|
|
31160
31180
|
|
|
@@ -31350,22 +31370,15 @@ function deriveTelemetryConfig(app) {
|
|
|
31350
31370
|
return cfg;
|
|
31351
31371
|
}
|
|
31352
31372
|
function deriveAuthConfig(auth, screenIds) {
|
|
31353
|
-
|
|
31354
|
-
|
|
31355
|
-
|
|
31356
|
-
|
|
31357
|
-
|
|
31358
|
-
config
|
|
31373
|
+
const lifted = liftAuthBlock(auth);
|
|
31374
|
+
if (!lifted) return void 0;
|
|
31375
|
+
const gateScreenId = liftedGateScreenId(auth);
|
|
31376
|
+
const problem = gateScreenId !== void 0 && screenIds ? gateScreenIdProblem({ providerId: lifted.providerId, config: { gateScreenId } }, screenIds) : null;
|
|
31377
|
+
if (problem) {
|
|
31378
|
+
console.warn(`[app-config] ${problem} \u2014 serving the stock login instead.`);
|
|
31379
|
+
delete lifted.gateScreenId;
|
|
31359
31380
|
}
|
|
31360
|
-
|
|
31361
|
-
const problem = typeof gateScreenId === "string" && screenIds ? gateScreenIdProblem(auth, screenIds) : null;
|
|
31362
|
-
if (problem) console.warn(`[app-config] ${problem} \u2014 serving the stock login instead.`);
|
|
31363
|
-
return {
|
|
31364
|
-
providerId: auth.providerId,
|
|
31365
|
-
required: auth.config?.required === true,
|
|
31366
|
-
...typeof gateScreenId === "string" && gateScreenId.length > 0 && !problem ? { gateScreenId } : {},
|
|
31367
|
-
config
|
|
31368
|
-
};
|
|
31381
|
+
return lifted;
|
|
31369
31382
|
}
|
|
31370
31383
|
function expandNavBundle(ref, ctx) {
|
|
31371
31384
|
const url = ref.url ?? "";
|
|
@@ -31559,7 +31572,7 @@ function handleAppConfigRequest(req, res, meta, override, bundleCtx, devNetMocks
|
|
|
31559
31572
|
});
|
|
31560
31573
|
res.end(body);
|
|
31561
31574
|
}
|
|
31562
|
-
var SHARED_WIX_SENTRY_DSN,
|
|
31575
|
+
var SHARED_WIX_SENTRY_DSN, NAV_OVERRIDES;
|
|
31563
31576
|
var init_app_config = __esm({
|
|
31564
31577
|
"../../core/dev-server/src/app-config.ts"() {
|
|
31565
31578
|
init_wix_cache_policy();
|
|
@@ -31568,7 +31581,6 @@ var init_app_config = __esm({
|
|
|
31568
31581
|
init_preview_surfaces();
|
|
31569
31582
|
init_wix_push_links();
|
|
31570
31583
|
SHARED_WIX_SENTRY_DSN = "https://5dfa4f23057c43149b6219c07d177472@sentry.io/84240";
|
|
31571
|
-
AUTH_SECRET_KEY_RE = /secret|private/i;
|
|
31572
31584
|
NAV_OVERRIDES = {
|
|
31573
31585
|
reference: {
|
|
31574
31586
|
deepLinkPatterns: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1473.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": "f58b0d57918c163a9918dbe6f1170839ea6c6d6cf0d2b9c38b7a7004"
|
|
75
75
|
}
|
|
@@ -66,9 +66,10 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
66
66
|
const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
|
|
67
67
|
const emit = () => {
|
|
68
68
|
if (disposed || revalidating === generation) return;
|
|
69
|
-
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept
|
|
69
|
+
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
|
|
70
|
+
// with nothing in it comes back as its empty state, never its loading look.
|
|
70
71
|
if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
|
|
71
|
-
lastShown =
|
|
72
|
+
lastShown = { state: snapshot(), loadedPages };
|
|
72
73
|
}
|
|
73
74
|
publish(snapshot());
|
|
74
75
|
};
|
|
@@ -161,12 +162,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
161
162
|
return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
|
|
162
163
|
},
|
|
163
164
|
/** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
|
|
164
|
-
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page
|
|
165
|
+
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
|
|
166
|
+
* empty one is the list's empty state. */
|
|
165
167
|
seed(page: unknown): boolean {
|
|
166
168
|
const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
|
|
167
169
|
const restored = candidate?.state;
|
|
168
170
|
if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
|
|
169
|
-
|| !Array.isArray(restored.items) ||
|
|
171
|
+
|| !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
|
|
170
172
|
try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
|
|
171
173
|
state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
|
|
172
174
|
loadingMore: false, pageError: false, provisional: false, error: '' };
|
|
@@ -352,11 +352,14 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
352
352
|
const handle = (options.register ?? register)(name, definition);
|
|
353
353
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
354
354
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
355
|
-
// starts its first load as before, never from rows it chose not to restore.
|
|
355
|
+
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
356
|
+
// list's own only when the page it showed was empty too; a page with rows means they were kept out.
|
|
356
357
|
const restored = (handle as { getState?: () => State }).getState?.();
|
|
357
358
|
const rowsKey = COLLECTIONS[name]?.rows;
|
|
358
359
|
const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
|
|
359
|
-
|
|
360
|
+
const shownItems = (restored?.collectionShown as { state?: { items?: unknown } } | undefined)?.state?.items;
|
|
361
|
+
const shownEmpty = Array.isArray(shownItems) && shownItems.length === 0;
|
|
362
|
+
if (Array.isArray(restoredRows) && (restoredRows.length > 0 || shownEmpty)) controller.seed(restored?.collectionShown);
|
|
360
363
|
publish = () => handle.collectionDataChanged();
|
|
361
364
|
return handle;
|
|
362
365
|
}
|
|
@@ -66,9 +66,10 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
66
66
|
const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
|
|
67
67
|
const emit = () => {
|
|
68
68
|
if (disposed || revalidating === generation) return;
|
|
69
|
-
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept
|
|
69
|
+
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
|
|
70
|
+
// with nothing in it comes back as its empty state, never its loading look.
|
|
70
71
|
if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
|
|
71
|
-
lastShown =
|
|
72
|
+
lastShown = { state: snapshot(), loadedPages };
|
|
72
73
|
}
|
|
73
74
|
publish(snapshot());
|
|
74
75
|
};
|
|
@@ -161,12 +162,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
161
162
|
return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
|
|
162
163
|
},
|
|
163
164
|
/** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
|
|
164
|
-
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page
|
|
165
|
+
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
|
|
166
|
+
* empty one is the list's empty state. */
|
|
165
167
|
seed(page: unknown): boolean {
|
|
166
168
|
const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
|
|
167
169
|
const restored = candidate?.state;
|
|
168
170
|
if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
|
|
169
|
-
|| !Array.isArray(restored.items) ||
|
|
171
|
+
|| !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
|
|
170
172
|
try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
|
|
171
173
|
state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
|
|
172
174
|
loadingMore: false, pageError: false, provisional: false, error: '' };
|
|
@@ -352,11 +352,14 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
352
352
|
const handle = (options.register ?? register)(name, definition);
|
|
353
353
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
354
354
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
355
|
-
// starts its first load as before, never from rows it chose not to restore.
|
|
355
|
+
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
356
|
+
// list's own only when the page it showed was empty too; a page with rows means they were kept out.
|
|
356
357
|
const restored = (handle as { getState?: () => State }).getState?.();
|
|
357
358
|
const rowsKey = COLLECTIONS[name]?.rows;
|
|
358
359
|
const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
|
|
359
|
-
|
|
360
|
+
const shownItems = (restored?.collectionShown as { state?: { items?: unknown } } | undefined)?.state?.items;
|
|
361
|
+
const shownEmpty = Array.isArray(shownItems) && shownItems.length === 0;
|
|
362
|
+
if (Array.isArray(restoredRows) && (restoredRows.length > 0 || shownEmpty)) controller.seed(restored?.collectionShown);
|
|
360
363
|
publish = () => handle.collectionDataChanged();
|
|
361
364
|
return handle;
|
|
362
365
|
}
|
|
@@ -140,6 +140,20 @@ export function liveState(svc: unknown): Record<string, unknown> | undefined {
|
|
|
140
140
|
* state it started from) are written, so anything the owner did while it awaited — an edit, a field in
|
|
141
141
|
* the details sheet, closing the sheet — survives a save that lands after it, success or failure. A
|
|
142
142
|
* key both changed goes to the action. Where the host gives no live state, the result as it is. */
|
|
143
|
+
/** An awaited load's result, unless the form moved to another record while it read. On a warm return the host's bare
|
|
144
|
+
* refresh and the screen's onAppear run together: after a save the refresh fetches the record just saved while the
|
|
145
|
+
* onAppear of a create screen opens a blank form, and the refresh landing last turned the new form into that record's
|
|
146
|
+
* edit. Nothing lands then (`undefined`, the runtime's no-op), nor from a load that changes nothing: started with the
|
|
147
|
+
* same state as the onAppear, it would put back the record that one replaced. A load for the record the form still
|
|
148
|
+
* holds lands as it is; so does any load where the host gives no live state. */
|
|
149
|
+
export function landsOnLive<S extends object>(svc: unknown, start: S, result: S): S | undefined {
|
|
150
|
+
if (result === start) return undefined;
|
|
151
|
+
const now = liveState(svc);
|
|
152
|
+
if (!now || now === start) return result;
|
|
153
|
+
const s0 = start as Record<string, unknown>;
|
|
154
|
+
return now.id !== s0.id || now.mode !== s0.mode ? undefined : result;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
export function settleOverLive<S extends object>(svc: unknown, start: S, result: S): S {
|
|
144
158
|
const now = liveState(svc) as S | undefined;
|
|
145
159
|
if (!now || now === start || now === result) return result;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// screen (himiOwnerLitePost.publish). Bound by buildForm ($service:himiOwnerLitePostForm.*).
|
|
5
5
|
|
|
6
6
|
import { postBodyText, plainPostBody, postCoverUrl } from "../_shared/owner-post-content.js";
|
|
7
|
-
import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm } from "../_shared/owner-editor-value.js";
|
|
7
|
+
import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm, landsOnLive } from "../_shared/owner-editor-value.js";
|
|
8
8
|
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
9
9
|
import { wixCall } from "@himalaya/wix-site-media";
|
|
10
10
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
|
|
@@ -157,7 +157,7 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
157
157
|
ricosUploadService("himiOwnerLitePostFormUploads");
|
|
158
158
|
|
|
159
159
|
const svc = defineService<PostFormState, {
|
|
160
|
-
load: (s: PostFormState, arg?: unknown) => Promise<PostFormState>;
|
|
160
|
+
load: (s: PostFormState, arg?: unknown) => Promise<PostFormState | undefined>;
|
|
161
161
|
setCover: (s: PostFormState, arg?: unknown) => PostFormState;
|
|
162
162
|
setExcerpt: (s: PostFormState, arg?: unknown) => PostFormState;
|
|
163
163
|
detailsOpen: (s: PostFormState) => PostFormState;
|
|
@@ -173,7 +173,7 @@ const svc = defineService<PostFormState, {
|
|
|
173
173
|
// perVisit: a cached embed's warm return starts this run fresh, as a cold boot does (`__resetRunState`).
|
|
174
174
|
perVisit: [...editorRunKeys("body"), "detailsOpen"],
|
|
175
175
|
actions: {
|
|
176
|
-
load: async (s, arg) => {
|
|
176
|
+
load: async (s, arg): Promise<PostFormState | undefined> => landsOnLive(svc, s, await (async () => {
|
|
177
177
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
178
178
|
// WS-C — nav-param input is PRIMARY (the canonical create-or-edit shell
|
|
179
179
|
// bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
|
|
@@ -240,7 +240,7 @@ const svc = defineService<PostFormState, {
|
|
|
240
240
|
}
|
|
241
241
|
return shape({ ...s, mode: "edit", id, status: "error", error: ownerLoadError(e) });
|
|
242
242
|
}
|
|
243
|
-
},
|
|
243
|
+
})()),
|
|
244
244
|
setCover: (s, arg) => shape({ ...s, coverUrl: valueOf(arg).trim(), error: "" }),
|
|
245
245
|
setExcerpt: (s, arg) => shape({ ...s, excerpt: valueOf(arg), error: "" }),
|
|
246
246
|
detailsOpen: (s) => shape({ ...s, detailsOpen: true }),
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, type DbEntity } from "../_shared/owner-local-db.js";
|
|
19
19
|
import { htmlIsTrivial, htmlPreviewText } from "../_shared/owner-rich-guard.js";
|
|
20
20
|
import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
|
|
21
|
-
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm } from "../_shared/owner-editor-value.js";
|
|
21
|
+
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm, landsOnLive } from "../_shared/owner-editor-value.js";
|
|
22
22
|
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
23
23
|
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
24
24
|
import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
|
|
@@ -190,7 +190,7 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
190
190
|
ricosUploadService("himiOwnerLiteProductFormUploads");
|
|
191
191
|
|
|
192
192
|
const svc = defineService<ProductFormState, {
|
|
193
|
-
load: (s: ProductFormState, arg?: unknown) => Promise<ProductFormState>;
|
|
193
|
+
load: (s: ProductFormState, arg?: unknown) => Promise<ProductFormState | undefined>;
|
|
194
194
|
descriptionChanged: (s: ProductFormState, arg?: unknown) => ProductFormState;
|
|
195
195
|
submit: (s: ProductFormState, values?: Record<string, unknown>) => Promise<ProductFormState>;
|
|
196
196
|
remove: (s: ProductFormState) => Promise<ProductFormState>;
|
|
@@ -202,7 +202,7 @@ const svc = defineService<ProductFormState, {
|
|
|
202
202
|
// perVisit: a cached embed's warm return starts this run fresh, as a cold boot does (`__resetRunState`).
|
|
203
203
|
perVisit: editorRunKeys("description"),
|
|
204
204
|
actions: {
|
|
205
|
-
load: async (s, arg) => {
|
|
205
|
+
load: async (s, arg): Promise<ProductFormState | undefined> => landsOnLive(svc, s, await (async () => {
|
|
206
206
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
207
207
|
// WS-C — nav-param input is PRIMARY (the canonical create-or-edit shell
|
|
208
208
|
// bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
|
|
@@ -287,7 +287,7 @@ const svc = defineService<ProductFormState, {
|
|
|
287
287
|
}
|
|
288
288
|
return shape({ ...s, mode: "edit", id, status: "error", error: ownerLoadError(e) });
|
|
289
289
|
}
|
|
290
|
-
},
|
|
290
|
+
})()),
|
|
291
291
|
// The editor's on_change carries `args.dirty` only; the document stays in the form bag.
|
|
292
292
|
descriptionChanged: (s) => shape({ ...s, descriptionDirty: true, descriptionEdits: s.descriptionEdits + 1, ...editorChanged(s) }),
|
|
293
293
|
// Laid over the live state ([rico] #3644): what the owner did while the save awaited survives it.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// expose, so this form only edits an existing service's name/description via
|
|
5
5
|
// updateService. Bound by buildForm ($service:himiOwnerLiteServiceForm.*).
|
|
6
6
|
|
|
7
|
+
import { landsOnLive } from "../_shared/owner-editor-value.js";
|
|
7
8
|
import { defineService } from "@himalaya/state";
|
|
8
9
|
import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
|
|
9
10
|
import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
|
|
@@ -108,12 +109,12 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
const svc = defineService<ServiceFormState, {
|
|
111
|
-
load: (s: ServiceFormState, arg?: unknown) => Promise<ServiceFormState>;
|
|
112
|
+
load: (s: ServiceFormState, arg?: unknown) => Promise<ServiceFormState | undefined>;
|
|
112
113
|
submit: (s: ServiceFormState, values?: Record<string, unknown>) => Promise<ServiceFormState>;
|
|
113
114
|
}>("himiOwnerLiteServiceForm", {
|
|
114
115
|
state: shape({ id: initialId }),
|
|
115
116
|
actions: {
|
|
116
|
-
load: async (s, arg) => {
|
|
117
|
+
load: async (s, arg): Promise<ServiceFormState | undefined> => landsOnLive(svc, s, await (async () => {
|
|
117
118
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
118
119
|
// WS-C — nav-param input is PRIMARY (the canonical form bakes no id); the
|
|
119
120
|
// onAppear `id` param is the #570 suffixed-build fallback.
|
|
@@ -167,7 +168,7 @@ const svc = defineService<ServiceFormState, {
|
|
|
167
168
|
}
|
|
168
169
|
return shape({ ...s, id, status: "error", error: ownerLoadError(e) });
|
|
169
170
|
}
|
|
170
|
-
},
|
|
171
|
+
})()),
|
|
171
172
|
submit: async (s, values) => {
|
|
172
173
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
173
174
|
const v = values ?? {};
|
|
@@ -66,9 +66,10 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
66
66
|
const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
|
|
67
67
|
const emit = () => {
|
|
68
68
|
if (disposed || revalidating === generation) return;
|
|
69
|
-
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept
|
|
69
|
+
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
|
|
70
|
+
// with nothing in it comes back as its empty state, never its loading look.
|
|
70
71
|
if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
|
|
71
|
-
lastShown =
|
|
72
|
+
lastShown = { state: snapshot(), loadedPages };
|
|
72
73
|
}
|
|
73
74
|
publish(snapshot());
|
|
74
75
|
};
|
|
@@ -161,12 +162,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
161
162
|
return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
|
|
162
163
|
},
|
|
163
164
|
/** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
|
|
164
|
-
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page
|
|
165
|
+
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
|
|
166
|
+
* empty one is the list's empty state. */
|
|
165
167
|
seed(page: unknown): boolean {
|
|
166
168
|
const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
|
|
167
169
|
const restored = candidate?.state;
|
|
168
170
|
if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
|
|
169
|
-
|| !Array.isArray(restored.items) ||
|
|
171
|
+
|| !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
|
|
170
172
|
try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
|
|
171
173
|
state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
|
|
172
174
|
loadingMore: false, pageError: false, provisional: false, error: '' };
|
|
@@ -352,11 +352,14 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
352
352
|
const handle = (options.register ?? register)(name, definition);
|
|
353
353
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
354
354
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
355
|
-
// starts its first load as before, never from rows it chose not to restore.
|
|
355
|
+
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
356
|
+
// list's own only when the page it showed was empty too; a page with rows means they were kept out.
|
|
356
357
|
const restored = (handle as { getState?: () => State }).getState?.();
|
|
357
358
|
const rowsKey = COLLECTIONS[name]?.rows;
|
|
358
359
|
const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
|
|
359
|
-
|
|
360
|
+
const shownItems = (restored?.collectionShown as { state?: { items?: unknown } } | undefined)?.state?.items;
|
|
361
|
+
const shownEmpty = Array.isArray(shownItems) && shownItems.length === 0;
|
|
362
|
+
if (Array.isArray(restoredRows) && (restoredRows.length > 0 || shownEmpty)) controller.seed(restored?.collectionShown);
|
|
360
363
|
publish = () => handle.collectionDataChanged();
|
|
361
364
|
return handle;
|
|
362
365
|
}
|
|
@@ -66,9 +66,10 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
66
66
|
const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
|
|
67
67
|
const emit = () => {
|
|
68
68
|
if (disposed || revalidating === generation) return;
|
|
69
|
-
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept
|
|
69
|
+
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
|
|
70
|
+
// with nothing in it comes back as its empty state, never its loading look.
|
|
70
71
|
if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
|
|
71
|
-
lastShown =
|
|
72
|
+
lastShown = { state: snapshot(), loadedPages };
|
|
72
73
|
}
|
|
73
74
|
publish(snapshot());
|
|
74
75
|
};
|
|
@@ -161,12 +162,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
161
162
|
return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
|
|
162
163
|
},
|
|
163
164
|
/** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
|
|
164
|
-
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page
|
|
165
|
+
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
|
|
166
|
+
* empty one is the list's empty state. */
|
|
165
167
|
seed(page: unknown): boolean {
|
|
166
168
|
const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
|
|
167
169
|
const restored = candidate?.state;
|
|
168
170
|
if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
|
|
169
|
-
|| !Array.isArray(restored.items) ||
|
|
171
|
+
|| !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
|
|
170
172
|
try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
|
|
171
173
|
state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
|
|
172
174
|
loadingMore: false, pageError: false, provisional: false, error: '' };
|
|
@@ -352,11 +352,14 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
352
352
|
const handle = (options.register ?? register)(name, definition);
|
|
353
353
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
354
354
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
355
|
-
// starts its first load as before, never from rows it chose not to restore.
|
|
355
|
+
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
356
|
+
// list's own only when the page it showed was empty too; a page with rows means they were kept out.
|
|
356
357
|
const restored = (handle as { getState?: () => State }).getState?.();
|
|
357
358
|
const rowsKey = COLLECTIONS[name]?.rows;
|
|
358
359
|
const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
|
|
359
|
-
|
|
360
|
+
const shownItems = (restored?.collectionShown as { state?: { items?: unknown } } | undefined)?.state?.items;
|
|
361
|
+
const shownEmpty = Array.isArray(shownItems) && shownItems.length === 0;
|
|
362
|
+
if (Array.isArray(restoredRows) && (restoredRows.length > 0 || shownEmpty)) controller.seed(restored?.collectionShown);
|
|
360
363
|
publish = () => handle.collectionDataChanged();
|
|
361
364
|
return handle;
|
|
362
365
|
}
|
|
@@ -66,9 +66,10 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
66
66
|
const snapshot = (): CollectionQueryState<Row> => ({ ...state, criteria: copyQuery(state.criteria), items: [...state.items] });
|
|
67
67
|
const emit = () => {
|
|
68
68
|
if (disposed || revalidating === generation) return;
|
|
69
|
-
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept
|
|
69
|
+
// A settled answer replaces it, an empty one included: rows the live query proved gone are never kept, and a list
|
|
70
|
+
// with nothing in it comes back as its empty state, never its loading look.
|
|
70
71
|
if (state.status === 'ready' && !state.pageError && !state.loadingMore && !state.provisional) {
|
|
71
|
-
lastShown =
|
|
72
|
+
lastShown = { state: snapshot(), loadedPages };
|
|
72
73
|
}
|
|
73
74
|
publish(snapshot());
|
|
74
75
|
};
|
|
@@ -161,12 +162,13 @@ export function createCollectionQuery<Row>(source: CollectionQuerySource<Row>, p
|
|
|
161
162
|
return { state: { ...page, criteria: copyQuery(page.criteria), items: [...page.items] }, loadedPages: lastShown.loadedPages };
|
|
162
163
|
},
|
|
163
164
|
/** A cold boot restored from disk: what the last run showed becomes what this one shows, so its first `load`
|
|
164
|
-
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page
|
|
165
|
+
* revalidates behind it instead of starting empty. Only before the first query, and only a well-formed page; an
|
|
166
|
+
* empty one is the list's empty state. */
|
|
165
167
|
seed(page: unknown): boolean {
|
|
166
168
|
const candidate = page && typeof page === 'object' ? page as Partial<CollectionShownPage<Row>> : undefined;
|
|
167
169
|
const restored = candidate?.state;
|
|
168
170
|
if (generation !== 0 || state.status !== 'idle' || !restored || restored.status !== 'ready'
|
|
169
|
-
|| !Array.isArray(restored.items) ||
|
|
171
|
+
|| !Array.isArray(restored.items) || typeof restored.nextCursor !== 'string') return false;
|
|
170
172
|
try { restored.items.forEach(item => { if (!source.id(item)) throw new Error('A backend collection row requires an identity'); }); } catch { return false; }
|
|
171
173
|
state = { ...state, ...restored, criteria: normalize(restored.criteria ?? {}), items: [...restored.items], status: 'ready',
|
|
172
174
|
loadingMore: false, pageError: false, provisional: false, error: '' };
|
|
@@ -352,11 +352,14 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
352
352
|
const handle = (options.register ?? register)(name, definition);
|
|
353
353
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
354
354
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
355
|
-
// starts its first load as before, never from rows it chose not to restore.
|
|
355
|
+
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
356
|
+
// list's own only when the page it showed was empty too; a page with rows means they were kept out.
|
|
356
357
|
const restored = (handle as { getState?: () => State }).getState?.();
|
|
357
358
|
const rowsKey = COLLECTIONS[name]?.rows;
|
|
358
359
|
const restoredRows = rowsKey ? restored?.[rowsKey] : undefined;
|
|
359
|
-
|
|
360
|
+
const shownItems = (restored?.collectionShown as { state?: { items?: unknown } } | undefined)?.state?.items;
|
|
361
|
+
const shownEmpty = Array.isArray(shownItems) && shownItems.length === 0;
|
|
362
|
+
if (Array.isArray(restoredRows) && (restoredRows.length > 0 || shownEmpty)) controller.seed(restored?.collectionShown);
|
|
360
363
|
publish = () => handle.collectionDataChanged();
|
|
361
364
|
return handle;
|
|
362
365
|
}
|
|
@@ -140,6 +140,20 @@ export function liveState(svc: unknown): Record<string, unknown> | undefined {
|
|
|
140
140
|
* state it started from) are written, so anything the owner did while it awaited — an edit, a field in
|
|
141
141
|
* the details sheet, closing the sheet — survives a save that lands after it, success or failure. A
|
|
142
142
|
* key both changed goes to the action. Where the host gives no live state, the result as it is. */
|
|
143
|
+
/** An awaited load's result, unless the form moved to another record while it read. On a warm return the host's bare
|
|
144
|
+
* refresh and the screen's onAppear run together: after a save the refresh fetches the record just saved while the
|
|
145
|
+
* onAppear of a create screen opens a blank form, and the refresh landing last turned the new form into that record's
|
|
146
|
+
* edit. Nothing lands then (`undefined`, the runtime's no-op), nor from a load that changes nothing: started with the
|
|
147
|
+
* same state as the onAppear, it would put back the record that one replaced. A load for the record the form still
|
|
148
|
+
* holds lands as it is; so does any load where the host gives no live state. */
|
|
149
|
+
export function landsOnLive<S extends object>(svc: unknown, start: S, result: S): S | undefined {
|
|
150
|
+
if (result === start) return undefined;
|
|
151
|
+
const now = liveState(svc);
|
|
152
|
+
if (!now || now === start) return result;
|
|
153
|
+
const s0 = start as Record<string, unknown>;
|
|
154
|
+
return now.id !== s0.id || now.mode !== s0.mode ? undefined : result;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
export function settleOverLive<S extends object>(svc: unknown, start: S, result: S): S {
|
|
144
158
|
const now = liveState(svc) as S | undefined;
|
|
145
159
|
if (!now || now === start || now === result) return result;
|
|
@@ -10,7 +10,7 @@ import { defineService } from "@himalaya/state";
|
|
|
10
10
|
import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
|
|
11
11
|
import { normalizeEvent } from "../_shared/himi-events.js";
|
|
12
12
|
import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
|
|
13
|
-
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm } from "../_shared/owner-editor-value.js";
|
|
13
|
+
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm, landsOnLive } from "../_shared/owner-editor-value.js";
|
|
14
14
|
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
15
15
|
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
16
16
|
import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
|
|
@@ -169,7 +169,7 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
169
169
|
ricosUploadService("himiOwnerLiteEventFormUploads");
|
|
170
170
|
|
|
171
171
|
const svc = defineService<EventFormState, {
|
|
172
|
-
load: (s: EventFormState, arg?: unknown) => Promise<EventFormState>;
|
|
172
|
+
load: (s: EventFormState, arg?: unknown) => Promise<EventFormState | undefined>;
|
|
173
173
|
descriptionChanged: (s: EventFormState, arg?: unknown) => EventFormState;
|
|
174
174
|
submit: (s: EventFormState, values?: Record<string, unknown>) => Promise<EventFormState>;
|
|
175
175
|
}>("himiOwnerLiteEventForm", {
|
|
@@ -179,7 +179,7 @@ const svc = defineService<EventFormState, {
|
|
|
179
179
|
// perVisit: a cached embed's warm return starts this run fresh, as a cold boot does (`__resetRunState`).
|
|
180
180
|
perVisit: editorRunKeys("description"),
|
|
181
181
|
actions: {
|
|
182
|
-
load: async (s, arg) => {
|
|
182
|
+
load: async (s, arg): Promise<EventFormState | undefined> => landsOnLive(svc, s, await (async () => {
|
|
183
183
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
184
184
|
const mode = modeFrom(arg, s.mode);
|
|
185
185
|
// WS-C — nav-param input is PRIMARY (the canonical form bakes no id); the
|
|
@@ -262,7 +262,7 @@ const svc = defineService<EventFormState, {
|
|
|
262
262
|
}
|
|
263
263
|
return shape({ ...s, mode, id, status: "error", error: ownerLoadError(e) });
|
|
264
264
|
}
|
|
265
|
-
},
|
|
265
|
+
})()),
|
|
266
266
|
// The editor's on_change carries `args.dirty` only; the document stays in the form bag.
|
|
267
267
|
descriptionChanged: (s) => shape({ ...s, descriptionDirty: true, descriptionEdits: s.descriptionEdits + 1, ...editorChanged(s) }),
|
|
268
268
|
// Laid over the live state ([rico] #3644): what the owner did while the save awaited survives it.
|