@wix/himalaya-cli 0.1159.0 → 0.1161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +370 -181
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +2 -8
- package/guides/component-reference.md +1 -3
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +382 -194
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/community-club-owner/dev/index.ts +382 -194
- package/templates/community-club-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/events-meetups-owner/dev/index.ts +382 -194
- package/templates/events-meetups-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/gym-fitness-owner/dev/index.ts +382 -194
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/nonprofit-cause-owner/dev/index.ts +382 -194
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/owner-lite/dev/index.ts +382 -194
- package/templates/owner-lite/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/podcast-creator-owner/dev/index.ts +382 -194
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/portfolio-creative-owner/dev/index.ts +382 -194
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/restaurant-owner/dev/index.ts +382 -194
- package/templates/restaurant-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/retail-store-owner/dev/index.ts +382 -194
- package/templates/retail-store-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
- package/templates/yoga-studio-owner/dev/index.ts +382 -194
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// [rico] #3371 — how an owner form reads a RichContentEditor out of `submit(s, values)`.
|
|
2
|
+
//
|
|
3
|
+
// The editor writes its Ricos document into the Form's value bag as a JSON STRING under its `name`,
|
|
4
|
+
// on every change and again on blur. The value is ABSENT when the editor never became ready (the
|
|
5
|
+
// engine page is not bundled, failed, or timed out): the stored body must then stay exactly as it is
|
|
6
|
+
// (user decision, 2026-09-26 — save everything but the body). A document is written exactly as the
|
|
7
|
+
// editor produced it: never re-encoded (encodeRichContent is the VIEWER's; it rewrites media ids and
|
|
8
|
+
// drops nodes the owner never touched).
|
|
9
|
+
// Inside the platform source this import is a relative path, so node test runs resolve it; an app's
|
|
10
|
+
// copy imports "@himalaya/wix-rich-content".
|
|
11
|
+
import { parseRicosJson, RICH_CONTENT_LIMITS, type RicosDocument } from "@himalaya/wix-rich-content";
|
|
12
|
+
|
|
13
|
+
export type EditorValue =
|
|
14
|
+
| { kind: "absent" } // leave the stored body untouched
|
|
15
|
+
| { kind: "unchanged" } // byte-identical to the seed: nothing to write
|
|
16
|
+
| { kind: "doc"; doc: RicosDocument; json: string } // write `doc`; `json` is the editor's own string
|
|
17
|
+
| { kind: "too_large" } // past the rich-content limits: save the rest, say why
|
|
18
|
+
| { kind: "invalid" }; // not a Ricos document: refuse, write nothing
|
|
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
|
+
/** `raw` is `values[name]`; `seededJson` is the string the form published as the editor's seed. */
|
|
49
|
+
export function editorValue(raw: unknown, seededJson: string): EditorValue {
|
|
50
|
+
if (typeof raw !== "string" || raw === "") return { kind: "absent" };
|
|
51
|
+
if (seededJson !== "" && raw === seededJson) return { kind: "unchanged" };
|
|
52
|
+
if (utf8Bytes(raw) > RICH_CONTENT_LIMITS.maxBytes) return { kind: "too_large" };
|
|
53
|
+
const doc = parseRicosJson(raw);
|
|
54
|
+
if (!doc) return { kind: "invalid" };
|
|
55
|
+
return overLimits(doc) ? { kind: "too_large" } : { kind: "doc", doc, json: raw };
|
|
56
|
+
}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// the post detail ("Edit"). On save it lands a draft; publish is on the detail
|
|
4
4
|
// screen (himiOwnerLitePost.publish). Bound by buildForm ($service:himiOwnerLitePostForm.*).
|
|
5
5
|
|
|
6
|
-
import { postBodyText,
|
|
6
|
+
import { postBodyText, plainPostBody, postCoverUrl } from "../_shared/owner-post-content.js";
|
|
7
|
+
import { editorValue } from "../_shared/owner-editor-value.js";
|
|
8
|
+
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
9
|
+
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
7
10
|
import { wixCall } from "@himalaya/wix-site-media";
|
|
8
11
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
|
|
9
12
|
import { defineService } from "@himalaya/state";
|
|
@@ -34,9 +37,21 @@ interface PostFormState {
|
|
|
34
37
|
excerpt: string;
|
|
35
38
|
body: string;
|
|
36
39
|
richContent: WixRichContent | null;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
/** [rico] #3371 — the post's raw `richContent` as a JSON string: the RichContentEditor's seed
|
|
41
|
+
* (`documentPath`). "" for a new post, and until a load lands (see the boot reset below).
|
|
42
|
+
* Re-based to the saved document after each save. */
|
|
43
|
+
bodyJson: string;
|
|
44
|
+
/** The "Post details" sheet (excerpt + cover) is open. */
|
|
45
|
+
detailsOpen: boolean;
|
|
46
|
+
/** The editor's on_change fired since the body was last loaded or saved. The bag ALWAYS holds a
|
|
47
|
+
* document once the editor is ready — the engine's own serialization of the seed, which is not
|
|
48
|
+
* byte-equal to it — so only this says the owner actually edited the body. */
|
|
49
|
+
bodyDirty: boolean;
|
|
50
|
+
/** Edit mode, until `load` lands: the descriptor hides the editor, because a provisional editor
|
|
51
|
+
* the owner typed into keeps that text when the real document arrives (the seed latch). */
|
|
52
|
+
bodyPending: boolean;
|
|
53
|
+
/** When a save last wrote the body (ms), so its own trailing on_change does not undo "Post updated.". */
|
|
54
|
+
bodySavedAt: number;
|
|
40
55
|
coverUrl: string;
|
|
41
56
|
originalCoverUrl: string;
|
|
42
57
|
coverPreview: string;
|
|
@@ -88,6 +103,13 @@ function boolString(value: unknown, fallback: string): string {
|
|
|
88
103
|
if (typeof value === "string") return value === "false" ? "false" : value === "true" ? "true" : fallback;
|
|
89
104
|
return fallback;
|
|
90
105
|
}
|
|
106
|
+
/** What every load that publishes a document resets: the editor may show it, nothing is edited yet. */
|
|
107
|
+
const BODY_LOADED = { bodyPending: false, bodyDirty: false, bodySavedAt: 0, detailsOpen: false } as const;
|
|
108
|
+
/** How long after a body save its own debounced on_change may still arrive (the arm waits 500 ms). */
|
|
109
|
+
const TRAILING_CHANGE_MS = 1500;
|
|
110
|
+
const valueOf = (arg: unknown): string =>
|
|
111
|
+
String(typeof arg === "string" ? arg : (arg as { value?: unknown; text?: unknown } | undefined)?.value ?? (arg as { text?: unknown } | undefined)?.text ?? "");
|
|
112
|
+
const docJson = (doc: unknown): string => (doc ? JSON.stringify(doc) : "");
|
|
91
113
|
|
|
92
114
|
|
|
93
115
|
function shape(s: Partial<PostFormState>): PostFormState {
|
|
@@ -102,9 +124,11 @@ function shape(s: Partial<PostFormState>): PostFormState {
|
|
|
102
124
|
excerpt: s.excerpt ?? "",
|
|
103
125
|
body: s.body ?? "",
|
|
104
126
|
richContent: s.richContent ?? null,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
bodyJson: s.bodyJson ?? "",
|
|
128
|
+
detailsOpen: s.detailsOpen ?? false,
|
|
129
|
+
bodyDirty: s.bodyDirty ?? false,
|
|
130
|
+
bodyPending: s.bodyPending ?? false,
|
|
131
|
+
bodySavedAt: s.bodySavedAt ?? 0,
|
|
108
132
|
coverUrl: s.coverUrl ?? "",
|
|
109
133
|
originalCoverUrl: s.originalCoverUrl ?? "",
|
|
110
134
|
coverPreview: wixMediaUrl(s.coverUrl ?? "", WIX_DETAIL_IMAGE),
|
|
@@ -133,12 +157,23 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
133
157
|
return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
|
|
134
158
|
}
|
|
135
159
|
|
|
160
|
+
// [rico] #3371 — the RichContentEditor's image tool (the editor's upload contract). The component picks
|
|
161
|
+
// and PUTs the bytes; this service mints the signed Wix Media URL with the worker's own wixApi
|
|
162
|
+
// (@himalaya/wix-site-media rides the `wix_api` grant), so no token reaches the page. Registered
|
|
163
|
+
// BEFORE the form's service on purpose: the worker test harnesses treat a bundle's
|
|
164
|
+
// LAST defineService as its service.
|
|
165
|
+
ricosUploadService("himiOwnerLitePostFormUploads");
|
|
166
|
+
|
|
136
167
|
const svc = defineService<PostFormState, {
|
|
137
168
|
load: (s: PostFormState, arg?: unknown) => Promise<PostFormState>;
|
|
138
169
|
setCover: (s: PostFormState, arg?: unknown) => PostFormState;
|
|
170
|
+
setExcerpt: (s: PostFormState, arg?: unknown) => PostFormState;
|
|
171
|
+
detailsOpen: (s: PostFormState) => PostFormState;
|
|
172
|
+
detailsClose: (s: PostFormState) => PostFormState;
|
|
173
|
+
bodyChanged: (s: PostFormState, arg?: unknown) => PostFormState;
|
|
139
174
|
submit: (s: PostFormState, values?: Record<string, unknown>) => Promise<PostFormState>;
|
|
140
175
|
}>("himiOwnerLitePostForm", {
|
|
141
|
-
state: shape({ mode: startMode, id: initialInput.id ?? "" }),
|
|
176
|
+
state: shape({ mode: startMode, id: initialInput.id ?? "", bodyPending: startMode === "edit" }),
|
|
142
177
|
actions: {
|
|
143
178
|
load: async (s, arg) => {
|
|
144
179
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
@@ -146,13 +181,13 @@ const svc = defineService<PostFormState, {
|
|
|
146
181
|
// bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
|
|
147
182
|
// fallback. An id ⇒ edit; no id anywhere ⇒ the create shell.
|
|
148
183
|
const id = navInputId() || idFrom(arg, s.id);
|
|
149
|
-
if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
|
|
184
|
+
if (!id) return shape({ ...s, ...BODY_LOADED, mode: "create", id: "", bodyJson: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
|
|
150
185
|
if (isOwnerPreviewDemo()) {
|
|
151
186
|
const d = DEMO_POSTS.find((x) => x.id === id);
|
|
152
187
|
if (d) {
|
|
153
188
|
return shape({
|
|
154
189
|
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
155
|
-
title: d.title, excerpt: d.excerpt, body: d.body, isFeatured: "false",
|
|
190
|
+
...BODY_LOADED, title: d.title, excerpt: d.excerpt, body: d.body, bodyJson: docJson(plainPostBody(d.body)), isFeatured: "false",
|
|
156
191
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
157
192
|
});
|
|
158
193
|
}
|
|
@@ -166,11 +201,13 @@ const svc = defineService<PostFormState, {
|
|
|
166
201
|
...(p.richContent ? { richContent: JSON.stringify(p.richContent) } : {}),
|
|
167
202
|
coverUrl: postCoverUrl(p), isFeatured: p.isFeatured ? "true" : "false" });
|
|
168
203
|
return shape({
|
|
169
|
-
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
204
|
+
...s, ...BODY_LOADED, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
170
205
|
title: (p as any).title ?? "",
|
|
171
206
|
excerpt: (p as any).excerpt ?? "",
|
|
172
207
|
body: postBodyText(p.richContent),
|
|
173
208
|
richContent: p.richContent ?? null,
|
|
209
|
+
// The editor's seed is the fetched document, byte for byte: never encodeRichContent.
|
|
210
|
+
bodyJson: docJson(p.richContent),
|
|
174
211
|
coverUrl: postCoverUrl(p), originalCoverUrl: postCoverUrl(p),
|
|
175
212
|
isFeatured: (p as any).isFeatured ? "true" : "false",
|
|
176
213
|
status: "idle", error: "", savedMessage: "",
|
|
@@ -180,11 +217,11 @@ const svc = defineService<PostFormState, {
|
|
|
180
217
|
const cached = await cacheGet(id);
|
|
181
218
|
if (cached) {
|
|
182
219
|
return shape({
|
|
183
|
-
...s, mode: "edit", id, heading: "Edit post",
|
|
220
|
+
...s, ...BODY_LOADED, mode: "edit", id, heading: "Edit post",
|
|
184
221
|
title: String(cached.title ?? cached.name ?? ""),
|
|
185
222
|
excerpt: String(cached.excerpt ?? cached.summary ?? ""),
|
|
186
223
|
body: String(cached.body ?? ""),
|
|
187
|
-
|
|
224
|
+
...((raw: string) => ({ richContent: raw ? JSON.parse(raw) : null, bodyJson: raw || docJson(plainPostBody(String(cached.body ?? ""))) }))(cached.richContent ? String(cached.richContent) : ""),
|
|
188
225
|
coverUrl: String(cached.coverUrl ?? ""), originalCoverUrl: String(cached.coverUrl ?? ""),
|
|
189
226
|
isFeatured: cached.isFeatured === "true" ? "true" : "false",
|
|
190
227
|
status: "idle", error: "", savedMessage: "",
|
|
@@ -195,7 +232,7 @@ const svc = defineService<PostFormState, {
|
|
|
195
232
|
if (d) {
|
|
196
233
|
return shape({
|
|
197
234
|
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
198
|
-
title: d.title, excerpt: d.excerpt, body: d.body, isFeatured: "false",
|
|
235
|
+
...BODY_LOADED, title: d.title, excerpt: d.excerpt, body: d.body, bodyJson: docJson(plainPostBody(d.body)), isFeatured: "false",
|
|
199
236
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
200
237
|
});
|
|
201
238
|
}
|
|
@@ -203,46 +240,85 @@ const svc = defineService<PostFormState, {
|
|
|
203
240
|
return shape({ ...s, mode: "edit", id, status: "error", error: ownerLoadError(e) });
|
|
204
241
|
}
|
|
205
242
|
},
|
|
206
|
-
setCover: (s, arg) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
243
|
+
setCover: (s, arg) => shape({ ...s, coverUrl: valueOf(arg).trim(), error: "" }),
|
|
244
|
+
setExcerpt: (s, arg) => shape({ ...s, excerpt: valueOf(arg), error: "" }),
|
|
245
|
+
detailsOpen: (s) => shape({ ...s, detailsOpen: true }),
|
|
246
|
+
detailsClose: (s) => shape({ ...s, detailsOpen: false }),
|
|
247
|
+
// The editor's debounced `on_change` carries `args.dirty` only; the document stays in the form
|
|
248
|
+
// bag. A body edited after a save is no longer saved, so drop the stale confirmation — unless
|
|
249
|
+
// this is the trailing on_change of the very edit that save wrote.
|
|
250
|
+
bodyChanged: (s) => {
|
|
251
|
+
const trailing = s.status === "saved" && s.bodySavedAt > 0 && Date.now() - s.bodySavedAt < TRAILING_CHANGE_MS;
|
|
252
|
+
return shape({ ...s, bodyDirty: true, ...(trailing ? {} : { savedMessage: "", status: s.status === "saved" ? "idle" : s.status }) });
|
|
210
253
|
},
|
|
211
254
|
submit: async (s, values) => {
|
|
212
255
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
213
256
|
const v = values ?? {};
|
|
214
257
|
const title = String(v.title ?? s.title).trim();
|
|
258
|
+
// Excerpt and cover live in the "Post details" sheet, outside the Form: worker state carries them.
|
|
215
259
|
const excerpt = String(v.excerpt ?? s.excerpt).trim();
|
|
216
|
-
const body = String(v.body ?? s.body);
|
|
217
260
|
const coverUrl = String(v.coverUrl ?? s.coverUrl).trim();
|
|
218
261
|
// Queue replay carries the target explicitly; it may boot a fresh worker.
|
|
219
262
|
const targetId = String(v.id ?? s.id);
|
|
220
263
|
const editing = (v.mode ?? s.mode) === "edit" && !!targetId;
|
|
221
|
-
|
|
222
|
-
|
|
264
|
+
// A queued write replays in a FRESH worker and says so with `preserveBody`. This bundle's items
|
|
265
|
+
// carry the editor's string as `richContentJson`; the pre-editor bundle's carry a plain `body`.
|
|
266
|
+
const replay = typeof v.preserveBody === "string";
|
|
223
267
|
const coverChanged = typeof v.coverChanged === "string"
|
|
224
268
|
? v.coverChanged === "true" : coverUrl !== s.originalCoverUrl;
|
|
225
|
-
if (!preserveBody && s.bodyProtected) return shape({ ...s, status: "error", error: copyParam("ownerPostForm.richBodyError", "This body contains formatting or media. Edit it in your site dashboard to keep that content intact."), savedMessage: "" });
|
|
226
269
|
if (coverChanged && coverUrl && !/^https:\/\/[^\s/]+(?:\/[^\s]*)?$/.test(coverUrl)) return shape({ ...s, status: "error", error: copyParam("ownerPostForm.coverInvalid", "Enter an HTTPS image URL."), savedMessage: "" });
|
|
227
270
|
const isFeatured = boolString(v.isFeatured, s.isFeatured) === "true";
|
|
271
|
+
// Required fields are checked HERE, not only by the renderer: native Save does not gate on the
|
|
272
|
+
// RichContentEditor, and a queue replay skips the renderer entirely.
|
|
228
273
|
if (!title) return shape({ ...s, status: "error", error: copyParam("ownerPostForm.titleRequired", "Title is required"), savedMessage: "" });
|
|
274
|
+
|
|
275
|
+
// The body to WRITE — the editor's own document, never re-encoded. null leaves the stored body as it is.
|
|
276
|
+
let richContent: WixRichContent | null = null;
|
|
277
|
+
let bodyRaw = "";
|
|
278
|
+
let bodyTooLarge = false;
|
|
279
|
+
if (replay) {
|
|
280
|
+
const queued = typeof v.richContentJson === "string" ? parseRicosJson(v.richContentJson) : null;
|
|
281
|
+
if (queued) { richContent = queued as unknown as WixRichContent; bodyRaw = String(v.richContentJson); }
|
|
282
|
+
else if (v.preserveBody === "false") { richContent = plainPostBody(String(v.body ?? "")); bodyRaw = docJson(richContent); }
|
|
283
|
+
} else {
|
|
284
|
+
// Only an edit the editor reported counts: before load lands the editor is hidden (a body
|
|
285
|
+
// here was typed into the provisional empty editor), and an untouched bag holds the engine's
|
|
286
|
+
// re-serialization of the seed, which must not be written back.
|
|
287
|
+
const ev = s.bodyPending || !s.bodyDirty ? { kind: "absent" as const } : editorValue(v.body, s.bodyJson);
|
|
288
|
+
if (ev.kind === "invalid") return shape({ ...s, status: "error", error: copyParam("ownerPostForm.bodyInvalid", "Couldn’t read the post body from the editor. Nothing was saved; try again."), savedMessage: "" });
|
|
289
|
+
if (ev.kind === "doc") { richContent = ev.doc as unknown as WixRichContent; bodyRaw = ev.json; }
|
|
290
|
+
// A body past the rich-content limits is not written (every viewer would show its
|
|
291
|
+
// fallback); the other fields still save, and the screen says why.
|
|
292
|
+
bodyTooLarge = ev.kind === "too_large";
|
|
293
|
+
// "absent": the editor never became ready — save the rest, leave the body (user decision 2026-09-26).
|
|
294
|
+
// "unchanged": the editor handed back exactly its seed — nothing to write.
|
|
295
|
+
}
|
|
296
|
+
const bodyEdited = richContent !== null;
|
|
229
297
|
const draftPost: WixDraftPostWrite & { media?: Record<string, unknown> } = {
|
|
230
298
|
title,
|
|
231
|
-
...(
|
|
299
|
+
...(bodyEdited ? { richContent: richContent! } : {}),
|
|
232
300
|
isFeatured,
|
|
233
301
|
excerpt,
|
|
234
302
|
};
|
|
235
303
|
|
|
236
304
|
// Cache the full document so later offline metadata edits retain its body.
|
|
237
|
-
let
|
|
238
|
-
const localRow = (id: string): DbEntity => ({ id, name: title, title, excerpt, body, coverUrl, ...(
|
|
305
|
+
let kept: WixRichContent | null = bodyEdited ? richContent : (replay ? null : s.richContent);
|
|
306
|
+
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" });
|
|
239
307
|
const localId = editing ? targetId : `local-${Date.now()}`;
|
|
308
|
+
const savedState = (id: string, savedMessage: string) => shape({
|
|
309
|
+
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
310
|
+
title, excerpt, body: postBodyText(kept), richContent: kept, bodyJson: bodyEdited ? bodyRaw : s.bodyJson,
|
|
311
|
+
bodyDirty: bodyEdited ? false : s.bodyDirty, bodySavedAt: bodyEdited ? Date.now() : 0,
|
|
312
|
+
coverUrl, originalCoverUrl: coverUrl, isFeatured: isFeatured ? "true" : "false",
|
|
313
|
+
status: "saved", savedMessage, savedId: id,
|
|
314
|
+
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.") : "",
|
|
315
|
+
});
|
|
240
316
|
try {
|
|
241
|
-
// A replay worker starts without the loaded document. Entity upserts
|
|
242
|
-
//
|
|
243
|
-
if (
|
|
317
|
+
// A replay worker starts without the loaded document. Entity upserts replace whole rows, so
|
|
318
|
+
// retain the cached rich body before writing back.
|
|
319
|
+
if (!bodyEdited && !kept) {
|
|
244
320
|
const cached = await cacheGet(targetId);
|
|
245
|
-
if (cached?.richContent)
|
|
321
|
+
if (cached?.richContent) kept = JSON.parse(String(cached.richContent));
|
|
246
322
|
}
|
|
247
323
|
// Blog cover images reference Media Manager file ids. The legacy editor
|
|
248
324
|
// wrote coverImage.id; current Blog v3 expects media.wixMedia.image.id.
|
|
@@ -254,6 +330,8 @@ const svc = defineService<PostFormState, {
|
|
|
254
330
|
draftPost.media = { wixMedia: { image: { id: result.file.id } }, displayed: true, custom: true };
|
|
255
331
|
}
|
|
256
332
|
}
|
|
333
|
+
// The draft PATCH has no field mask (`fieldsets` only shapes the response): leaving
|
|
334
|
+
// `richContent` out of `draftPost` is the whole omission.
|
|
257
335
|
let id = targetId;
|
|
258
336
|
if (editing) {
|
|
259
337
|
await updateDraftPost(targetId, draftPost);
|
|
@@ -264,31 +342,23 @@ const svc = defineService<PostFormState, {
|
|
|
264
342
|
if (id !== localId) await cacheDrop(localId);
|
|
265
343
|
}
|
|
266
344
|
await cachePut(localRow(id));
|
|
267
|
-
return
|
|
268
|
-
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
269
|
-
title, excerpt, body, richContent, coverUrl, originalCoverUrl: coverUrl, isFeatured: isFeatured ? "true" : "false",
|
|
270
|
-
status: "saved", error: "",
|
|
271
|
-
savedMessage: editing
|
|
272
|
-
? copyParam("ownerPostForm.updatedMessage", "Post updated.")
|
|
273
|
-
: copyParam("ownerPostForm.createdMessage", "Draft created."),
|
|
274
|
-
savedId: id,
|
|
275
|
-
});
|
|
345
|
+
return savedState(id, editing ? copyParam("ownerPostForm.updatedMessage", "Post updated.") : copyParam("ownerPostForm.createdMessage", "Draft created."));
|
|
276
346
|
} catch (e) {
|
|
277
347
|
if (shouldQueueOwnerWrite(e)) {
|
|
278
348
|
// Demo / web-preview (no owner bridge; the record came from demo sample
|
|
279
349
|
// data, marked by demoNote): a queued write could never replay, so answer
|
|
280
350
|
// with the demo save note instead of the real-offline write-behind.
|
|
281
351
|
if (s.demoNote) return demoRefusal(s);
|
|
282
|
-
// Real device offline: cache and queue the edit for reconnect.
|
|
352
|
+
// Real device offline: cache and queue the edit for reconnect. `preserveBody` stays
|
|
353
|
+
// "true" even for a body edit, so a bundle rolled back past this change keeps the stored
|
|
354
|
+
// body rather than flattening it to text. This bundle replays `richContentJson`.
|
|
283
355
|
await cachePut(localRow(localId));
|
|
284
|
-
await queueWrite("tier5.himiOwnerLitePostForm.submit", {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
status: "saved", error: "",
|
|
289
|
-
savedMessage: copyParam("ownerPostForm.savedOffline", "Saved offline — will sync when you reconnect."),
|
|
290
|
-
savedId: localId,
|
|
356
|
+
await queueWrite("tier5.himiOwnerLitePostForm.submit", {
|
|
357
|
+
id: targetId, title, excerpt, coverUrl, coverChanged: String(coverChanged), preserveBody: "true",
|
|
358
|
+
body: postBodyText(kept), ...(bodyEdited ? { richContentJson: bodyRaw } : {}),
|
|
359
|
+
isFeatured: String(isFeatured), mode: editing ? "edit" : "create",
|
|
291
360
|
});
|
|
361
|
+
return savedState(localId, copyParam("ownerPostForm.savedOffline", "Saved offline — will sync when you reconnect."));
|
|
292
362
|
}
|
|
293
363
|
return shape({ ...s, status: "error", error: ownerLoadError(e), savedMessage: "" });
|
|
294
364
|
}
|
|
@@ -296,4 +366,20 @@ const svc = defineService<PostFormState, {
|
|
|
296
366
|
},
|
|
297
367
|
});
|
|
298
368
|
|
|
369
|
+
// [rico] #3371 — adopter contract (#3368's review): the editor seeds ONCE, from the first
|
|
370
|
+
// document `bodyJson` holds. A cold boot hydrates this service from its last persisted snapshot, so
|
|
371
|
+
// that first value would be the document as it was when this screen last ran — and a Save after
|
|
372
|
+
// typing would write it over whatever the post holds now. Blank the hydrated seed before anything
|
|
373
|
+
// renders: the editor stays provisional (writes nothing) until `load` publishes the fresh document.
|
|
374
|
+
// In place, at boot, because no action has run yet; the state object is the runtime's own merge.
|
|
375
|
+
{
|
|
376
|
+
const booted = (svc as { getState?: () => Partial<PostFormState> | undefined }).getState?.();
|
|
377
|
+
if (booted && typeof booted === "object") {
|
|
378
|
+
if (booted.bodyJson) booted.bodyJson = "";
|
|
379
|
+
// Nor does it carry this run's UI over: the editor waits for this run's load, the sheet is shut.
|
|
380
|
+
booted.bodyPending = startMode === "edit";
|
|
381
|
+
booted.detailsOpen = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
299
385
|
export { svc };
|