@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
|
@@ -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.
|
|
@@ -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 }),
|
|
@@ -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
|
}
|
|
@@ -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.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Paging loops carry a seen-cursor guard: a server that repeats a cursor would
|
|
2
2
|
// otherwise spin forever and leave a create/submit permanently pending.
|
|
3
|
+
import { landsOnLive } from "../_shared/owner-editor-value.js";
|
|
3
4
|
import { defineService } from "@himalaya/state";
|
|
4
5
|
import { createCategory, getCategory, queryCategories, updateCategory } from "@himalaya/wix-faq";
|
|
5
6
|
interface State { id:string;revision:string;mode:"create"|"edit";heading:string;title:string;error:string;conflict:boolean;noConflict:boolean;savedMessage:string;savedId:string; }
|
|
6
7
|
const input=((globalThis as {himalaya?:{input?:{id?:string}}}).himalaya?.input)??{}; const msg=(e:unknown)=>String((e as {message?:unknown})?.message??e); const conflict=(e:unknown)=>/\b409\b|conflict|revision/i.test(msg(e)); const offline=(e:unknown)=>/offline|network|connect|bridge|fetch/i.test(msg(e));
|
|
7
8
|
async function appendOrder():Promise<number>{let max=-1,cursor="";const seen=new Set<string>();do{const r=await queryCategories({query:{cursorPaging:{limit:100,...(cursor?{cursor}:{})}}});for(const c of r.categories??[])max=Math.max(max,c.sortOrder??-1);const next=r.pagingMetadata?.cursors?.next??"";if(!next||seen.has(next))break;seen.add(next);cursor=next;}while(cursor);return max+1;}
|
|
8
9
|
const shape=(s:Partial<State>):State=>({id:s.id??input.id??"",revision:s.revision??"",mode:s.mode??((s.id??input.id)?"edit":"create"),heading:s.heading??((s.id??input.id)?"Edit category":"New category"),title:s.title??"",error:s.error??"",conflict:s.conflict??false,noConflict:!(s.conflict??false),savedMessage:s.savedMessage??"",savedId:s.savedId??""});
|
|
9
|
-
const svc=defineService<State,{load:(s:State,a?:unknown)=>Promise<State>;submit:(s:State,v?:Record<string,unknown>)=>Promise<State>}>("himiOwnerLiteFaqCategoryForm",{state:shape({}),actions:{load:async(s,a)=>{
|
|
10
|
+
const svc=defineService<State,{load:(s:State,a?:unknown)=>Promise<State | undefined>;submit:(s:State,v?:Record<string,unknown>)=>Promise<State>}>("himiOwnerLiteFaqCategoryForm",{state:shape({}),actions:{load: async (s,a): Promise<State | undefined> => landsOnLive(svc, s, await (async () => {
|
|
10
11
|
// The held id is for the host's bare refresh only (no argument): an onAppear without an id is a create screen.
|
|
11
12
|
const id=input.id||String((a as {id?:unknown})?.id??(a===undefined?s.id:""));
|
|
12
13
|
// A form that held a record (the one just saved) opens blank.
|
|
13
|
-
if(!id)return shape({...(s.id?{}:s),mode:"create",id:""});try{const c=(await getCategory(id)).category;if(!c)return shape({...s,id,error:"Category not found"});return shape({...s,id,revision:c.revision??"",mode:"edit",heading:"Edit category",title:c.title??"",error:"",conflict:false});}catch(e){return shape({...s,id,error:offline(e)?"Connect to the internet to load this category.":msg(e)});}},submit:async(s,v)=>{const title=String(v?.title??s.title).trim();if(!title)return shape({...s,error:"Category name is required.",savedMessage:""});try{if(s.mode==="edit"){if(!s.revision)return shape({...s,error:"Refresh before saving; this category has no current revision.",conflict:true});const r=await updateCategory(s.id,{category:{id:s.id,revision:s.revision,title},fieldMask:["title"]});return shape({...s,revision:r.category?.revision??s.revision,title,error:"",conflict:false,savedMessage:"Category saved.",savedId:s.id});}const r=await createCategory({category:{title,sortOrder:await appendOrder()}});return shape({...s,id:r.category?.id??"",revision:r.category?.revision??"",mode:"edit",heading:"Edit category",title,error:"",savedMessage:"Category created.",savedId:r.category?.id??""});}catch(e){if(conflict(e))return shape({...s,error:"This category changed elsewhere. Refresh before saving again.",conflict:true,savedMessage:""});return shape({...s,error:offline(e)?"Connect to the internet and try again. This change was not queued.":msg(e),savedMessage:""});}}}});export{svc,appendOrder};
|
|
14
|
+
if(!id)return shape({...(s.id?{}:s),mode:"create",id:""});try{const c=(await getCategory(id)).category;if(!c)return shape({...s,id,error:"Category not found"});return shape({...s,id,revision:c.revision??"",mode:"edit",heading:"Edit category",title:c.title??"",error:"",conflict:false});}catch(e){return shape({...s,id,error:offline(e)?"Connect to the internet to load this category.":msg(e)});}})()),submit:async(s,v)=>{const title=String(v?.title??s.title).trim();if(!title)return shape({...s,error:"Category name is required.",savedMessage:""});try{if(s.mode==="edit"){if(!s.revision)return shape({...s,error:"Refresh before saving; this category has no current revision.",conflict:true});const r=await updateCategory(s.id,{category:{id:s.id,revision:s.revision,title},fieldMask:["title"]});return shape({...s,revision:r.category?.revision??s.revision,title,error:"",conflict:false,savedMessage:"Category saved.",savedId:s.id});}const r=await createCategory({category:{title,sortOrder:await appendOrder()}});return shape({...s,id:r.category?.id??"",revision:r.category?.revision??"",mode:"edit",heading:"Edit category",title,error:"",savedMessage:"Category created.",savedId:r.category?.id??""});}catch(e){if(conflict(e))return shape({...s,error:"This category changed elsewhere. Refresh before saving again.",conflict:true,savedMessage:""});return shape({...s,error:offline(e)?"Connect to the internet and try again. This change was not queued.":msg(e),savedMessage:""});}}}});export{svc,appendOrder};
|
|
@@ -4,7 +4,7 @@ import { defineService } from "@himalaya/state";
|
|
|
4
4
|
import { createQuestionEntry, getQuestionEntry, queryCategories, queryQuestionEntries, updateQuestionEntry, type WixCategory, type WixQuestionEntry } from "@himalaya/wix-faq";
|
|
5
5
|
import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
|
|
6
6
|
import { draftjsText } from "../_shared/owner-rich-guard.js";
|
|
7
|
-
import { editorSubmitValue, isEmptyDocument, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm } from "../_shared/owner-editor-value.js";
|
|
7
|
+
import { editorSubmitValue, isEmptyDocument, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive, holdsLoadedForm, landsOnLive } from "../_shared/owner-editor-value.js";
|
|
8
8
|
import { parseRicosJson, type RicosDocument } from "@himalaya/wix-rich-content";
|
|
9
9
|
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
10
10
|
|
|
@@ -45,18 +45,18 @@ const shape=(s:Partial<State>):State=>({ id:s.id??input.id??"", revision:s.revis
|
|
|
45
45
|
answerProtected: mode === "locked", answerEditable: mode !== "locked", answerNote: mode === "locked" ? RICH_ANSWER_NOTE : "" }; })() });
|
|
46
46
|
// The RichContentEditor's image tool: this service mints the signed Wix Media URL with the worker's own wixApi.
|
|
47
47
|
ricosUploadService("himiOwnerLiteFaqQuestionFormUploads");
|
|
48
|
-
const svc=defineService<State,{load:(s:State,a?:unknown)=>Promise<State>;answerChanged:(s:State,a?:unknown)=>State;submit:(s:State,v?:Record<string,unknown>)=>Promise<State>}>("himiOwnerLiteFaqQuestionForm",{state:shape({answerPending:!!input.id}),
|
|
48
|
+
const svc=defineService<State,{load:(s:State,a?:unknown)=>Promise<State | undefined>;answerChanged:(s:State,a?:unknown)=>State;submit:(s:State,v?:Record<string,unknown>)=>Promise<State>}>("himiOwnerLiteFaqQuestionForm",{state:shape({answerPending:!!input.id}),
|
|
49
49
|
// [rico] #3644 — this run's editor state: a cold boot takes it from `state`, never the snapshot.
|
|
50
50
|
notHydrated:editorRunKeys("answer"),
|
|
51
51
|
// perVisit: a cached embed's warm return starts this run fresh, as a cold boot does (`__resetRunState`).
|
|
52
52
|
perVisit:editorRunKeys("answer"),actions:{
|
|
53
|
-
load:async(s,a)=>{
|
|
53
|
+
load: async (s,a): Promise<State | undefined> => landsOnLive(svc, s, await (async () => {
|
|
54
54
|
// The held id is for the host's bare refresh only (no argument): an onAppear without an id is a create screen.
|
|
55
55
|
const id=input.id||String((a as {id?:unknown})?.id??(a===undefined?s.id:""));if(holdsLoadedForm(s,{id,mode:id?"edit":"create"},"answer"))return s;try{const cats=await categories();const options=cats.map(c=>({id:c.id??"",label:c.title?.trim()||"Untitled category"}));
|
|
56
56
|
// A form that held a record (the one just saved) opens blank.
|
|
57
57
|
if(!id)return shape({...(s.id?{}:s),...ANSWER_LOADED,mode:"create",id:"",answerMode:"plain",answerJson:"",categoryOptions:options,statusValue:"HIDDEN"});const q=(await getQuestionEntry(id)).questionEntry;if(!q) return shape({...s,id,error:"Question not found",categoryOptions:options});return shape({...s,id,revision:q.revision??"",mode:"edit",heading:"Edit question",question:q.question??"",...((a)=>({...a,...ANSWER_LOADED,answerMode:a.answerFormat==="draftjs"?"locked" as const:"editor" as const,
|
|
58
58
|
// The seed is the stored document byte for byte (never re-encoded); a plain answer seeds as paragraphs.
|
|
59
|
-
answerJson:a.answerFormat==="draftjs"?"":q.richContent?JSON.stringify(q.richContent):q.plainText?JSON.stringify(plainPostBody(q.plainText)):""}))(answerOf(q)),categoryId:q.categoryId??"",statusValue:q.status==="VISIBLE"?"VISIBLE":"HIDDEN",categoryOptions:options,error:"",conflict:false});}catch(e){return shape({...s,id,error:offline(e)?"Connect to the internet to load this question.":msg(e)});}},
|
|
59
|
+
answerJson:a.answerFormat==="draftjs"?"":q.richContent?JSON.stringify(q.richContent):q.plainText?JSON.stringify(plainPostBody(q.plainText)):""}))(answerOf(q)),categoryId:q.categoryId??"",statusValue:q.status==="VISIBLE"?"VISIBLE":"HIDDEN",categoryOptions:options,error:"",conflict:false});}catch(e){return shape({...s,id,error:offline(e)?"Connect to the internet to load this question.":msg(e)});}})()),
|
|
60
60
|
// The editor's on_change carries `args.dirty` only; the document stays in the form bag.
|
|
61
61
|
// Any on_change after a save is an edit that save did not write (every arm fires the pending on_change
|
|
62
62
|
// before releasing a submit), so the confirmation goes.
|
|
@@ -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
|
}
|
|
@@ -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.
|