@wix/himalaya-cli 0.1173.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 +37 -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
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
} from "@himalaya/wix-stores";
|
|
18
18
|
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
19
19
|
import { htmlIsTrivial, htmlPreviewText } from "../_shared/owner-rich-guard.js";
|
|
20
|
+
import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
|
|
21
|
+
import { editorSubmitValue, queuedDocument, editorChanged, cachedRichVerdict, resetHydratedEditor } from "../_shared/owner-editor-value.js";
|
|
22
|
+
import { parseRicosJson } from "@himalaya/wix-rich-content";
|
|
23
|
+
import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
|
|
20
24
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
21
25
|
import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch, editablePrice } from "../_shared/owner-preview-demo.js";
|
|
22
26
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
@@ -45,12 +49,27 @@ interface ProductFormState {
|
|
|
45
49
|
name: string;
|
|
46
50
|
price: string;
|
|
47
51
|
sku: string;
|
|
52
|
+
/** The description as text: the plain field's value, the locked preview, and the cached row's mirror. */
|
|
48
53
|
description: string;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
54
|
+
/** The description as loaded (or last saved). The plain field sends `description` only when the
|
|
55
|
+
* submitted value differs from it, so an untouched body is never re-written. */
|
|
51
56
|
originalDescription: string;
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
57
|
+
/** How THIS record's description is edited:
|
|
58
|
+
* "editor" — the RichContentEditor, seeded with `descriptionJson`: a Catalog-V3 product (its Ricos
|
|
59
|
+
* document, or trivial HTML as paragraphs), and sample data;
|
|
60
|
+
* "plain" — the plain text field: a new product, a V1 product with trivial HTML;
|
|
61
|
+
* "locked" — neither: V1 rich HTML, or a V3 product whose Ricos document did not come back. */
|
|
62
|
+
descriptionMode: "editor" | "plain" | "locked";
|
|
63
|
+
/** The V3 Ricos description as a JSON string: the editor's seed ("" when none, and until this run's
|
|
64
|
+
* load lands — see the boot reset below). Re-based to the saved document after a save. */
|
|
65
|
+
descriptionJson: string;
|
|
66
|
+
descriptionFieldHidden: boolean; // the plain text field (bag key `descriptionText`)
|
|
67
|
+
descriptionEditorHidden: boolean; // the RichContentEditor (bag key `description`)
|
|
68
|
+
/** The editor gate (`EditorGate` in _shared/owner-editor-value.ts): edited since load/save, and
|
|
69
|
+
* hidden until this run's load lands. */
|
|
70
|
+
descriptionDirty: boolean;
|
|
71
|
+
descriptionPending: boolean;
|
|
72
|
+
/** Derived from the mode: "locked" shows the preview + `descriptionNote`, and refuses an edit. */
|
|
54
73
|
descriptionProtected: boolean;
|
|
55
74
|
descriptionEditable: boolean;
|
|
56
75
|
descriptionNote: string;
|
|
@@ -111,10 +130,15 @@ function priceOf(p: WixProduct): string {
|
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
|
|
133
|
+
/** What every load that publishes a description resets: the editor may show it, nothing is edited yet. */
|
|
134
|
+
const DESCRIPTION_LOADED = { descriptionPending: false, descriptionDirty: false } as const;
|
|
135
|
+
const docJson = (doc: unknown): string => (doc ? JSON.stringify(doc) : "");
|
|
136
|
+
|
|
114
137
|
function shape(s: Partial<ProductFormState>): ProductFormState {
|
|
115
138
|
const mode: Mode = s.mode ?? "create";
|
|
116
139
|
const id = typeof s.id === "string" ? s.id : "";
|
|
117
140
|
const savedId = typeof s.savedId === "string" ? s.savedId : id;
|
|
141
|
+
const dMode = s.descriptionMode ?? (mode === "edit" ? "editor" : "plain");
|
|
118
142
|
return {
|
|
119
143
|
mode,
|
|
120
144
|
id,
|
|
@@ -124,9 +148,15 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
|
|
|
124
148
|
sku: s.sku ?? "",
|
|
125
149
|
description: s.description ?? "",
|
|
126
150
|
originalDescription: s.originalDescription ?? s.description ?? "",
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
descriptionMode: dMode,
|
|
152
|
+
descriptionJson: s.descriptionJson ?? "",
|
|
153
|
+
descriptionFieldHidden: dMode !== "plain",
|
|
154
|
+
descriptionEditorHidden: dMode !== "editor",
|
|
155
|
+
descriptionDirty: s.descriptionDirty ?? false,
|
|
156
|
+
descriptionPending: s.descriptionPending ?? false,
|
|
157
|
+
descriptionProtected: dMode === "locked",
|
|
158
|
+
descriptionEditable: dMode !== "locked",
|
|
159
|
+
descriptionNote: dMode === "locked" ? copyParam("ownerProductForm.richDescriptionNote", "This description has formatting or media. Edit it in your site dashboard to keep that content intact; name, price and SKU can be changed here.") : "",
|
|
130
160
|
visible: s.visible ?? "true",
|
|
131
161
|
status: s.status ?? "idle",
|
|
132
162
|
error: s.error ?? "",
|
|
@@ -151,12 +181,19 @@ function demoRefusal(s: Parameters<typeof shape>[0]): ReturnType<typeof shape> {
|
|
|
151
181
|
return shape({ ...s, ...demoRefusalPatch(copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE)) });
|
|
152
182
|
}
|
|
153
183
|
|
|
184
|
+
// The RichContentEditor's image tool (the editor's upload contract). The component picks and PUTs
|
|
185
|
+
// the bytes; this service mints the signed Wix Media URL with the worker's own wixApi, so no token
|
|
186
|
+
// reaches the page. Registered BEFORE the form's service on purpose: the worker test harnesses treat a
|
|
187
|
+
// bundle's LAST defineService as its service.
|
|
188
|
+
ricosUploadService("himiOwnerLiteProductFormUploads");
|
|
189
|
+
|
|
154
190
|
const svc = defineService<ProductFormState, {
|
|
155
191
|
load: (s: ProductFormState, arg?: unknown) => Promise<ProductFormState>;
|
|
192
|
+
descriptionChanged: (s: ProductFormState, arg?: unknown) => ProductFormState;
|
|
156
193
|
submit: (s: ProductFormState, values?: Record<string, unknown>) => Promise<ProductFormState>;
|
|
157
194
|
remove: (s: ProductFormState) => Promise<ProductFormState>;
|
|
158
195
|
}>("himiOwnerLiteProductForm", {
|
|
159
|
-
state: shape({ mode: startMode, id: initialInput.id ?? "" }),
|
|
196
|
+
state: shape({ mode: startMode, id: initialInput.id ?? "", descriptionPending: startMode === "edit" }),
|
|
160
197
|
actions: {
|
|
161
198
|
load: async (s, arg) => {
|
|
162
199
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
@@ -164,13 +201,14 @@ const svc = defineService<ProductFormState, {
|
|
|
164
201
|
// bakes no id/mode); the onAppear `id` param is the #570 suffixed-build
|
|
165
202
|
// fallback. An id ⇒ edit; no id anywhere ⇒ the create shell.
|
|
166
203
|
const id = navInputId() || idFrom(arg, s.id);
|
|
167
|
-
if (!id) return shape({ ...s, mode: "create", id: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
|
|
204
|
+
if (!id) return shape({ ...s, ...DESCRIPTION_LOADED, mode: "create", id: "", descriptionMode: "plain", descriptionJson: "", status: "idle", error: "", demoNote: isOwnerPreviewDemo() ? copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) : "" });
|
|
168
205
|
if (isOwnerPreviewDemo()) {
|
|
169
206
|
const d = DEMO_PRODUCTS.find((p) => p.id === id);
|
|
170
207
|
if (d) {
|
|
171
208
|
return shape({
|
|
172
209
|
...s, mode: "edit", id, heading: copyParam("ownerProductForm.headingEdit", "Edit product"),
|
|
173
210
|
name: d.name, price: editablePrice(d.price), sku: d.sku, description: d.description,
|
|
211
|
+
...DESCRIPTION_LOADED, descriptionMode: "editor", descriptionJson: docJson(plainPostBody(d.description)),
|
|
174
212
|
visible: d.visible ? "true" : "false",
|
|
175
213
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
176
214
|
});
|
|
@@ -179,7 +217,9 @@ const svc = defineService<ProductFormState, {
|
|
|
179
217
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
180
218
|
}
|
|
181
219
|
try {
|
|
182
|
-
|
|
220
|
+
// `richDescription` (Catalog V3): the Ricos description beside the HTML one. An SDK without the
|
|
221
|
+
// option ignores it, and the key is absent: the form then keeps the plain/locked split.
|
|
222
|
+
const p = (await (getProduct as (id: string, opts?: { richDescription?: boolean }) => ReturnType<typeof getProduct>)(id, { richDescription: true })).product as WixProduct;
|
|
183
223
|
if (!p || !(p as any).id) return shape({ ...s, mode: "edit", id, status: "error", error: copyParam("ownerProductForm.errorNotFound", "Product not found") });
|
|
184
224
|
return shape({
|
|
185
225
|
...s,
|
|
@@ -189,7 +229,19 @@ const svc = defineService<ProductFormState, {
|
|
|
189
229
|
name: (p as any).name ?? "",
|
|
190
230
|
price: priceOf(p),
|
|
191
231
|
sku: (p as any).sku ?? "",
|
|
192
|
-
...((html: string
|
|
232
|
+
...((html: string, rich: unknown) => {
|
|
233
|
+
// `richDescription` is present only on a Catalog V3 site. V3 is edited in the RichContentEditor;
|
|
234
|
+
// a V3 product whose Ricos document did not come back is edited only when its HTML is trivial
|
|
235
|
+
// (seeded as paragraphs), else locked — fail closed. V1 keeps the plain/locked split: the
|
|
236
|
+
// editor writes Ricos, and a V1 description is HTML.
|
|
237
|
+
const doc = rich === undefined || rich === null ? null : parseRicosJson(rich);
|
|
238
|
+
const hasDoc = !!doc && doc.nodes.length > 0;
|
|
239
|
+
const text = htmlPreviewText(html);
|
|
240
|
+
const descriptionMode = rich === undefined ? (htmlIsTrivial(html) ? "plain" as const : "locked" as const)
|
|
241
|
+
: hasDoc || htmlIsTrivial(html) ? "editor" as const : "locked" as const;
|
|
242
|
+
const descriptionJson = descriptionMode !== "editor" ? "" : hasDoc ? JSON.stringify(rich) : text ? docJson(plainPostBody(text)) : "";
|
|
243
|
+
return { ...DESCRIPTION_LOADED, description: text, originalDescription: text, descriptionMode, descriptionJson };
|
|
244
|
+
})(String((p as any).description ?? ""), (p as { richDescription?: unknown }).richDescription),
|
|
193
245
|
visible: (p as any).visible !== false ? "true" : "false",
|
|
194
246
|
status: "idle",
|
|
195
247
|
error: "",
|
|
@@ -202,8 +254,11 @@ const svc = defineService<ProductFormState, {
|
|
|
202
254
|
return shape({
|
|
203
255
|
...s, mode: "edit", id, heading: "Edit product",
|
|
204
256
|
name: String(cached.name ?? ""), price: editablePrice(String(cached.price ?? "")), sku: String(cached.sku ?? ""),
|
|
205
|
-
|
|
206
|
-
|
|
257
|
+
description: String(cached.description ?? ""), originalDescription: String(cached.description ?? ""), ...DESCRIPTION_LOADED,
|
|
258
|
+
// A row cached with its document reopens in the editor. An older row has none, so its verdict
|
|
259
|
+
// decides: plain when it was plain, else locked (it carries no verdict either: fail closed).
|
|
260
|
+
...(cached.descriptionJson ? { descriptionMode: "editor" as const, descriptionJson: String(cached.descriptionJson) }
|
|
261
|
+
: { descriptionMode: cached.descriptionProtected === "false" ? "plain" as const : "locked" as const, descriptionJson: "" }),
|
|
207
262
|
visible: cached.visible === "false" ? "false" : "true",
|
|
208
263
|
status: "idle", error: "", savedMessage: "",
|
|
209
264
|
});
|
|
@@ -214,6 +269,7 @@ const svc = defineService<ProductFormState, {
|
|
|
214
269
|
return shape({
|
|
215
270
|
...s, mode: "edit", id, heading: copyParam("ownerProductForm.headingEdit", "Edit product"),
|
|
216
271
|
name: d.name, price: editablePrice(d.price), sku: d.sku, description: d.description,
|
|
272
|
+
...DESCRIPTION_LOADED, descriptionMode: "editor", descriptionJson: docJson(plainPostBody(d.description)),
|
|
217
273
|
visible: d.visible ? "true" : "false",
|
|
218
274
|
status: "idle", error: "", savedMessage: "", demoNote: copyParam("ownerCommon.demoNote", DEMO_NOTE),
|
|
219
275
|
});
|
|
@@ -222,6 +278,8 @@ const svc = defineService<ProductFormState, {
|
|
|
222
278
|
return shape({ ...s, mode: "edit", id, status: "error", error: ownerLoadError(e) });
|
|
223
279
|
}
|
|
224
280
|
},
|
|
281
|
+
// The editor's on_change carries `args.dirty` only; the document stays in the form bag.
|
|
282
|
+
descriptionChanged: (s) => shape({ ...s, descriptionDirty: true, ...editorChanged(s) }),
|
|
225
283
|
submit: async (s, values) => {
|
|
226
284
|
if (isOwnerPreviewDemo() || s.demoNote) return demoRefusal(s);
|
|
227
285
|
const v = values ?? {};
|
|
@@ -230,10 +288,34 @@ const svc = defineService<ProductFormState, {
|
|
|
230
288
|
// "12,50" (comma-decimal keypad) is accepted and written as 12.50.
|
|
231
289
|
const price = editablePrice(rawPrice);
|
|
232
290
|
const sku = String(v.sku ?? s.sku).trim();
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
const
|
|
236
|
-
|
|
291
|
+
// The description, per record mode. A queue replay (a FRESH worker) says so with
|
|
292
|
+
// `descriptionEdited`; this bundle's V3 editor items add the document as `descriptionJson`.
|
|
293
|
+
const replay = typeof v.descriptionEdited === "string";
|
|
294
|
+
let richDescription: { nodes: unknown[] } | null = null; // the V3 Ricos description to WRITE
|
|
295
|
+
let docRaw = "";
|
|
296
|
+
let tooLarge = false;
|
|
297
|
+
let description = s.description; // the plain text (V1 / create), or the mirror
|
|
298
|
+
let descriptionEdited = false; // write the PLAIN description (V1 / create)
|
|
299
|
+
if (replay) {
|
|
300
|
+
const queued = queuedDocument(v.descriptionJson);
|
|
301
|
+
// A queued document that no longer reads (or is past the limits) writes NOTHING, and says so.
|
|
302
|
+
if (queued.kind === "invalid" || queued.kind === "too_large") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerProductForm.queuedDescriptionError", "The description saved offline could not be read, so nothing was written. Edit it again.") });
|
|
303
|
+
if (queued.kind === "doc") { richDescription = queued.doc as { nodes: unknown[] }; docRaw = queued.json; }
|
|
304
|
+
else if (v.descriptionEdited === "true") { description = String(v.description ?? "").trim(); descriptionEdited = true; }
|
|
305
|
+
} else if (s.descriptionMode === "editor") {
|
|
306
|
+
// Only an edit the editor reported counts (EditorGate). Absent (the editor never became ready):
|
|
307
|
+
// save the rest and leave the description as it is.
|
|
308
|
+
const ev = editorSubmitValue(v.description, s.descriptionJson, { pending: s.descriptionPending, dirty: s.descriptionDirty });
|
|
309
|
+
if (ev.kind === "invalid") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerProductForm.editorReadError", "Couldn’t read the description from the editor. Nothing was saved; try again.") });
|
|
310
|
+
if (ev.kind === "doc") { richDescription = ev.doc as { nodes: unknown[] }; docRaw = ev.json; }
|
|
311
|
+
tooLarge = ev.kind === "too_large";
|
|
312
|
+
} else {
|
|
313
|
+
// Plain or locked: the plain text field, under its own bag key.
|
|
314
|
+
description = String(v.descriptionText ?? s.description).trim();
|
|
315
|
+
descriptionEdited = description !== s.originalDescription.trim();
|
|
316
|
+
if (descriptionEdited && s.descriptionMode === "locked") return shape({ ...s, status: "error", savedMessage: "", error: copyParam("ownerProductForm.richDescriptionError", "This description has formatting or media. Edit it in your site dashboard to keep that content intact.") });
|
|
317
|
+
}
|
|
318
|
+
if (richDescription) description = postBodyText(richDescription as never);
|
|
237
319
|
const visible = boolString(v.visible, s.visible) === "true";
|
|
238
320
|
if (!name) return shape({ ...s, status: "error", error: copyParam("ownerProductForm.nameRequired", "Name is required"), savedMessage: "" });
|
|
239
321
|
const priceNum = Number(price);
|
|
@@ -244,23 +326,41 @@ const svc = defineService<ProductFormState, {
|
|
|
244
326
|
return shape({ ...s, name, price, sku, description, visible: visible ? "true" : "false", status: "error", error: copyParam("ownerProductForm.priceInvalid", "Enter a price of 0 or more."), savedMessage: "" });
|
|
245
327
|
}
|
|
246
328
|
|
|
247
|
-
|
|
329
|
+
// A local structural type for `richDescription`: templates compile against the PUBLISHED SDK.
|
|
330
|
+
const write: WixProductWrite & { richDescription?: { nodes: unknown[] } } = {
|
|
248
331
|
name,
|
|
249
332
|
priceData: { price: priceNum, currency: CURRENCY },
|
|
250
333
|
visible,
|
|
251
334
|
...(sku ? { sku } : {}),
|
|
252
335
|
...(descriptionEdited ? { description } : {}),
|
|
336
|
+
...(richDescription ? { richDescription } : {}),
|
|
253
337
|
...(s.mode === "edit" && s.id ? {} : { productType: "physical" }),
|
|
254
338
|
};
|
|
339
|
+
// The document the record now holds, when known: an offline reopen seeds the editor with it.
|
|
340
|
+
const loadedJson = s.descriptionMode === "editor" && !s.descriptionPending ? s.descriptionJson : "";
|
|
341
|
+
const heldJson = docRaw || loadedJson;
|
|
342
|
+
// The cache's verdict comes from the CONTENT (a rolled-back bundle reads it to decide whether its
|
|
343
|
+
// plain field may overwrite the body): from the document when known; plain only when the record
|
|
344
|
+
// loaded as plain, or a new product's plain text. Before load lands it stays protected.
|
|
345
|
+
const heldDoc = richDescription ?? (loadedJson ? parseRicosJson(loadedJson) : null);
|
|
346
|
+
const knownPlain = s.descriptionMode === "plain" || (s.descriptionMode === "editor" && !s.descriptionPending && s.descriptionJson === "");
|
|
347
|
+
const descriptionProtected = cachedRichVerdict(heldDoc as never, knownPlain);
|
|
255
348
|
|
|
256
349
|
// Optimistic local write — the list repaints from cache immediately (offline or not).
|
|
257
|
-
const localRow = (id: string): DbEntity => ({ id, name, price, sku, description, descriptionProtected
|
|
350
|
+
const localRow = (id: string): DbEntity => ({ id, name, price, sku, description, descriptionProtected, ...(heldJson ? { descriptionJson: heldJson } : {}), visible: visible ? "true" : "false", statusLabel: visible ? "Visible" : "Hidden" });
|
|
351
|
+
const wrote = richDescription !== null;
|
|
352
|
+
const savedDescription = {
|
|
353
|
+
description, originalDescription: descriptionEdited || wrote ? description : s.originalDescription,
|
|
354
|
+
...(docRaw ? { descriptionJson: docRaw } : {}),
|
|
355
|
+
descriptionDirty: wrote ? false : s.descriptionDirty,
|
|
356
|
+
error: tooLarge ? copyParam("ownerProductForm.descriptionTooLarge", "The description is too long to save, so it was left as it was. Your other changes were saved.") : "",
|
|
357
|
+
};
|
|
258
358
|
const localId = s.mode === "edit" && s.id ? s.id : `local-${Date.now()}`;
|
|
259
359
|
await cachePut(localRow(localId));
|
|
260
360
|
try {
|
|
261
361
|
let id = s.id;
|
|
262
362
|
if (s.mode === "edit" && s.id) {
|
|
263
|
-
await updateProduct(s.id, write);
|
|
363
|
+
await updateProduct(s.id, write as WixProductWrite);
|
|
264
364
|
} else {
|
|
265
365
|
const r = await createProduct(write);
|
|
266
366
|
id = r.product?.id || localId;
|
|
@@ -275,11 +375,9 @@ const svc = defineService<ProductFormState, {
|
|
|
275
375
|
name,
|
|
276
376
|
price,
|
|
277
377
|
sku,
|
|
278
|
-
description,
|
|
279
|
-
originalDescription: descriptionEdited ? description : s.originalDescription,
|
|
280
378
|
visible: visible ? "true" : "false",
|
|
281
379
|
status: "saved",
|
|
282
|
-
|
|
380
|
+
...savedDescription,
|
|
283
381
|
savedMessage: copyParam("ownerProductForm.savedMessage", "Product saved."),
|
|
284
382
|
savedId: id,
|
|
285
383
|
});
|
|
@@ -291,11 +389,13 @@ const svc = defineService<ProductFormState, {
|
|
|
291
389
|
if (s.demoNote) return demoRefusal(s);
|
|
292
390
|
// Real device offline: the optimistic cache row already shows; queue the
|
|
293
391
|
// write to replay (WriteQueue, idempotent) when connectivity returns.
|
|
294
|
-
|
|
392
|
+
// A document rides as `descriptionJson` with descriptionEdited "false", so a bundle rolled back
|
|
393
|
+
// past this change leaves the description rather than flattening it to text.
|
|
394
|
+
await queueWrite("tier5.himiOwnerLiteProductForm.submit", { id: s.id, name, price, sku, description, visible: String(visible), mode: s.mode,
|
|
395
|
+
...(docRaw ? { descriptionJson: docRaw, descriptionEdited: "false" } : { descriptionEdited: String(descriptionEdited) }) });
|
|
295
396
|
return shape({
|
|
296
|
-
...s, mode: "edit", id: localId, name, price, sku,
|
|
297
|
-
|
|
298
|
-
visible: visible ? "true" : "false", status: "saved", error: "",
|
|
397
|
+
...s, mode: "edit", id: localId, name, price, sku, ...savedDescription,
|
|
398
|
+
visible: visible ? "true" : "false", status: "saved",
|
|
299
399
|
savedMessage: copyParam("ownerProductForm.savedOffline", "Saved offline — will sync when you reconnect."),
|
|
300
400
|
savedId: localId,
|
|
301
401
|
});
|
|
@@ -323,4 +423,7 @@ const svc = defineService<ProductFormState, {
|
|
|
323
423
|
},
|
|
324
424
|
});
|
|
325
425
|
|
|
426
|
+
// Adopter contract: the editor seeds once, so a cold boot must not seed it with the last run's document.
|
|
427
|
+
resetHydratedEditor(svc, { json: "descriptionJson", pending: "descriptionPending", dirty: "descriptionDirty" }, startMode === "edit");
|
|
428
|
+
|
|
326
429
|
export { svc };
|
|
@@ -67444,7 +67444,7 @@ const SCREENS = runtimeSetting({
|
|
|
67444
67444
|
"onChange": {
|
|
67445
67445
|
"actionId": "tier5.himiOwnerLitePostForm.bodyChanged"
|
|
67446
67446
|
},
|
|
67447
|
-
"minHeight":
|
|
67447
|
+
"minHeight": 240,
|
|
67448
67448
|
"fallback": {
|
|
67449
67449
|
"id": "post_form.body_unavailable",
|
|
67450
67450
|
"body": {
|
|
@@ -67860,7 +67860,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
67860
67860
|
"package": "himalaya",
|
|
67861
67861
|
"type": "FormField",
|
|
67862
67862
|
"props": {
|
|
67863
|
-
"id": "
|
|
67863
|
+
"id": "descriptionText",
|
|
67864
67864
|
"label": "Description",
|
|
67865
67865
|
"kind": "text",
|
|
67866
67866
|
"text": {
|
|
@@ -67873,7 +67873,90 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
67873
67873
|
}
|
|
67874
67874
|
},
|
|
67875
67875
|
"modifiers": {
|
|
67876
|
-
"hiddenPath": "$service:himiOwnerLiteEventForm.
|
|
67876
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionFieldHidden"
|
|
67877
|
+
}
|
|
67878
|
+
},
|
|
67879
|
+
{
|
|
67880
|
+
"id": "event_form.description_slot",
|
|
67881
|
+
"body": {
|
|
67882
|
+
"package": "himalaya",
|
|
67883
|
+
"type": "VStack",
|
|
67884
|
+
"props": {
|
|
67885
|
+
"spacing": 6,
|
|
67886
|
+
"alignment": "leading",
|
|
67887
|
+
"children": [
|
|
67888
|
+
{
|
|
67889
|
+
"id": "",
|
|
67890
|
+
"body": {
|
|
67891
|
+
"package": "himalaya",
|
|
67892
|
+
"type": "Text",
|
|
67893
|
+
"props": {
|
|
67894
|
+
"text": "Description",
|
|
67895
|
+
"style": "bodyEmphasis"
|
|
67896
|
+
}
|
|
67897
|
+
}
|
|
67898
|
+
},
|
|
67899
|
+
{
|
|
67900
|
+
"id": "event_description_editor",
|
|
67901
|
+
"body": {
|
|
67902
|
+
"package": "himalaya",
|
|
67903
|
+
"type": "RichContentEditor",
|
|
67904
|
+
"props": {
|
|
67905
|
+
"name": "description",
|
|
67906
|
+
"documentPath": "$service:himiOwnerLiteEventForm.descriptionJson",
|
|
67907
|
+
"placeholder": "What attendees should expect",
|
|
67908
|
+
"uploadService": "himiOwnerLiteEventFormUploads",
|
|
67909
|
+
"onChange": {
|
|
67910
|
+
"actionId": "tier5.himiOwnerLiteEventForm.descriptionChanged"
|
|
67911
|
+
},
|
|
67912
|
+
"minHeight": 200,
|
|
67913
|
+
"fallback": {
|
|
67914
|
+
"id": "event_form.description_unavailable",
|
|
67915
|
+
"body": {
|
|
67916
|
+
"package": "himalaya",
|
|
67917
|
+
"type": "VStack",
|
|
67918
|
+
"props": {
|
|
67919
|
+
"spacing": 4,
|
|
67920
|
+
"alignment": "leading",
|
|
67921
|
+
"children": [
|
|
67922
|
+
{
|
|
67923
|
+
"id": "",
|
|
67924
|
+
"body": {
|
|
67925
|
+
"package": "himalaya",
|
|
67926
|
+
"type": "Text",
|
|
67927
|
+
"props": {
|
|
67928
|
+
"text": "Editor unavailable",
|
|
67929
|
+
"style": "title3"
|
|
67930
|
+
}
|
|
67931
|
+
}
|
|
67932
|
+
},
|
|
67933
|
+
{
|
|
67934
|
+
"id": "",
|
|
67935
|
+
"body": {
|
|
67936
|
+
"package": "himalaya",
|
|
67937
|
+
"type": "Text",
|
|
67938
|
+
"props": {
|
|
67939
|
+
"text": "The description can’t be edited here right now. The title still saves, and the description stays as it is.",
|
|
67940
|
+
"style": "footnote",
|
|
67941
|
+
"color": "textSecondary"
|
|
67942
|
+
}
|
|
67943
|
+
}
|
|
67944
|
+
}
|
|
67945
|
+
]
|
|
67946
|
+
}
|
|
67947
|
+
}
|
|
67948
|
+
}
|
|
67949
|
+
}
|
|
67950
|
+
},
|
|
67951
|
+
"modifiers": {
|
|
67952
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionPending"
|
|
67953
|
+
}
|
|
67954
|
+
}
|
|
67955
|
+
]
|
|
67956
|
+
}
|
|
67957
|
+
},
|
|
67958
|
+
"modifiers": {
|
|
67959
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionEditorHidden"
|
|
67877
67960
|
}
|
|
67878
67961
|
}
|
|
67879
67962
|
],
|
|
@@ -68147,7 +68230,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
68147
68230
|
"onChange": {
|
|
68148
68231
|
"actionId": "tier5.himiOwnerLitePostForm.bodyChanged"
|
|
68149
68232
|
},
|
|
68150
|
-
"minHeight":
|
|
68233
|
+
"minHeight": 240,
|
|
68151
68234
|
"fallback": {
|
|
68152
68235
|
"id": "post_form.body_unavailable",
|
|
68153
68236
|
"body": {
|
|
@@ -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
|
+
}
|