@wix/himalaya-cli 0.1177.0 → 0.1179.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/sdk/state/src/index.d.ts +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/rich-content/src/index.d.ts +8 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-rich-content.mjs +24 -0
- package/dist/cli.mjs +16 -16
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +7 -1
- package/package.json +2 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/community-club-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/events-meetups-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/owner-lite/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq-question-form/index.ts +16 -10
- package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/restaurant-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/retail-store-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +20 -8
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-editor-value.ts +57 -39
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +21 -10
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +23 -13
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// drops nodes the owner never touched).
|
|
9
9
|
// Inside the platform source this import is a relative path, so node test runs resolve it; an app's
|
|
10
10
|
// copy imports "@himalaya/wix-rich-content".
|
|
11
|
-
import { parseRicosJson, isPlainRicos,
|
|
11
|
+
import { parseRicosJson, isPlainRicos, richContentOverLimits, type RicosDocument } from "@himalaya/wix-rich-content";
|
|
12
12
|
|
|
13
13
|
export type EditorValue =
|
|
14
14
|
| { kind: "absent" } // leave the stored body untouched
|
|
@@ -17,34 +17,6 @@ export type EditorValue =
|
|
|
17
17
|
| { kind: "too_large" } // past the rich-content limits: save the rest, say why
|
|
18
18
|
| { kind: "invalid" }; // not a Ricos document: refuse, write nothing
|
|
19
19
|
|
|
20
|
-
function utf8Bytes(s: string): number {
|
|
21
|
-
let n = 0;
|
|
22
|
-
for (let i = 0; i < s.length; i++) {
|
|
23
|
-
const c = s.charCodeAt(i);
|
|
24
|
-
if (c < 0x80) n += 1;
|
|
25
|
-
else if (c < 0x800) n += 2;
|
|
26
|
-
else if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) { n += 4; i++; }
|
|
27
|
-
else n += 3;
|
|
28
|
-
}
|
|
29
|
-
return n;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Every viewer renders a body past the rich-content limits as its fallback, so it is never written.
|
|
33
|
-
* The viewer's order: bytes (before parsing), then depth, then the node count. */
|
|
34
|
-
function overLimits(doc: RicosDocument): boolean {
|
|
35
|
-
let count = 0;
|
|
36
|
-
const work: Array<[unknown, number]> = doc.nodes.map((n) => [n, 1] as [unknown, number]);
|
|
37
|
-
while (work.length) {
|
|
38
|
-
const [n, depth] = work.pop()!;
|
|
39
|
-
if (!n || typeof n !== "object" || Array.isArray(n)) continue;
|
|
40
|
-
if (depth > RICH_CONTENT_LIMITS.maxDepth) return true;
|
|
41
|
-
if (++count > RICH_CONTENT_LIMITS.maxNodes) return true;
|
|
42
|
-
const kids = (n as { nodes?: unknown }).nodes;
|
|
43
|
-
if (Array.isArray(kids)) for (const k of kids) work.push([k, depth + 1]);
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
20
|
const TEXT_ONLY = new Set(["PARAGRAPH", "TEXT", "HEADING", "BULLETED_LIST", "ORDERED_LIST", "LIST_ITEM", "BLOCKQUOTE", "CODE_BLOCK"]);
|
|
49
21
|
/** No visible text and no non-text node (an image, a divider, an embed…): an empty body. */
|
|
50
22
|
export function isEmptyDocument(doc: RicosDocument): boolean {
|
|
@@ -55,10 +27,12 @@ export function isEmptyDocument(doc: RicosDocument): boolean {
|
|
|
55
27
|
|
|
56
28
|
/** A Ricos JSON string checked as every viewer would: the limits first, then the shape. */
|
|
57
29
|
function checkedDocument(raw: string): EditorValue {
|
|
58
|
-
|
|
30
|
+
// Every viewer renders a body past the limits as its fallback, so it is never written. One parse
|
|
31
|
+
// feeds both checks; a string past the byte limit is refused before it is parsed at all.
|
|
32
|
+
if (richContentOverLimits(raw, null) === "too_large") return { kind: "too_large" };
|
|
59
33
|
const doc = parseRicosJson(raw);
|
|
60
34
|
if (!doc) return { kind: "invalid" };
|
|
61
|
-
return
|
|
35
|
+
return richContentOverLimits(raw, doc) ? { kind: "too_large" } : { kind: "doc", doc, json: raw };
|
|
62
36
|
}
|
|
63
37
|
|
|
64
38
|
/** `raw` is `values[name]`; `seededJson` is the string the form published as the editor's seed. */
|
|
@@ -108,15 +82,59 @@ export function cachedRichVerdict(doc: RicosDocument | null, knownPlain: boolean
|
|
|
108
82
|
return knownPlain ? "false" : "true";
|
|
109
83
|
}
|
|
110
84
|
|
|
111
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
|
|
85
|
+
/** The keys of one editor field that describe THIS run, never the last one: the seed (`<key>Json`),
|
|
86
|
+
* the load gate (`<key>Pending`), the edit flag (`<key>Dirty`) and its counter (`<key>Edits`). A form
|
|
87
|
+
* lists them in `defineService(name, { notHydrated })`, so a cold boot takes their DECLARED values: the
|
|
88
|
+
* editor seeds once, and a persisted seed would be painted as its content and saved back over a newer
|
|
89
|
+
* record. */
|
|
90
|
+
export function editorRunKeys<K extends string>(key: K): [`${K}Json`, `${K}Pending`, `${K}Dirty`, `${K}Edits`] {
|
|
91
|
+
return [`${key}Json`, `${key}Pending`, `${key}Dirty`, `${key}Edits`];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The same keys on a runtime that ignores `notHydrated` (a binary from before #3644, still served new
|
|
95
|
+
* bundles over the air): reset them in place, before any action runs. A no-op where the runtime
|
|
96
|
+
* honours the option (`himalaya.hydrationOptOut`). */
|
|
97
|
+
export function resetHydratedEditorOnOlderRuntime(svc: unknown, key: string, pendingAtBoot: boolean, extra: Record<string, unknown> = {}): void {
|
|
98
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut === true) return;
|
|
116
99
|
const booted = (svc as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
117
100
|
if (!booted || typeof booted !== "object") return;
|
|
118
|
-
if (booted[
|
|
119
|
-
booted[
|
|
120
|
-
booted[
|
|
101
|
+
if (booted[`${key}Json`]) booted[`${key}Json`] = "";
|
|
102
|
+
booted[`${key}Pending`] = pendingAtBoot;
|
|
103
|
+
booted[`${key}Dirty`] = false;
|
|
104
|
+
booted[`${key}Edits`] = 0;
|
|
121
105
|
Object.assign(booted, extra);
|
|
122
106
|
}
|
|
107
|
+
|
|
108
|
+
/** What an awaited save leaves the field as, read from the LIVE state when it lands. An on_change that
|
|
109
|
+
* arrived while the save was in flight (`<key>Edits` moved) is an edit the save did not write: the
|
|
110
|
+
* field stays dirty and the save's confirmation is stale. Otherwise a save that wrote the field
|
|
111
|
+
* re-bases it as saved. `live` is the service's state now (none, or one that never moved: the start state). */
|
|
112
|
+
export function editorAfterSave(key: string, start: Record<string, unknown>, live: Record<string, unknown> | undefined, wrote: boolean):
|
|
113
|
+
{ dirty: boolean; edits: number; editedDuringSave: boolean } {
|
|
114
|
+
const atStart = Number(start[`${key}Edits`] ?? 0);
|
|
115
|
+
const now = Number(live?.[`${key}Edits`] ?? atStart);
|
|
116
|
+
// Only a counter that MOVED ON counts; one behind the start is a host that does not apply results.
|
|
117
|
+
const editedDuringSave = now > atStart;
|
|
118
|
+
return editedDuringSave
|
|
119
|
+
? { dirty: true, edits: now, editedDuringSave }
|
|
120
|
+
: { dirty: wrote ? false : Boolean(start[`${key}Dirty`]), edits: atStart, editedDuringSave };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The live state of a service handle, when the runtime gives one. */
|
|
124
|
+
export function liveState(svc: unknown): Record<string, unknown> | undefined {
|
|
125
|
+
const st = (svc as { getState?: () => unknown } | undefined)?.getState?.();
|
|
126
|
+
return st && typeof st === "object" ? st as Record<string, unknown> : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** An awaited action's result, laid over the LIVE state: only the keys the action CHANGED (against the
|
|
130
|
+
* state it started from) are written, so anything the owner did while it awaited — an edit, a field in
|
|
131
|
+
* the details sheet, closing the sheet — survives a save that lands after it, success or failure. A
|
|
132
|
+
* key both changed goes to the action. Where the host gives no live state, the result as it is. */
|
|
133
|
+
export function settleOverLive<S extends object>(svc: unknown, start: S, result: S): S {
|
|
134
|
+
const now = liveState(svc) as S | undefined;
|
|
135
|
+
if (!now || now === start || now === result) return result;
|
|
136
|
+
const out = { ...now } as Record<string, unknown>;
|
|
137
|
+
const r = result as Record<string, unknown>, s0 = start as Record<string, unknown>;
|
|
138
|
+
for (const k of Object.keys(r)) if (!Object.is(r[k], s0[k])) out[k] = r[k];
|
|
139
|
+
return out as S;
|
|
140
|
+
}
|
|
@@ -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,
|
|
13
|
+
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive } 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, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
@@ -59,6 +59,8 @@ interface EventFormState {
|
|
|
59
59
|
* hidden until this run's load lands. */
|
|
60
60
|
descriptionDirty: boolean;
|
|
61
61
|
descriptionPending: boolean;
|
|
62
|
+
/** Counts the editor's on_change events, so a save can tell an edit made while it was in flight. */
|
|
63
|
+
descriptionEdits: number;
|
|
62
64
|
/** Derived from the mode: "locked" shows the preview + `descriptionNote`, and refuses an edit. */
|
|
63
65
|
descriptionProtected: boolean;
|
|
64
66
|
descriptionEditable: boolean;
|
|
@@ -133,6 +135,7 @@ function shape(s: Partial<EventFormState>): EventFormState {
|
|
|
133
135
|
descriptionEditorHidden: mode !== "editor",
|
|
134
136
|
descriptionDirty: s.descriptionDirty ?? false,
|
|
135
137
|
descriptionPending: s.descriptionPending ?? false,
|
|
138
|
+
descriptionEdits: s.descriptionEdits ?? 0,
|
|
136
139
|
descriptionProtected: mode === "locked",
|
|
137
140
|
descriptionEditable: mode !== "locked",
|
|
138
141
|
descriptionNote: mode === "locked" ? copyParam("ownerEventForm.richDescriptionNote", "This description has formatting or media. Edit it in your site dashboard to keep that content intact; the title can be changed here.") : "",
|
|
@@ -163,8 +166,8 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
163
166
|
|
|
164
167
|
// The RichContentEditor's image tool (the editor's upload contract). The component picks and PUTs
|
|
165
168
|
// the bytes; this service mints the signed Wix Media URL with the worker's own wixApi, so no token
|
|
166
|
-
// reaches the page.
|
|
167
|
-
//
|
|
169
|
+
// reaches the page. Any order: the
|
|
170
|
+
// worker test harnesses find each service by name.
|
|
168
171
|
ricosUploadService("himiOwnerLiteEventFormUploads");
|
|
169
172
|
|
|
170
173
|
const svc = defineService<EventFormState, {
|
|
@@ -173,6 +176,8 @@ const svc = defineService<EventFormState, {
|
|
|
173
176
|
submit: (s: EventFormState, values?: Record<string, unknown>) => Promise<EventFormState>;
|
|
174
177
|
}>("himiOwnerLiteEventForm", {
|
|
175
178
|
state: shape({ mode: startMode, id: initialInput.id ?? "", descriptionPending: true }),
|
|
179
|
+
// [rico] #3644 — this run's editor state: a cold boot takes it from `state`, never the snapshot.
|
|
180
|
+
notHydrated: editorRunKeys("description"),
|
|
176
181
|
actions: {
|
|
177
182
|
load: async (s, arg) => {
|
|
178
183
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
@@ -258,8 +263,9 @@ const svc = defineService<EventFormState, {
|
|
|
258
263
|
}
|
|
259
264
|
},
|
|
260
265
|
// The editor's on_change carries `args.dirty` only; the document stays in the form bag.
|
|
261
|
-
descriptionChanged: (s) => shape({ ...s, descriptionDirty: true, ...editorChanged(s) }),
|
|
262
|
-
|
|
266
|
+
descriptionChanged: (s) => shape({ ...s, descriptionDirty: true, descriptionEdits: s.descriptionEdits + 1, ...editorChanged(s) }),
|
|
267
|
+
// Laid over the live state ([rico] #3644): what the owner did while the save awaited survives it.
|
|
268
|
+
submit: async (s, values): Promise<EventFormState> => settleOverLive(svc, s, await (async (): Promise<EventFormState> => {
|
|
263
269
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
264
270
|
const v = values ?? {};
|
|
265
271
|
const title = String(v.title ?? s.title).trim();
|
|
@@ -310,14 +316,19 @@ const svc = defineService<EventFormState, {
|
|
|
310
316
|
// lands nothing is known, so the row stays protected (fail closed).
|
|
311
317
|
const knownPlain = s.descriptionMode === "plain" || (s.descriptionMode === "editor" && !s.descriptionPending && s.descriptionJson === "");
|
|
312
318
|
await cachePut({ id: s.id, name: title, title, description, descriptionProtected: cachedRichVerdict(heldDoc as never, knownPlain), ...(heldJson ? { descriptionJson: heldJson } : {}), when: s.when, location: s.location });
|
|
313
|
-
const savedState = (savedMessage: string) =>
|
|
319
|
+
const savedState = (savedMessage: string) => {
|
|
320
|
+
// An edit typed while this save was in flight stays unsaved work ([rico] #3644).
|
|
321
|
+
const after = editorAfterSave("description", s as never, liveState(svc), !!doc);
|
|
322
|
+
return shape({
|
|
314
323
|
...s, mode: "edit", id: s.id, heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
315
324
|
title, description, originalDescription: doc ? description : s.originalDescription,
|
|
316
325
|
...(docRaw ? { descriptionJson: docRaw } : {}),
|
|
317
|
-
descriptionDirty:
|
|
326
|
+
descriptionDirty: after.dirty, descriptionEdits: after.edits,
|
|
318
327
|
status: "saved", savedMessage, savedId: s.id,
|
|
328
|
+
...(after.editedDuringSave ? { status: "idle" as const, savedMessage: "" } : {}),
|
|
319
329
|
error: tooLarge ? copyParam("ownerEventForm.descriptionTooLarge", "The description is too long to save, so it was left as it was. Your other changes were saved.") : "",
|
|
320
|
-
|
|
330
|
+
});
|
|
331
|
+
};
|
|
321
332
|
try {
|
|
322
333
|
await updateEvent(s.id, event);
|
|
323
334
|
return savedState(copyParam("ownerEventForm.savedMessage", "Event saved."));
|
|
@@ -337,11 +348,11 @@ const svc = defineService<EventFormState, {
|
|
|
337
348
|
}
|
|
338
349
|
return shape({ ...s, status: "error", error: ownerLoadError(e), savedMessage: "" });
|
|
339
350
|
}
|
|
340
|
-
},
|
|
351
|
+
})()),
|
|
341
352
|
},
|
|
342
353
|
});
|
|
343
354
|
|
|
344
355
|
// Adopter contract: the editor seeds once, so a cold boot must not seed it with the last run's document.
|
|
345
|
-
|
|
356
|
+
resetHydratedEditorOnOlderRuntime(svc, "description", true);
|
|
346
357
|
|
|
347
358
|
export { svc };
|
|
@@ -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,
|
|
7
|
+
import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetHydratedEditorOnOlderRuntime, editorAfterSave, liveState, settleOverLive } 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";
|
|
@@ -46,6 +46,8 @@ interface PostFormState {
|
|
|
46
46
|
* the body was loaded or saved, and (edit mode) the editor is hidden until this run's load lands. */
|
|
47
47
|
bodyDirty: boolean;
|
|
48
48
|
bodyPending: boolean;
|
|
49
|
+
/** Counts the editor's on_change events, so a save can tell an edit made while it was in flight. */
|
|
50
|
+
bodyEdits: number;
|
|
49
51
|
coverUrl: string;
|
|
50
52
|
originalCoverUrl: string;
|
|
51
53
|
coverPreview: string;
|
|
@@ -120,6 +122,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
|
|
|
120
122
|
detailsOpen: s.detailsOpen ?? false,
|
|
121
123
|
bodyDirty: s.bodyDirty ?? false,
|
|
122
124
|
bodyPending: s.bodyPending ?? false,
|
|
125
|
+
bodyEdits: s.bodyEdits ?? 0,
|
|
123
126
|
coverUrl: s.coverUrl ?? "",
|
|
124
127
|
originalCoverUrl: s.originalCoverUrl ?? "",
|
|
125
128
|
coverPreview: wixMediaUrl(s.coverUrl ?? "", WIX_DETAIL_IMAGE),
|
|
@@ -150,9 +153,8 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
150
153
|
|
|
151
154
|
// [rico] #3371 — the RichContentEditor's image tool (the editor's upload contract). The component picks
|
|
152
155
|
// and PUTs the bytes; this service mints the signed Wix Media URL with the worker's own wixApi
|
|
153
|
-
// (@himalaya/wix-site-media rides the `wix_api` grant), so no token reaches the page.
|
|
154
|
-
//
|
|
155
|
-
// LAST defineService as its service.
|
|
156
|
+
// (@himalaya/wix-site-media rides the `wix_api` grant), so no token reaches the page. Any order:
|
|
157
|
+
// the worker test harnesses find each service by name.
|
|
156
158
|
ricosUploadService("himiOwnerLitePostFormUploads");
|
|
157
159
|
|
|
158
160
|
const svc = defineService<PostFormState, {
|
|
@@ -165,6 +167,9 @@ const svc = defineService<PostFormState, {
|
|
|
165
167
|
submit: (s: PostFormState, values?: Record<string, unknown>) => Promise<PostFormState>;
|
|
166
168
|
}>("himiOwnerLitePostForm", {
|
|
167
169
|
state: shape({ mode: startMode, id: initialInput.id ?? "", bodyPending: startMode === "edit" }),
|
|
170
|
+
// [rico] #3644 — this run's editor state and UI: a cold boot takes these from `state`, never from
|
|
171
|
+
// the persisted snapshot (the editor seeds once; last run's sheet is not this run's).
|
|
172
|
+
notHydrated: [...editorRunKeys("body"), "detailsOpen"],
|
|
168
173
|
actions: {
|
|
169
174
|
load: async (s, arg) => {
|
|
170
175
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
@@ -236,8 +241,9 @@ const svc = defineService<PostFormState, {
|
|
|
236
241
|
detailsOpen: (s) => shape({ ...s, detailsOpen: true }),
|
|
237
242
|
detailsClose: (s) => shape({ ...s, detailsOpen: false }),
|
|
238
243
|
// The editor's `on_change` carries `args.dirty` only; the document stays in the form bag.
|
|
239
|
-
bodyChanged: (s) => shape({ ...s, bodyDirty: true, ...editorChanged(s) }),
|
|
240
|
-
|
|
244
|
+
bodyChanged: (s) => shape({ ...s, bodyDirty: true, bodyEdits: s.bodyEdits + 1, ...editorChanged(s) }),
|
|
245
|
+
// Laid over the live state ([rico] #3644): what the owner did while the save awaited survives it.
|
|
246
|
+
submit: async (s, values): Promise<PostFormState> => settleOverLive(svc, s, await (async (): Promise<PostFormState> => {
|
|
241
247
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
242
248
|
const v = values ?? {};
|
|
243
249
|
const title = String(v.title ?? s.title).trim();
|
|
@@ -292,14 +298,19 @@ const svc = defineService<PostFormState, {
|
|
|
292
298
|
let kept: WixRichContent | null = bodyEdited ? richContent : (replay ? null : s.richContent);
|
|
293
299
|
const localRow = (id: string): DbEntity => ({ id, name: title, title, excerpt, body: postBodyText(kept), coverUrl, ...(kept ? { richContent: JSON.stringify(kept) } : {}), isFeatured: isFeatured ? "true" : "false", postStatus: "Draft", postStatusRaw: "DRAFT" });
|
|
294
300
|
const localId = editing ? targetId : `local-${Date.now()}`;
|
|
295
|
-
const savedState = (id: string, savedMessage: string) =>
|
|
301
|
+
const savedState = (id: string, savedMessage: string) => {
|
|
302
|
+
// An edit typed while this save was in flight stays unsaved work ([rico] #3644).
|
|
303
|
+
const after = editorAfterSave("body", s as never, liveState(svc), bodyEdited);
|
|
304
|
+
return shape({
|
|
296
305
|
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
297
306
|
title, excerpt, body: postBodyText(kept), richContent: kept, bodyJson: bodyEdited ? bodyRaw : s.bodyJson,
|
|
298
|
-
bodyDirty:
|
|
307
|
+
bodyDirty: after.dirty, bodyEdits: after.edits,
|
|
299
308
|
coverUrl, originalCoverUrl: coverUrl, isFeatured: isFeatured ? "true" : "false",
|
|
300
309
|
status: "saved", savedMessage, savedId: id,
|
|
310
|
+
...(after.editedDuringSave ? { status: "idle" as const, savedMessage: "" } : {}),
|
|
301
311
|
error: bodyTooLarge ? copyParam("ownerPostForm.bodyTooLarge", "The post body is too long to save, so it was left as it was. Your other changes were saved.") : "",
|
|
302
|
-
|
|
312
|
+
});
|
|
313
|
+
};
|
|
303
314
|
try {
|
|
304
315
|
// A replay worker starts without the loaded document. Entity upserts replace whole rows, so
|
|
305
316
|
// retain the cached rich body before writing back.
|
|
@@ -349,12 +360,11 @@ const svc = defineService<PostFormState, {
|
|
|
349
360
|
}
|
|
350
361
|
return shape({ ...s, status: "error", error: ownerLoadError(e), savedMessage: "" });
|
|
351
362
|
}
|
|
352
|
-
},
|
|
363
|
+
})()),
|
|
353
364
|
},
|
|
354
365
|
});
|
|
355
366
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
resetHydratedEditor(svc, { json: "bodyJson", pending: "bodyPending", dirty: "bodyDirty" }, startMode === "edit", { detailsOpen: false });
|
|
367
|
+
// The same reset on a runtime from before `notHydrated` existed (an older binary on a new bundle).
|
|
368
|
+
resetHydratedEditorOnOlderRuntime(svc, "body", startMode === "edit", { detailsOpen: false });
|
|
359
369
|
|
|
360
370
|
export { svc };
|