@wix/himalaya-cli 0.1172.0 → 0.1174.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/stdlib/wix-sdk/stores-v1/src/extensions.d.ts +13 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/products-v3.d.ts +13 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +10 -3
- package/dist/cli.mjs +38 -16
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +15 -3
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +172 -6
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/community-club-owner/dev/index.ts +257 -8
- package/templates/community-club-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/community-club-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/events-meetups-owner/dev/index.ts +87 -4
- package/templates/events-meetups-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/gym-fitness-owner/dev/index.ts +257 -8
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/nonprofit-cause-owner/dev/index.ts +87 -4
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/owner-lite/dev/index.ts +512 -17
- package/templates/owner-lite/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/owner-lite/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq-question-form/index.ts +77 -10
- package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/podcast-creator-owner/dev/index.ts +257 -8
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/portfolio-creative-owner/dev/index.ts +172 -6
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/restaurant-owner/dev/index.ts +257 -8
- package/templates/restaurant-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/restaurant-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/retail-store-owner/dev/index.ts +172 -6
- package/templates/retail-store-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/retail-store-owner/tier5-src/_shared/owner-rich-guard.ts +1 -9
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +130 -27
- package/templates/yoga-studio-owner/dev/index.ts +87 -4
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-editor-value.ts +71 -5
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +117 -51
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +17 -42
- package/templates/events-meetups-owner/tier5-src/_shared/owner-rich-guard.ts +0 -35
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +0 -35
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +0 -35
|
@@ -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, RICH_CONTENT_LIMITS, type RicosDocument } from "@himalaya/wix-rich-content";
|
|
11
|
+
import { parseRicosJson, isPlainRicos, RICH_CONTENT_LIMITS, type RicosDocument } from "@himalaya/wix-rich-content";
|
|
12
12
|
|
|
13
13
|
export type EditorValue =
|
|
14
14
|
| { kind: "absent" } // leave the stored body untouched
|
|
@@ -45,12 +45,78 @@ function overLimits(doc: RicosDocument): boolean {
|
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const TEXT_ONLY = new Set(["PARAGRAPH", "TEXT", "HEADING", "BULLETED_LIST", "ORDERED_LIST", "LIST_ITEM", "BLOCKQUOTE", "CODE_BLOCK"]);
|
|
49
|
+
/** No visible text and no non-text node (an image, a divider, an embed…): an empty body. */
|
|
50
|
+
export function isEmptyDocument(doc: RicosDocument): boolean {
|
|
51
|
+
const empty = (nodes: RicosDocument["nodes"] | undefined): boolean => (nodes ?? []).every((n) =>
|
|
52
|
+
TEXT_ONLY.has(String(n?.type)) && !String(n.textData?.text ?? "").trim() && empty(n.nodes));
|
|
53
|
+
return empty(doc.nodes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A Ricos JSON string checked as every viewer would: the limits first, then the shape. */
|
|
57
|
+
function checkedDocument(raw: string): EditorValue {
|
|
52
58
|
if (utf8Bytes(raw) > RICH_CONTENT_LIMITS.maxBytes) return { kind: "too_large" };
|
|
53
59
|
const doc = parseRicosJson(raw);
|
|
54
60
|
if (!doc) return { kind: "invalid" };
|
|
55
61
|
return overLimits(doc) ? { kind: "too_large" } : { kind: "doc", doc, json: raw };
|
|
56
62
|
}
|
|
63
|
+
|
|
64
|
+
/** `raw` is `values[name]`; `seededJson` is the string the form published as the editor's seed. */
|
|
65
|
+
export function editorValue(raw: unknown, seededJson: string): EditorValue {
|
|
66
|
+
if (typeof raw !== "string" || raw === "") return { kind: "absent" };
|
|
67
|
+
if (seededJson !== "" && raw === seededJson) return { kind: "unchanged" };
|
|
68
|
+
const checked = checkedDocument(raw);
|
|
69
|
+
// An empty editor over no body (a record that had none) writes nothing, rather than an empty document.
|
|
70
|
+
if (checked.kind === "doc" && seededJson === "" && isEmptyDocument(checked.doc)) return { kind: "unchanged" };
|
|
71
|
+
return checked;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── The per-field editor machinery every owner form shares (post body, event / product description,
|
|
75
|
+
// FAQ answer). Each form keeps `<key>Json` (the seed), `<key>Pending` and `<key>Dirty` in its state.
|
|
76
|
+
|
|
77
|
+
/** `<key>Pending`: until this run's load lands, the descriptor hides the editor — it seeds ONCE, so a
|
|
78
|
+
* provisional editor the owner typed into would keep that text over the real document.
|
|
79
|
+
* `<key>Dirty`: the editor's on_change fired since the field was loaded or saved. Once ready the bag
|
|
80
|
+
* ALWAYS holds a document — the engine's own re-serialization of the seed, not byte-equal to it — so
|
|
81
|
+
* only this says the owner edited the field. Every arm fires a pending on_change before it releases a
|
|
82
|
+
* Form submit, so an edit made inside the debounce window is reported before the submit reads it. */
|
|
83
|
+
export interface EditorGate { pending: boolean; dirty: boolean }
|
|
84
|
+
|
|
85
|
+
/** What a live submit does with `values[name]`: only an edit the editor reported counts. */
|
|
86
|
+
export function editorSubmitValue(raw: unknown, seededJson: string, gate: EditorGate): EditorValue {
|
|
87
|
+
return gate.pending || !gate.dirty ? { kind: "absent" } : editorValue(raw, seededJson);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A queued write's document. A replay runs in a FRESH worker with no editor, so the string is checked
|
|
91
|
+
* again (shape and limits); "absent" when the item carries none. Never "unchanged": it was an edit. */
|
|
92
|
+
export function queuedDocument(raw: unknown): EditorValue {
|
|
93
|
+
return typeof raw === "string" && raw !== "" ? checkedDocument(raw) : { kind: "absent" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The editor's on_change: the field is now edited, and a confirmation of an earlier save is stale.
|
|
97
|
+
* No timing window: an arm fires the pending on_change BEFORE it releases a submit, so any on_change
|
|
98
|
+
* that arrives after a save carries an edit that save did not write. */
|
|
99
|
+
export function editorChanged<S extends { status: string; savedMessage: string }>(s: S): Pick<S, "status" | "savedMessage"> {
|
|
100
|
+
return { status: s.status === "saved" ? "idle" : s.status, savedMessage: "" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The offline cache's verdict on the stored body (`<key>Protected`): rich unless KNOWN plain. From the
|
|
104
|
+
* document when the form knows it; otherwise plain only when the record was loaded as plain. A bundle
|
|
105
|
+
* rolled back past the editor reads this flag to decide whether its plain field may overwrite the body. */
|
|
106
|
+
export function cachedRichVerdict(doc: RicosDocument | null, knownPlain: boolean): "true" | "false" {
|
|
107
|
+
if (doc) return isPlainRicos(doc) ? "false" : "true";
|
|
108
|
+
return knownPlain ? "false" : "true";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Cold boot: the runtime lays this service's last persisted snapshot over its declared state, so the
|
|
112
|
+
* editor's first seed would be the document as it was when the screen last ran — and a Save after
|
|
113
|
+
* typing would write it over whatever the record holds now. Blank the hydrated seed and reset the gate
|
|
114
|
+
* before anything renders, in place (no action has run yet). `extra` resets further UI state. */
|
|
115
|
+
export function resetHydratedEditor(svc: unknown, keys: { json: string; pending: string; dirty: string }, pendingAtBoot: boolean, extra: Record<string, unknown> = {}): void {
|
|
116
|
+
const booted = (svc as { getState?: () => Record<string, unknown> | undefined }).getState?.();
|
|
117
|
+
if (!booted || typeof booted !== "object") return;
|
|
118
|
+
if (booted[keys.json]) booted[keys.json] = "";
|
|
119
|
+
booted[keys.pending] = pendingAtBoot;
|
|
120
|
+
booted[keys.dirty] = false;
|
|
121
|
+
Object.assign(booted, extra);
|
|
122
|
+
}
|
|
@@ -10,9 +10,9 @@ 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
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
13
|
+
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, resetHydratedEditor } from "../_shared/owner-editor-value.js";
|
|
14
|
+
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
15
|
+
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
16
16
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
17
17
|
import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
|
|
18
18
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
@@ -39,14 +39,27 @@ interface EventFormState {
|
|
|
39
39
|
id: string;
|
|
40
40
|
heading: string;
|
|
41
41
|
title: string;
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
42
|
+
/** The Events v3 Ricos `description` as text (read with `fields=TEXTS`): the plain field's value,
|
|
43
|
+
* the locked preview, and the cached row's mirror. */
|
|
44
44
|
description: string;
|
|
45
|
-
/** The description as loaded (or last saved).
|
|
45
|
+
/** The description as loaded (or last saved). The plain field writes `description` only when the
|
|
46
46
|
* submitted value differs from it, so an untouched body is never re-written. */
|
|
47
47
|
originalDescription: string;
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
48
|
+
/** How THIS record's description is edited:
|
|
49
|
+
* "editor" — the RichContentEditor, seeded with `descriptionJson` (every live event; sample data);
|
|
50
|
+
* "plain" — the plain text field (an offline row cached as plain before the editor existed);
|
|
51
|
+
* "locked" — neither (an offline row cached as rich, with no document to seed the editor). */
|
|
52
|
+
descriptionMode: "editor" | "plain" | "locked";
|
|
53
|
+
/** The Ricos description as a JSON string: the editor's seed ("" when the event has none, and until
|
|
54
|
+
* this run's load lands — see the boot reset below). Re-based to the saved document after a save. */
|
|
55
|
+
descriptionJson: string;
|
|
56
|
+
descriptionFieldHidden: boolean; // the plain text field (bag key `descriptionText`)
|
|
57
|
+
descriptionEditorHidden: boolean; // the RichContentEditor (bag key `description`)
|
|
58
|
+
/** The editor gate (`EditorGate` in _shared/owner-editor-value.ts): edited since load/save, and
|
|
59
|
+
* hidden until this run's load lands. */
|
|
60
|
+
descriptionDirty: boolean;
|
|
61
|
+
descriptionPending: boolean;
|
|
62
|
+
/** Derived from the mode: "locked" shows the preview + `descriptionNote`, and refuses an edit. */
|
|
50
63
|
descriptionProtected: boolean;
|
|
51
64
|
descriptionEditable: boolean;
|
|
52
65
|
descriptionNote: string;
|
|
@@ -99,9 +112,14 @@ function navInputId(): string {
|
|
|
99
112
|
return typeof input?.id === "string" ? input.id : "";
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
/** What every load that publishes a description resets: the editor may show it, nothing is edited yet. */
|
|
116
|
+
const DESCRIPTION_LOADED = { descriptionPending: false, descriptionDirty: false } as const;
|
|
117
|
+
const docJson = (doc: unknown): string => (doc ? JSON.stringify(doc) : "");
|
|
118
|
+
|
|
102
119
|
function shape(s: Partial<EventFormState>): EventFormState {
|
|
103
120
|
const id = typeof s.id === "string" ? s.id : "";
|
|
104
121
|
const savedId = typeof s.savedId === "string" ? s.savedId : id;
|
|
122
|
+
const mode = s.descriptionMode ?? "editor";
|
|
105
123
|
return {
|
|
106
124
|
mode: "edit",
|
|
107
125
|
id,
|
|
@@ -109,9 +127,15 @@ function shape(s: Partial<EventFormState>): EventFormState {
|
|
|
109
127
|
title: s.title ?? "",
|
|
110
128
|
description: s.description ?? "",
|
|
111
129
|
originalDescription: s.originalDescription ?? s.description ?? "",
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
descriptionMode: mode,
|
|
131
|
+
descriptionJson: s.descriptionJson ?? "",
|
|
132
|
+
descriptionFieldHidden: mode !== "plain",
|
|
133
|
+
descriptionEditorHidden: mode !== "editor",
|
|
134
|
+
descriptionDirty: s.descriptionDirty ?? false,
|
|
135
|
+
descriptionPending: s.descriptionPending ?? false,
|
|
136
|
+
descriptionProtected: mode === "locked",
|
|
137
|
+
descriptionEditable: mode !== "locked",
|
|
138
|
+
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.") : "",
|
|
115
139
|
when: s.when ?? "—",
|
|
116
140
|
location: s.location ?? "—",
|
|
117
141
|
status: s.status ?? "idle",
|
|
@@ -137,11 +161,18 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
137
161
|
return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
// The RichContentEditor's image tool (the editor's upload contract). The component picks and PUTs
|
|
165
|
+
// the bytes; this service mints the signed Wix Media URL with the worker's own wixApi, so no token
|
|
166
|
+
// reaches the page. Registered BEFORE the form's service on purpose: the worker test harnesses treat a
|
|
167
|
+
// bundle's LAST defineService as its service.
|
|
168
|
+
ricosUploadService("himiOwnerLiteEventFormUploads");
|
|
169
|
+
|
|
140
170
|
const svc = defineService<EventFormState, {
|
|
141
171
|
load: (s: EventFormState, arg?: unknown) => Promise<EventFormState>;
|
|
172
|
+
descriptionChanged: (s: EventFormState, arg?: unknown) => EventFormState;
|
|
142
173
|
submit: (s: EventFormState, values?: Record<string, unknown>) => Promise<EventFormState>;
|
|
143
174
|
}>("himiOwnerLiteEventForm", {
|
|
144
|
-
state: shape({ mode: startMode, id: initialInput.id ?? "" }),
|
|
175
|
+
state: shape({ mode: startMode, id: initialInput.id ?? "", descriptionPending: true }),
|
|
145
176
|
actions: {
|
|
146
177
|
load: async (s, arg) => {
|
|
147
178
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
@@ -156,6 +187,7 @@ const svc = defineService<EventFormState, {
|
|
|
156
187
|
return shape({
|
|
157
188
|
...s, mode, id, heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
158
189
|
title: d.title, description: d.description, when: d.dateLine, location: d.location,
|
|
190
|
+
...DESCRIPTION_LOADED, descriptionMode: "editor", descriptionJson: docJson(plainPostBody(d.description)),
|
|
159
191
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
160
192
|
});
|
|
161
193
|
}
|
|
@@ -182,7 +214,10 @@ const svc = defineService<EventFormState, {
|
|
|
182
214
|
const raw = (ev as unknown as { description?: unknown }).description;
|
|
183
215
|
const doc = raw && typeof raw === "object" && Array.isArray((raw as { nodes?: unknown }).nodes) ? raw as Parameters<typeof postBodyText>[0] : null;
|
|
184
216
|
const text = doc ? postBodyText(doc) : typeof raw === "string" ? raw.trim() : "";
|
|
185
|
-
|
|
217
|
+
// Edited in the RichContentEditor, seeded with the event's own document byte for byte (never
|
|
218
|
+
// re-encoded); a legacy string seeds as paragraphs, and none seeds an empty editor.
|
|
219
|
+
return { ...DESCRIPTION_LOADED, description: text, originalDescription: text, descriptionMode: "editor" as const,
|
|
220
|
+
descriptionJson: doc ? JSON.stringify(raw) : text ? docJson(plainPostBody(text)) : "" };
|
|
186
221
|
})(),
|
|
187
222
|
when: row.dateLine || "—",
|
|
188
223
|
location: row.location || "—",
|
|
@@ -198,8 +233,11 @@ const svc = defineService<EventFormState, {
|
|
|
198
233
|
...s, mode, id, heading: "Edit event",
|
|
199
234
|
title: String(cached.title ?? cached.name ?? ""),
|
|
200
235
|
description: String(cached.description ?? ""),
|
|
201
|
-
|
|
202
|
-
|
|
236
|
+
originalDescription: String(cached.description ?? ""), ...DESCRIPTION_LOADED,
|
|
237
|
+
// A row cached with its document reopens in the editor. An older row has none, so its verdict
|
|
238
|
+
// decides: plain when it was plain, else locked (it carries no verdict either: fail closed).
|
|
239
|
+
...(cached.descriptionJson ? { descriptionMode: "editor" as const, descriptionJson: String(cached.descriptionJson) }
|
|
240
|
+
: { descriptionMode: cached.descriptionProtected === "false" ? "plain" as const : "locked" as const, descriptionJson: "" }),
|
|
203
241
|
when: String(cached.when ?? cached.dateLine ?? "—"),
|
|
204
242
|
location: String(cached.location ?? "—"),
|
|
205
243
|
status: "idle", error: "", savedMessage: "",
|
|
@@ -211,6 +249,7 @@ const svc = defineService<EventFormState, {
|
|
|
211
249
|
return shape({
|
|
212
250
|
...s, mode, id, heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
213
251
|
title: d.title, description: d.description, when: d.dateLine, location: d.location,
|
|
252
|
+
...DESCRIPTION_LOADED, descriptionMode: "editor", descriptionJson: docJson(plainPostBody(d.description)),
|
|
214
253
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
215
254
|
});
|
|
216
255
|
}
|
|
@@ -218,59 +257,83 @@ const svc = defineService<EventFormState, {
|
|
|
218
257
|
return shape({ ...s, mode, id, status: "error", error: ownerLoadError(e) });
|
|
219
258
|
}
|
|
220
259
|
},
|
|
260
|
+
// 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) }),
|
|
221
262
|
submit: async (s, values) => {
|
|
222
263
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
223
264
|
const v = values ?? {};
|
|
224
265
|
const title = String(v.title ?? s.title).trim();
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
266
|
+
// The description, per record mode. A queue replay boots a FRESH worker and says so with
|
|
267
|
+
// `descriptionEdited`: this bundle's editor items add the document as `descriptionJson`; the
|
|
268
|
+
// plain items (and the pre-editor bundle's) carry plain text only.
|
|
269
|
+
const replay = typeof v.descriptionEdited === "string";
|
|
270
|
+
let doc: { nodes: unknown[] } | null = null; // the Ricos description to WRITE; null leaves it as it is
|
|
271
|
+
let docRaw = ""; // the editor's own string (queue item, re-seed, cache)
|
|
272
|
+
let tooLarge = false;
|
|
273
|
+
if (replay) {
|
|
274
|
+
const queued = queuedDocument(v.descriptionJson);
|
|
275
|
+
// A queued document that no longer reads (or is past the limits) writes NOTHING, and says so.
|
|
276
|
+
if (queued.kind === "invalid" || queued.kind === "too_large") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerEventForm.queuedDescriptionError", "The description saved offline could not be read, so nothing was written. Edit it again.") });
|
|
277
|
+
if (queued.kind === "doc") { doc = queued.doc as { nodes: unknown[] }; docRaw = queued.json; }
|
|
278
|
+
else if (v.descriptionEdited === "true") { const t = String(v.description ?? "").trim(); doc = t ? plainPostBody(t) as { nodes: unknown[] } : { nodes: [] }; }
|
|
279
|
+
} else if (s.descriptionMode === "editor") {
|
|
280
|
+
// Only an edit the editor reported counts (EditorGate). Absent (the editor never became ready):
|
|
281
|
+
// save the rest and leave the description as it is.
|
|
282
|
+
const ev = editorSubmitValue(v.description, s.descriptionJson, { pending: s.descriptionPending, dirty: s.descriptionDirty });
|
|
283
|
+
if (ev.kind === "invalid") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerEventForm.editorReadError", "Couldn’t read the description from the editor. Nothing was saved; try again.") });
|
|
284
|
+
if (ev.kind === "doc") { doc = ev.doc as { nodes: unknown[] }; docRaw = ev.json; }
|
|
285
|
+
tooLarge = ev.kind === "too_large";
|
|
286
|
+
} else {
|
|
287
|
+
// Plain or locked: the plain text field, under its own bag key.
|
|
288
|
+
const t = String(v.descriptionText ?? s.description).trim();
|
|
289
|
+
if (t !== s.originalDescription.trim()) {
|
|
290
|
+
if (s.descriptionMode === "locked") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerEventForm.richDescriptionError", "This description has formatting or media. Edit it in your site dashboard to keep that content intact.") });
|
|
291
|
+
doc = t ? plainPostBody(t) as { nodes: unknown[] } : { nodes: [] };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const description = doc ? postBodyText(doc as never) : s.description; // the plain-text mirror
|
|
229
295
|
if (!title) return shape({ ...s, status: "error", error: copyParam("ownerEventForm.titleRequired", "Title is required"), savedMessage: "" });
|
|
230
296
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerEventForm.errorNoEvent", "No event to edit."), savedMessage: "" });
|
|
231
297
|
|
|
232
|
-
// updateEvent(id, event) wraps the body as { event } internally, so pass the
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// A local structural type, not WixEventWrite: templates compile against the
|
|
298
|
+
// updateEvent(id, event) wraps the body as { event } internally, so pass the event fields
|
|
299
|
+
// directly. Only what the owner edited is written, the description always as a Ricos document
|
|
300
|
+
// (never a string). A local structural type, not WixEventWrite: templates compile against the
|
|
236
301
|
// PUBLISHED SDK until its next release.
|
|
237
|
-
const event: { title: string; description?: { nodes: unknown[] } } = {
|
|
238
|
-
title, ...(descriptionEdited ? { description: description ? plainPostBody(description) as { nodes: unknown[] } : { nodes: [] } } : {}),
|
|
239
|
-
};
|
|
302
|
+
const event: { title: string; description?: { nodes: unknown[] } } = { title, ...(doc ? { description: doc } : {}) };
|
|
240
303
|
|
|
241
|
-
// Optimistic local write — the detail/list repaint from cache immediately.
|
|
242
|
-
|
|
243
|
-
|
|
304
|
+
// Optimistic local write — the detail/list repaint from cache immediately. The document the record
|
|
305
|
+
// now holds, when known, rides along so an offline reopen seeds the editor with it.
|
|
306
|
+
const loadedJson = s.descriptionMode === "editor" && !s.descriptionPending ? s.descriptionJson : "";
|
|
307
|
+
const heldJson = docRaw || (doc ? JSON.stringify(doc) : loadedJson);
|
|
308
|
+
const heldDoc = doc ?? (loadedJson ? parseRicosJson(loadedJson) : null);
|
|
309
|
+
// Plain only when KNOWN plain: a plain row, or a loaded event with no description. Before load
|
|
310
|
+
// lands nothing is known, so the row stays protected (fail closed).
|
|
311
|
+
const knownPlain = s.descriptionMode === "plain" || (s.descriptionMode === "editor" && !s.descriptionPending && s.descriptionJson === "");
|
|
312
|
+
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) => shape({
|
|
314
|
+
...s, mode: "edit", id: s.id, heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
315
|
+
title, description, originalDescription: doc ? description : s.originalDescription,
|
|
316
|
+
...(docRaw ? { descriptionJson: docRaw } : {}),
|
|
317
|
+
descriptionDirty: doc ? false : s.descriptionDirty,
|
|
318
|
+
status: "saved", savedMessage, savedId: s.id,
|
|
319
|
+
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
|
+
});
|
|
244
321
|
try {
|
|
245
322
|
await updateEvent(s.id, event);
|
|
246
|
-
return
|
|
247
|
-
...s,
|
|
248
|
-
mode: "edit",
|
|
249
|
-
id: s.id,
|
|
250
|
-
heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
251
|
-
title,
|
|
252
|
-
description,
|
|
253
|
-
originalDescription: descriptionEdited ? description : s.originalDescription,
|
|
254
|
-
status: "saved",
|
|
255
|
-
error: "",
|
|
256
|
-
savedMessage: copyParam("ownerEventForm.savedMessage", "Event saved."),
|
|
257
|
-
savedId: s.id,
|
|
258
|
-
});
|
|
323
|
+
return savedState(copyParam("ownerEventForm.savedMessage", "Event saved."));
|
|
259
324
|
} catch (e) {
|
|
260
325
|
if (shouldQueueOwnerWrite(e)) {
|
|
261
326
|
// Demo / web-preview (no owner bridge; the record came from demo sample
|
|
262
327
|
// data, marked by demoNote): a queued write could never replay, so answer
|
|
263
328
|
// with the demo save note instead of the real-offline write-behind.
|
|
264
329
|
if (s.demoNote) return demoRefusal(s);
|
|
265
|
-
// Real device offline: the optimistic cache row already shows; queue the
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
savedId: s.id,
|
|
273
|
-
});
|
|
330
|
+
// Real device offline: the optimistic cache row already shows; queue the write to replay
|
|
331
|
+
// (WriteQueue, idempotent) when connectivity returns. A document rides as `descriptionJson`
|
|
332
|
+
// with descriptionEdited "false", so a bundle rolled back past this change leaves the
|
|
333
|
+
// description rather than flattening it to text.
|
|
334
|
+
await queueWrite("tier5.himiOwnerLiteEventForm.submit", { id: s.id, title, description,
|
|
335
|
+
...(docRaw ? { descriptionJson: docRaw, descriptionEdited: "false" } : { descriptionEdited: String(doc !== null) }) });
|
|
336
|
+
return savedState(copyParam("ownerEventForm.savedOffline", "Saved offline — will sync when you reconnect."));
|
|
274
337
|
}
|
|
275
338
|
return shape({ ...s, status: "error", error: ownerLoadError(e), savedMessage: "" });
|
|
276
339
|
}
|
|
@@ -278,4 +341,7 @@ const svc = defineService<EventFormState, {
|
|
|
278
341
|
},
|
|
279
342
|
});
|
|
280
343
|
|
|
344
|
+
// Adopter contract: the editor seeds once, so a cold boot must not seed it with the last run's document.
|
|
345
|
+
resetHydratedEditor(svc, { json: "descriptionJson", pending: "descriptionPending", dirty: "descriptionDirty" }, true);
|
|
346
|
+
|
|
281
347
|
export { svc };
|
|
@@ -4,8 +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 {
|
|
8
|
-
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
7
|
+
import { editorSubmitValue, queuedDocument, editorChanged, resetHydratedEditor } from "../_shared/owner-editor-value.js";
|
|
9
8
|
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
10
9
|
import { wixCall } from "@himalaya/wix-site-media";
|
|
11
10
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
|
|
@@ -43,15 +42,10 @@ interface PostFormState {
|
|
|
43
42
|
bodyJson: string;
|
|
44
43
|
/** The "Post details" sheet (excerpt + cover) is open. */
|
|
45
44
|
detailsOpen: boolean;
|
|
46
|
-
/** The editor
|
|
47
|
-
*
|
|
48
|
-
* byte-equal to it — so only this says the owner actually edited the body. */
|
|
45
|
+
/** The editor gate (`EditorGate` in _shared/owner-editor-value.ts): the editor's on_change fired since
|
|
46
|
+
* the body was loaded or saved, and (edit mode) the editor is hidden until this run's load lands. */
|
|
49
47
|
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
48
|
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;
|
|
55
49
|
coverUrl: string;
|
|
56
50
|
originalCoverUrl: string;
|
|
57
51
|
coverPreview: string;
|
|
@@ -104,9 +98,7 @@ function boolString(value: unknown, fallback: string): string {
|
|
|
104
98
|
return fallback;
|
|
105
99
|
}
|
|
106
100
|
/** 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,
|
|
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;
|
|
101
|
+
const BODY_LOADED = { bodyPending: false, bodyDirty: false, detailsOpen: false } as const;
|
|
110
102
|
const valueOf = (arg: unknown): string =>
|
|
111
103
|
String(typeof arg === "string" ? arg : (arg as { value?: unknown; text?: unknown } | undefined)?.value ?? (arg as { text?: unknown } | undefined)?.text ?? "");
|
|
112
104
|
const docJson = (doc: unknown): string => (doc ? JSON.stringify(doc) : "");
|
|
@@ -128,7 +120,6 @@ function shape(s: Partial<PostFormState>): PostFormState {
|
|
|
128
120
|
detailsOpen: s.detailsOpen ?? false,
|
|
129
121
|
bodyDirty: s.bodyDirty ?? false,
|
|
130
122
|
bodyPending: s.bodyPending ?? false,
|
|
131
|
-
bodySavedAt: s.bodySavedAt ?? 0,
|
|
132
123
|
coverUrl: s.coverUrl ?? "",
|
|
133
124
|
originalCoverUrl: s.originalCoverUrl ?? "",
|
|
134
125
|
coverPreview: wixMediaUrl(s.coverUrl ?? "", WIX_DETAIL_IMAGE),
|
|
@@ -244,13 +235,8 @@ const svc = defineService<PostFormState, {
|
|
|
244
235
|
setExcerpt: (s, arg) => shape({ ...s, excerpt: valueOf(arg), error: "" }),
|
|
245
236
|
detailsOpen: (s) => shape({ ...s, detailsOpen: true }),
|
|
246
237
|
detailsClose: (s) => shape({ ...s, detailsOpen: false }),
|
|
247
|
-
// The editor's
|
|
248
|
-
|
|
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 }) });
|
|
253
|
-
},
|
|
238
|
+
// 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) }),
|
|
254
240
|
submit: async (s, values) => {
|
|
255
241
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
256
242
|
const v = values ?? {};
|
|
@@ -277,14 +263,15 @@ const svc = defineService<PostFormState, {
|
|
|
277
263
|
let bodyRaw = "";
|
|
278
264
|
let bodyTooLarge = false;
|
|
279
265
|
if (replay) {
|
|
280
|
-
const queued =
|
|
281
|
-
|
|
266
|
+
const queued = queuedDocument(v.richContentJson);
|
|
267
|
+
// A queued body that no longer reads (or is past the limits) writes NOTHING, and says so.
|
|
268
|
+
if (queued.kind === "invalid" || queued.kind === "too_large") return shape({ ...s, status: "error", error: copyParam("ownerPostForm.queuedBodyError", "The post body saved offline could not be read, so nothing was written. Edit it again."), savedMessage: "" });
|
|
269
|
+
if (queued.kind === "doc") { richContent = queued.doc as unknown as WixRichContent; bodyRaw = queued.json; }
|
|
282
270
|
else if (v.preserveBody === "false") { richContent = plainPostBody(String(v.body ?? "")); bodyRaw = docJson(richContent); }
|
|
283
271
|
} else {
|
|
284
|
-
// Only an edit the editor reported counts: before load lands the editor is hidden
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
const ev = s.bodyPending || !s.bodyDirty ? { kind: "absent" as const } : editorValue(v.body, s.bodyJson);
|
|
272
|
+
// Only an edit the editor reported counts (EditorGate): before load lands the editor is hidden,
|
|
273
|
+
// and an untouched bag holds the engine's re-serialization of the seed.
|
|
274
|
+
const ev = editorSubmitValue(v.body, s.bodyJson, { pending: s.bodyPending, dirty: s.bodyDirty });
|
|
288
275
|
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
276
|
if (ev.kind === "doc") { richContent = ev.doc as unknown as WixRichContent; bodyRaw = ev.json; }
|
|
290
277
|
// A body past the rich-content limits is not written (every viewer would show its
|
|
@@ -308,7 +295,7 @@ const svc = defineService<PostFormState, {
|
|
|
308
295
|
const savedState = (id: string, savedMessage: string) => shape({
|
|
309
296
|
...s, mode: "edit", id, heading: copyParam("ownerPostForm.headingEdit", "Edit post"),
|
|
310
297
|
title, excerpt, body: postBodyText(kept), richContent: kept, bodyJson: bodyEdited ? bodyRaw : s.bodyJson,
|
|
311
|
-
bodyDirty: bodyEdited ? false : s.bodyDirty,
|
|
298
|
+
bodyDirty: bodyEdited ? false : s.bodyDirty,
|
|
312
299
|
coverUrl, originalCoverUrl: coverUrl, isFeatured: isFeatured ? "true" : "false",
|
|
313
300
|
status: "saved", savedMessage, savedId: id,
|
|
314
301
|
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.") : "",
|
|
@@ -366,20 +353,8 @@ const svc = defineService<PostFormState, {
|
|
|
366
353
|
},
|
|
367
354
|
});
|
|
368
355
|
|
|
369
|
-
// [rico] #3371 — adopter contract
|
|
370
|
-
// document
|
|
371
|
-
|
|
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
|
-
}
|
|
356
|
+
// [rico] #3371 — adopter contract: the editor seeds once, so a cold boot must not seed it with the last
|
|
357
|
+
// run's document. Nor does it carry this run's UI over: the details sheet is shut.
|
|
358
|
+
resetHydratedEditor(svc, { json: "bodyJson", pending: "bodyPending", dirty: "bodyDirty" }, startMode === "edit", { detailsOpen: false });
|
|
384
359
|
|
|
385
360
|
export { svc };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
|
|
2
|
-
// this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
|
|
3
|
-
// Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
|
|
4
|
-
// imports "@himalaya/wix-rich-content".
|
|
5
|
-
import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
|
|
6
|
-
|
|
7
|
-
export { htmlIsTrivial };
|
|
8
|
-
|
|
9
|
-
const ENTITIES: Record<string, string> = { " ": " ", "&": "&", "<": "<", ">": ">", """: "\"", "'": "'" };
|
|
10
|
-
|
|
11
|
-
/** Read-only preview text for an HTML body: block closes become lines, tags are stripped (a bare
|
|
12
|
-
* `<` in prose is kept), the basic entities decode, and blank lines drop. */
|
|
13
|
-
export function htmlPreviewText(html: string | null | undefined): string {
|
|
14
|
-
return (html ?? "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|div|blockquote|pre|tr)>/gi, "\n")
|
|
15
|
-
.replace(/<\/?[a-z!?][^>]*>/gi, "").replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m] ?? m)
|
|
16
|
-
.split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** True when a plain text field can round-trip this Ricos document: no document styling, and
|
|
20
|
-
* only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
|
|
21
|
-
* `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
|
|
22
|
-
* a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
|
|
23
|
-
* document (callers decide what an absent body means). */
|
|
24
|
-
export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
|
|
25
|
-
!!doc && isPlainRicos(doc);
|
|
26
|
-
|
|
27
|
-
/** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
|
|
28
|
-
* when the payload is not DraftJS JSON (never a throw). */
|
|
29
|
-
export function draftjsText(raw: string | null | undefined): string {
|
|
30
|
-
try {
|
|
31
|
-
const doc = JSON.parse(raw ?? "") as { blocks?: Array<{ text?: unknown }> } | null;
|
|
32
|
-
const blocks = Array.isArray(doc?.blocks) ? doc.blocks : [];
|
|
33
|
-
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("\n\n").trim();
|
|
34
|
-
} catch { return ""; }
|
|
35
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
|
|
2
|
-
// this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
|
|
3
|
-
// Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
|
|
4
|
-
// imports "@himalaya/wix-rich-content".
|
|
5
|
-
import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
|
|
6
|
-
|
|
7
|
-
export { htmlIsTrivial };
|
|
8
|
-
|
|
9
|
-
const ENTITIES: Record<string, string> = { " ": " ", "&": "&", "<": "<", ">": ">", """: "\"", "'": "'" };
|
|
10
|
-
|
|
11
|
-
/** Read-only preview text for an HTML body: block closes become lines, tags are stripped (a bare
|
|
12
|
-
* `<` in prose is kept), the basic entities decode, and blank lines drop. */
|
|
13
|
-
export function htmlPreviewText(html: string | null | undefined): string {
|
|
14
|
-
return (html ?? "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|div|blockquote|pre|tr)>/gi, "\n")
|
|
15
|
-
.replace(/<\/?[a-z!?][^>]*>/gi, "").replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m] ?? m)
|
|
16
|
-
.split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** True when a plain text field can round-trip this Ricos document: no document styling, and
|
|
20
|
-
* only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
|
|
21
|
-
* `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
|
|
22
|
-
* a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
|
|
23
|
-
* document (callers decide what an absent body means). */
|
|
24
|
-
export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
|
|
25
|
-
!!doc && isPlainRicos(doc);
|
|
26
|
-
|
|
27
|
-
/** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
|
|
28
|
-
* when the payload is not DraftJS JSON (never a throw). */
|
|
29
|
-
export function draftjsText(raw: string | null | undefined): string {
|
|
30
|
-
try {
|
|
31
|
-
const doc = JSON.parse(raw ?? "") as { blocks?: Array<{ text?: unknown }> } | null;
|
|
32
|
-
const blocks = Array.isArray(doc?.blocks) ? doc.blocks : [];
|
|
33
|
-
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("\n\n").trim();
|
|
34
|
-
} catch { return ""; }
|
|
35
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// May a plain owner form round-trip this body? The Ricos and HTML checks are @himalaya/wix-rich-content's;
|
|
2
|
-
// this file adds the read-only previews the forms show for a locked body, and the FAQ's legacy DraftJS answer.
|
|
3
|
-
// Inside the platform source this import is a relative path, so node test runs resolve it; an app's copy
|
|
4
|
-
// imports "@himalaya/wix-rich-content".
|
|
5
|
-
import { htmlIsTrivial, isPlainRicos } from "@himalaya/wix-rich-content";
|
|
6
|
-
|
|
7
|
-
export { htmlIsTrivial };
|
|
8
|
-
|
|
9
|
-
const ENTITIES: Record<string, string> = { " ": " ", "&": "&", "<": "<", ">": ">", """: "\"", "'": "'" };
|
|
10
|
-
|
|
11
|
-
/** Read-only preview text for an HTML body: block closes become lines, tags are stripped (a bare
|
|
12
|
-
* `<` in prose is kept), the basic entities decode, and blank lines drop. */
|
|
13
|
-
export function htmlPreviewText(html: string | null | undefined): string {
|
|
14
|
-
return (html ?? "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|div|blockquote|pre|tr)>/gi, "\n")
|
|
15
|
-
.replace(/<\/?[a-z!?][^>]*>/gi, "").replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m] ?? m)
|
|
16
|
-
.split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** True when a plain text field can round-trip this Ricos document: no document styling, and
|
|
20
|
-
* only paragraphs of undecorated text whose paragraph data is the wire default (Events v3 sends
|
|
21
|
-
* `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). Anything else —
|
|
22
|
-
* a heading, list, link, bold run, media node, alignment or indent — is rich, and so is a missing
|
|
23
|
-
* document (callers decide what an absent body means). */
|
|
24
|
-
export const ricosIsPlain = (doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean =>
|
|
25
|
-
!!doc && isPlainRicos(doc);
|
|
26
|
-
|
|
27
|
-
/** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
|
|
28
|
-
* when the payload is not DraftJS JSON (never a throw). */
|
|
29
|
-
export function draftjsText(raw: string | null | undefined): string {
|
|
30
|
-
try {
|
|
31
|
-
const doc = JSON.parse(raw ?? "") as { blocks?: Array<{ text?: unknown }> } | null;
|
|
32
|
-
const blocks = Array.isArray(doc?.blocks) ? doc.blocks : [];
|
|
33
|
-
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("\n\n").trim();
|
|
34
|
-
} catch { return ""; }
|
|
35
|
-
}
|