@wix/himalaya-cli 0.1070.0 → 0.1072.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/events/src/index.d.ts +21 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +4 -2
- package/guides/sdk-reference.json +17 -5
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +64 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/community-club-owner/dev/index.ts +96 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/events-meetups-owner/dev/index.ts +32 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/gym-fitness-owner/dev/index.ts +96 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/nonprofit-cause-owner/dev/index.ts +32 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/owner-lite/dev/index.ts +192 -0
- package/templates/owner-lite/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq-question-form/index.ts +18 -5
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/podcast-creator-owner/dev/index.ts +96 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/portfolio-creative-owner/dev/index.ts +64 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/restaurant-owner/dev/index.ts +96 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/retail-store-owner/dev/index.ts +64 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +24 -5
- package/templates/yoga-studio-owner/dev/index.ts +32 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +64 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +44 -10
|
@@ -51,7 +51,9 @@ export interface WixEvent {
|
|
|
51
51
|
mainImage?: WixEventImage;
|
|
52
52
|
slug?: string;
|
|
53
53
|
about?: string;
|
|
54
|
-
|
|
54
|
+
/** Events v3: a Ricos document ({@link WixEventRichContent}), returned only when the `TEXTS`
|
|
55
|
+
* fieldset is requested (`getEvent(id, { fields: ["TEXTS"] })`). Older payloads sent a string. */
|
|
56
|
+
description?: string | WixEventRichContent;
|
|
55
57
|
}
|
|
56
58
|
export interface WixGetEventListResponse {
|
|
57
59
|
events: WixEvent[];
|
|
@@ -132,12 +134,26 @@ export declare function decodeEvent(raw: WixEvent): WixEvent;
|
|
|
132
134
|
* not just an owner session. (NOT the owner-only GET /events-in-clubs/chunked-events,
|
|
133
135
|
* which returns an HTML error page for a visitor token.) */
|
|
134
136
|
export declare function queryEvents(opts?: WixQueryEventsOptions): Promise<WixGetEventListResponse>;
|
|
137
|
+
/** Events v3 response fieldsets (`RequestedFields`). A bare read returns the base fieldset;
|
|
138
|
+
* each value adds fields: `DETAILS` → `shortDescription`, `mainImage`, `calendarUrls`;
|
|
139
|
+
* `TEXTS` → the Ricos `description`; `REGISTRATION` → `registration`; `URLS` → `eventPageUrl`. */
|
|
140
|
+
export type WixEventFieldset = "DETAILS" | "TEXTS" | "REGISTRATION" | "URLS";
|
|
141
|
+
/** A Ricos document (Events v3 `description`). Opaque here: read it with a Ricos walker, and
|
|
142
|
+
* write it back whole — a PATCH replaces the entire description. */
|
|
143
|
+
export interface WixEventRichContent {
|
|
144
|
+
nodes?: unknown[];
|
|
145
|
+
metadata?: unknown;
|
|
146
|
+
documentStyle?: unknown;
|
|
147
|
+
}
|
|
135
148
|
/** Get one event — GET /events/v3/events/{eventId}. Response wraps as `{ event }`.
|
|
136
149
|
* NOTE: this GET returns the Events v3 BASE fieldset — slimmer than `queryEvents`,
|
|
137
150
|
* which returns the richer default (fields such as `registration` and `calendarUrls`
|
|
138
151
|
* are absent from a bare `getEvent`). If a detail screen needs those fields for one
|
|
139
|
-
* event, prefer `queryEvents` and pick the row by id over `getEvent`.
|
|
140
|
-
|
|
152
|
+
* event, prefer `queryEvents` and pick the row by id over `getEvent`.
|
|
153
|
+
* `opts.fields` requests extra v3 fieldsets; `["TEXTS"]` returns `description`. */
|
|
154
|
+
export declare function getEvent(eventId: string, opts?: {
|
|
155
|
+
fields?: WixEventFieldset[];
|
|
156
|
+
}): Promise<WixGetEventResponse>;
|
|
141
157
|
/** Delete an event — DELETE /events/v3/events/{eventId}. */
|
|
142
158
|
export declare function deleteEvent(eventId: string): Promise<WixDeleteEventResponse>;
|
|
143
159
|
/** Write shape for create/update (Events v3). Verified-minimal: a create needs
|
|
@@ -160,6 +176,8 @@ export interface WixEventWrite {
|
|
|
160
176
|
};
|
|
161
177
|
mainImage?: WixEventImage;
|
|
162
178
|
about?: string;
|
|
179
|
+
/** Ricos document; replaces the whole description. */
|
|
180
|
+
description?: WixEventRichContent;
|
|
163
181
|
}
|
|
164
182
|
/** Create an event — POST /events/v3/events → { event }. */
|
|
165
183
|
export declare function createEvent(event: WixEventWrite): Promise<WixGetEventResponse>;
|
|
@@ -301,9 +301,11 @@ async function queryEvents(opts = {}) {
|
|
|
301
301
|
if (opts.order === "start") events = events.slice().sort((a, b) => eventStartMs(a) - eventStartMs(b));
|
|
302
302
|
return { ...res, events };
|
|
303
303
|
}
|
|
304
|
-
async function getEvent(eventId) {
|
|
304
|
+
async function getEvent(eventId, opts = {}) {
|
|
305
305
|
if (!eventId) throw new Error("getEvent: eventId is required");
|
|
306
|
-
const
|
|
306
|
+
const fields = (opts.fields ?? []).map((f) => `fields=${encodeURIComponent(f)}`).join("&");
|
|
307
|
+
const query = fields ? `?${fields}` : "";
|
|
308
|
+
const res = await wixCall("GET", `/events/v3/events/${encodeURIComponent(eventId)}${query}`);
|
|
307
309
|
return { ...res, event: decodeEvent(res.event) };
|
|
308
310
|
}
|
|
309
311
|
function deleteEvent(eventId) {
|
|
@@ -14370,7 +14370,7 @@
|
|
|
14370
14370
|
"name": "WixEvent",
|
|
14371
14371
|
"kind": "interface",
|
|
14372
14372
|
"jsdoc": "",
|
|
14373
|
-
"decl": "export interface WixEvent {\n id: string;\n title?: string;\n /** Normally one of `WixEventStatus` — `UPCOMING` (published and scheduled to start),\n * `STARTED`, `ENDED`, `CANCELED`, `DRAFT`. Events v1 called the first of those `SCHEDULED`,\n * and this SDK reads v3, so a row here never carries `SCHEDULED`.\n *\n * Deliberately NOT narrowed to that union: `decodeEvent` fills this from the server's\n * `status` and passes a value it does not recognise straight through, because dropping or\n * rewriting one would lose a real event. A union here would be a claim this SDK cannot\n * enforce. Compare against `WixEventStatus` when you need the set it knows. */\n eventStatus?: string;\n /** EventType: RSVP | TICKETS | EXTERNAL | NO_REGISTRATION */\n eventType?: string;\n scheduling?: WixEventScheduling;\n location?: WixEventLocation;\n mainImage?: WixEventImage;\n slug?: string;\n about?: string;\n description?: string;\n}"
|
|
14373
|
+
"decl": "export interface WixEvent {\n id: string;\n title?: string;\n /** Normally one of `WixEventStatus` — `UPCOMING` (published and scheduled to start),\n * `STARTED`, `ENDED`, `CANCELED`, `DRAFT`. Events v1 called the first of those `SCHEDULED`,\n * and this SDK reads v3, so a row here never carries `SCHEDULED`.\n *\n * Deliberately NOT narrowed to that union: `decodeEvent` fills this from the server's\n * `status` and passes a value it does not recognise straight through, because dropping or\n * rewriting one would lose a real event. A union here would be a claim this SDK cannot\n * enforce. Compare against `WixEventStatus` when you need the set it knows. */\n eventStatus?: string;\n /** EventType: RSVP | TICKETS | EXTERNAL | NO_REGISTRATION */\n eventType?: string;\n scheduling?: WixEventScheduling;\n location?: WixEventLocation;\n mainImage?: WixEventImage;\n slug?: string;\n about?: string;\n /** Events v3: a Ricos document ({@link WixEventRichContent}), returned only when the `TEXTS`\n * fieldset is requested (`getEvent(id, { fields: [\"TEXTS\"] })`). Older payloads sent a string. */\n description?: string | WixEventRichContent;\n}"
|
|
14374
14374
|
},
|
|
14375
14375
|
{
|
|
14376
14376
|
"name": "WixGetEventListResponse",
|
|
@@ -14421,12 +14421,24 @@
|
|
|
14421
14421
|
"decl": "export async function queryEvents(opts: WixQueryEventsOptions = {}): Promise<WixGetEventListResponse>;",
|
|
14422
14422
|
"http": "POST /events/v3/events/query"
|
|
14423
14423
|
},
|
|
14424
|
+
{
|
|
14425
|
+
"name": "WixEventFieldset",
|
|
14426
|
+
"kind": "type",
|
|
14427
|
+
"jsdoc": "/** Events v3 response fieldsets (`RequestedFields`). A bare read returns the base fieldset;\n * each value adds fields: `DETAILS` → `shortDescription`, `mainImage`, `calendarUrls`;\n * `TEXTS` → the Ricos `description`; `REGISTRATION` → `registration`; `URLS` → `eventPageUrl`. */",
|
|
14428
|
+
"decl": "export type WixEventFieldset = \"DETAILS\" | \"TEXTS\" | \"REGISTRATION\" | \"URLS\";"
|
|
14429
|
+
},
|
|
14430
|
+
{
|
|
14431
|
+
"name": "WixEventRichContent",
|
|
14432
|
+
"kind": "interface",
|
|
14433
|
+
"jsdoc": "/** A Ricos document (Events v3 `description`). Opaque here: read it with a Ricos walker, and\n * write it back whole — a PATCH replaces the entire description. */",
|
|
14434
|
+
"decl": "export interface WixEventRichContent { nodes?: unknown[]; metadata?: unknown; documentStyle?: unknown }"
|
|
14435
|
+
},
|
|
14424
14436
|
{
|
|
14425
14437
|
"name": "getEvent",
|
|
14426
14438
|
"kind": "function",
|
|
14427
|
-
"jsdoc": "/** Get one event — GET /events/v3/events/{eventId}. Response wraps as `{ event }`.\n * NOTE: this GET returns the Events v3 BASE fieldset — slimmer than `queryEvents`,\n * which returns the richer default (fields such as `registration` and `calendarUrls`\n * are absent from a bare `getEvent`). If a detail screen needs those fields for one\n * event, prefer `queryEvents` and pick the row by id over `getEvent`. */",
|
|
14428
|
-
"decl": "export async function getEvent(eventId: string): Promise<WixGetEventResponse>;",
|
|
14429
|
-
"http": "GET /events/v3/events/{…}"
|
|
14439
|
+
"jsdoc": "/** Get one event — GET /events/v3/events/{eventId}. Response wraps as `{ event }`.\n * NOTE: this GET returns the Events v3 BASE fieldset — slimmer than `queryEvents`,\n * which returns the richer default (fields such as `registration` and `calendarUrls`\n * are absent from a bare `getEvent`). If a detail screen needs those fields for one\n * event, prefer `queryEvents` and pick the row by id over `getEvent`.\n * `opts.fields` requests extra v3 fieldsets; `[\"TEXTS\"]` returns `description`. */",
|
|
14440
|
+
"decl": "export async function getEvent(eventId: string, opts: { fields?: WixEventFieldset[] } = {}): Promise<WixGetEventResponse>;",
|
|
14441
|
+
"http": "GET /events/v3/events/{…}{…}"
|
|
14430
14442
|
},
|
|
14431
14443
|
{
|
|
14432
14444
|
"name": "deleteEvent",
|
|
@@ -14439,7 +14451,7 @@
|
|
|
14439
14451
|
"name": "WixEventWrite",
|
|
14440
14452
|
"kind": "interface",
|
|
14441
14453
|
"jsdoc": "/** Write shape for create/update (Events v3). Verified-minimal: a create needs\n * BOTH `name` and `title`, a `dateAndTimeSettings` window, a `location`, and a\n * `registration.initialType` (RSVP | TICKETS | RSVP_AND_TICKETS). */",
|
|
14442
|
-
"decl": "export interface WixEventWrite {\n name?: string;\n title?: string;\n dateAndTimeSettings?: { startDate?: string; endDate?: string; timeZoneId?: string };\n location?: { type?: string; name?: string };\n registration?: { initialType?: string };\n mainImage?: WixEventImage;\n about?: string;\n}"
|
|
14454
|
+
"decl": "export interface WixEventWrite {\n name?: string;\n title?: string;\n dateAndTimeSettings?: { startDate?: string; endDate?: string; timeZoneId?: string };\n location?: { type?: string; name?: string };\n registration?: { initialType?: string };\n mainImage?: WixEventImage;\n about?: string;\n /** Ricos document; replaces the whole description. */\n description?: WixEventRichContent;\n}"
|
|
14443
14455
|
},
|
|
14444
14456
|
{
|
|
14445
14457
|
"name": "createEvent",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1072.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"artifactId": "himalaya-cli"
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
|
-
"falconPackageHash": "
|
|
68
|
+
"falconPackageHash": "18f2e2636552474acd27ddb5b45d00aa4507785ea72311c47c6039ae"
|
|
69
69
|
}
|
|
@@ -73953,6 +73953,35 @@ const SCREENS = runtimeSetting({
|
|
|
73953
73953
|
}
|
|
73954
73954
|
}
|
|
73955
73955
|
},
|
|
73956
|
+
{
|
|
73957
|
+
"id": "product_form.description_note",
|
|
73958
|
+
"body": {
|
|
73959
|
+
"package": "himalaya",
|
|
73960
|
+
"type": "Text",
|
|
73961
|
+
"props": {
|
|
73962
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
73963
|
+
"style": "footnote",
|
|
73964
|
+
"color": "textSecondary",
|
|
73965
|
+
"collapseWhenEmpty": true
|
|
73966
|
+
}
|
|
73967
|
+
}
|
|
73968
|
+
},
|
|
73969
|
+
{
|
|
73970
|
+
"id": "product_form.description_preview",
|
|
73971
|
+
"body": {
|
|
73972
|
+
"package": "himalaya",
|
|
73973
|
+
"type": "Text",
|
|
73974
|
+
"props": {
|
|
73975
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
73976
|
+
"style": "body",
|
|
73977
|
+
"maxLines": 8,
|
|
73978
|
+
"collapseWhenEmpty": true
|
|
73979
|
+
}
|
|
73980
|
+
},
|
|
73981
|
+
"modifiers": {
|
|
73982
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
73983
|
+
}
|
|
73984
|
+
},
|
|
73956
73985
|
{
|
|
73957
73986
|
"id": "product_form.form",
|
|
73958
73987
|
"body": {
|
|
@@ -74039,6 +74068,9 @@ const SCREENS = runtimeSetting({
|
|
|
74039
74068
|
"multilineMaxLines": 3
|
|
74040
74069
|
}
|
|
74041
74070
|
}
|
|
74071
|
+
},
|
|
74072
|
+
"modifiers": {
|
|
74073
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
74042
74074
|
}
|
|
74043
74075
|
}
|
|
74044
74076
|
],
|
|
@@ -74566,6 +74598,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
74566
74598
|
}
|
|
74567
74599
|
}
|
|
74568
74600
|
},
|
|
74601
|
+
{
|
|
74602
|
+
"id": "product_form.description_note",
|
|
74603
|
+
"body": {
|
|
74604
|
+
"package": "himalaya",
|
|
74605
|
+
"type": "Text",
|
|
74606
|
+
"props": {
|
|
74607
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
74608
|
+
"style": "footnote",
|
|
74609
|
+
"color": "textSecondary",
|
|
74610
|
+
"collapseWhenEmpty": true
|
|
74611
|
+
}
|
|
74612
|
+
}
|
|
74613
|
+
},
|
|
74614
|
+
{
|
|
74615
|
+
"id": "product_form.description_preview",
|
|
74616
|
+
"body": {
|
|
74617
|
+
"package": "himalaya",
|
|
74618
|
+
"type": "Text",
|
|
74619
|
+
"props": {
|
|
74620
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
74621
|
+
"style": "body",
|
|
74622
|
+
"maxLines": 8,
|
|
74623
|
+
"collapseWhenEmpty": true
|
|
74624
|
+
}
|
|
74625
|
+
},
|
|
74626
|
+
"modifiers": {
|
|
74627
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
74628
|
+
}
|
|
74629
|
+
},
|
|
74569
74630
|
{
|
|
74570
74631
|
"id": "product_form.form",
|
|
74571
74632
|
"body": {
|
|
@@ -74652,6 +74713,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
74652
74713
|
"multilineMaxLines": 3
|
|
74653
74714
|
}
|
|
74654
74715
|
}
|
|
74716
|
+
},
|
|
74717
|
+
"modifiers": {
|
|
74718
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
74655
74719
|
}
|
|
74656
74720
|
}
|
|
74657
74721
|
],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
|
|
2
|
+
// hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
|
|
3
|
+
// [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
|
|
4
|
+
|
|
5
|
+
/** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
|
|
6
|
+
const TAG = /<[a-z!/?]/i;
|
|
7
|
+
|
|
8
|
+
/** True when a plain text field can hold this HTML without losing anything: empty, bare text
|
|
9
|
+
* (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
|
|
10
|
+
* inner tags. */
|
|
11
|
+
export function htmlIsTrivial(html: string | null | undefined): boolean {
|
|
12
|
+
const s = (html ?? "").trim();
|
|
13
|
+
return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ENTITIES: Record<string, string> = { " ": " ", "&": "&", "<": "<", ">": ">", """: "\"", "'": "'" };
|
|
17
|
+
|
|
18
|
+
/** Read-only preview text for an HTML body: block closes become lines, tags are stripped (a bare
|
|
19
|
+
* `<` in prose is kept), the basic entities decode, and blank lines drop. */
|
|
20
|
+
export function htmlPreviewText(html: string | null | undefined): string {
|
|
21
|
+
return (html ?? "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|div|blockquote|pre|tr)>/gi, "\n")
|
|
22
|
+
.replace(/<\/?[a-z!?][^>]*>/gi, "").replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m] ?? m)
|
|
23
|
+
.split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
|
|
27
|
+
const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
|
|
28
|
+
const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
|
|
29
|
+
|
|
30
|
+
/** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
|
|
31
|
+
* `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
|
|
32
|
+
function defaultParagraphData(pd: unknown): boolean {
|
|
33
|
+
if (pd === undefined) return true;
|
|
34
|
+
if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
|
|
35
|
+
const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
|
|
36
|
+
if (indentation !== undefined && indentation !== 0) return false;
|
|
37
|
+
if (textStyle === undefined) return true;
|
|
38
|
+
if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
|
|
39
|
+
const align = (textStyle as { textAlignment?: unknown }).textAlignment;
|
|
40
|
+
return align === undefined || align === "AUTO";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True when a plain text field can round-trip this Ricos document: no document styling, and
|
|
44
|
+
* only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
|
|
45
|
+
* a heading, list, link, bold run, media node, alignment or indent — is rich. */
|
|
46
|
+
export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
|
|
47
|
+
if (!doc || !emptyObject(doc.documentStyle)) return false;
|
|
48
|
+
const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
|
|
49
|
+
return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
|
|
50
|
+
&& onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
|
|
51
|
+
&& (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
|
|
52
|
+
&& onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
|
|
53
|
+
&& !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
|
|
57
|
+
* when the payload is not DraftJS JSON (never a throw). */
|
|
58
|
+
export function draftjsText(raw: string | null | undefined): string {
|
|
59
|
+
try {
|
|
60
|
+
const doc = JSON.parse(raw ?? "") as { blocks?: Array<{ text?: unknown }> } | null;
|
|
61
|
+
const blocks = Array.isArray(doc?.blocks) ? doc.blocks : [];
|
|
62
|
+
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("\n\n").trim();
|
|
63
|
+
} catch { return ""; }
|
|
64
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type WixProductWrite,
|
|
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
|
-
import {
|
|
19
|
+
import { htmlIsTrivial, htmlPreviewText } from "../_shared/owner-rich-guard.js";
|
|
20
20
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
21
21
|
import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch, editablePrice } from "../_shared/owner-preview-demo.js";
|
|
22
22
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
@@ -46,6 +46,14 @@ interface ProductFormState {
|
|
|
46
46
|
price: string;
|
|
47
47
|
sku: string;
|
|
48
48
|
description: string;
|
|
49
|
+
/** [rico] #3363 — the description as loaded (or last saved). A save sends `description` only
|
|
50
|
+
* when the submitted value differs from it, so an untouched body is never re-written. */
|
|
51
|
+
originalDescription: string;
|
|
52
|
+
/** A rich HTML description (more than one bare `<p>`) the plain field would flatten: it loads
|
|
53
|
+
* locked, the descriptor shows `description` as a preview + `descriptionNote`, and an edit is refused. */
|
|
54
|
+
descriptionProtected: boolean;
|
|
55
|
+
descriptionEditable: boolean;
|
|
56
|
+
descriptionNote: string;
|
|
49
57
|
visible: string;
|
|
50
58
|
status: "idle" | "loading" | "saving" | "saved" | "error";
|
|
51
59
|
error: string;
|
|
@@ -115,6 +123,10 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
|
|
|
115
123
|
price: s.price ?? "",
|
|
116
124
|
sku: s.sku ?? "",
|
|
117
125
|
description: s.description ?? "",
|
|
126
|
+
originalDescription: s.originalDescription ?? s.description ?? "",
|
|
127
|
+
descriptionProtected: s.descriptionProtected ?? false,
|
|
128
|
+
descriptionEditable: !(s.descriptionProtected ?? false),
|
|
129
|
+
descriptionNote: s.descriptionProtected ? 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.") : "",
|
|
118
130
|
visible: s.visible ?? "true",
|
|
119
131
|
status: s.status ?? "idle",
|
|
120
132
|
error: s.error ?? "",
|
|
@@ -177,7 +189,7 @@ const svc = defineService<ProductFormState, {
|
|
|
177
189
|
name: (p as any).name ?? "",
|
|
178
190
|
price: priceOf(p),
|
|
179
191
|
sku: (p as any).sku ?? "",
|
|
180
|
-
description:
|
|
192
|
+
...((html: string) => ({ description: htmlPreviewText(html), originalDescription: htmlPreviewText(html), descriptionProtected: !htmlIsTrivial(html) }))(String((p as any).description ?? "")),
|
|
181
193
|
visible: (p as any).visible !== false ? "true" : "false",
|
|
182
194
|
status: "idle",
|
|
183
195
|
error: "",
|
|
@@ -190,6 +202,8 @@ const svc = defineService<ProductFormState, {
|
|
|
190
202
|
return shape({
|
|
191
203
|
...s, mode: "edit", id, heading: "Edit product",
|
|
192
204
|
name: String(cached.name ?? ""), price: editablePrice(String(cached.price ?? "")), sku: String(cached.sku ?? ""),
|
|
205
|
+
// An older cached row carries no verdict, so it stays locked: fail closed.
|
|
206
|
+
description: String(cached.description ?? ""), originalDescription: String(cached.description ?? ""), descriptionProtected: cached.descriptionProtected !== "false",
|
|
193
207
|
visible: cached.visible === "false" ? "false" : "true",
|
|
194
208
|
status: "idle", error: "", savedMessage: "",
|
|
195
209
|
});
|
|
@@ -217,6 +231,9 @@ const svc = defineService<ProductFormState, {
|
|
|
217
231
|
const price = editablePrice(rawPrice);
|
|
218
232
|
const sku = String(v.sku ?? s.sku).trim();
|
|
219
233
|
const description = String(v.description ?? s.description).trim();
|
|
234
|
+
// A queue replay boots a FRESH worker (empty baseline), so it carries the original verdict.
|
|
235
|
+
const descriptionEdited = typeof v.descriptionEdited === "string" ? v.descriptionEdited === "true" : description !== s.originalDescription.trim();
|
|
236
|
+
if (descriptionEdited && s.descriptionProtected) 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.") });
|
|
220
237
|
const visible = boolString(v.visible, s.visible) === "true";
|
|
221
238
|
if (!name) return shape({ ...s, status: "error", error: copyParam("ownerProductForm.nameRequired", "Name is required"), savedMessage: "" });
|
|
222
239
|
const priceNum = Number(price);
|
|
@@ -232,12 +249,12 @@ const svc = defineService<ProductFormState, {
|
|
|
232
249
|
priceData: { price: priceNum, currency: CURRENCY },
|
|
233
250
|
visible,
|
|
234
251
|
...(sku ? { sku } : {}),
|
|
235
|
-
...(
|
|
252
|
+
...(descriptionEdited ? { description } : {}),
|
|
236
253
|
...(s.mode === "edit" && s.id ? {} : { productType: "physical" }),
|
|
237
254
|
};
|
|
238
255
|
|
|
239
256
|
// Optimistic local write — the list repaints from cache immediately (offline or not).
|
|
240
|
-
const localRow = (id: string): DbEntity => ({ id, name, price, sku, visible: visible ? "true" : "false", statusLabel: visible ? "Visible" : "Hidden" });
|
|
257
|
+
const localRow = (id: string): DbEntity => ({ id, name, price, sku, description, descriptionProtected: s.descriptionProtected ? "true" : "false", visible: visible ? "true" : "false", statusLabel: visible ? "Visible" : "Hidden" });
|
|
241
258
|
const localId = s.mode === "edit" && s.id ? s.id : `local-${Date.now()}`;
|
|
242
259
|
await cachePut(localRow(localId));
|
|
243
260
|
try {
|
|
@@ -259,6 +276,7 @@ const svc = defineService<ProductFormState, {
|
|
|
259
276
|
price,
|
|
260
277
|
sku,
|
|
261
278
|
description,
|
|
279
|
+
originalDescription: descriptionEdited ? description : s.originalDescription,
|
|
262
280
|
visible: visible ? "true" : "false",
|
|
263
281
|
status: "saved",
|
|
264
282
|
error: "",
|
|
@@ -273,9 +291,10 @@ const svc = defineService<ProductFormState, {
|
|
|
273
291
|
if (s.demoNote) return demoRefusal(s);
|
|
274
292
|
// Real device offline: the optimistic cache row already shows; queue the
|
|
275
293
|
// write to replay (WriteQueue, idempotent) when connectivity returns.
|
|
276
|
-
await queueWrite("tier5.himiOwnerLiteProductForm.submit", { id: s.id, name, price, sku, visible: String(visible), mode: s.mode });
|
|
294
|
+
await queueWrite("tier5.himiOwnerLiteProductForm.submit", { id: s.id, name, price, sku, description, descriptionEdited: String(descriptionEdited), visible: String(visible), mode: s.mode });
|
|
277
295
|
return shape({
|
|
278
296
|
...s, mode: "edit", id: localId, name, price, sku, description,
|
|
297
|
+
originalDescription: descriptionEdited ? description : s.originalDescription,
|
|
279
298
|
visible: visible ? "true" : "false", status: "saved", error: "",
|
|
280
299
|
savedMessage: copyParam("ownerProductForm.savedOffline", "Saved offline — will sync when you reconnect."),
|
|
281
300
|
savedId: localId,
|
|
@@ -72939,6 +72939,35 @@ const SCREENS = runtimeSetting({
|
|
|
72939
72939
|
}
|
|
72940
72940
|
}
|
|
72941
72941
|
},
|
|
72942
|
+
{
|
|
72943
|
+
"id": "product_form.description_note",
|
|
72944
|
+
"body": {
|
|
72945
|
+
"package": "himalaya",
|
|
72946
|
+
"type": "Text",
|
|
72947
|
+
"props": {
|
|
72948
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
72949
|
+
"style": "footnote",
|
|
72950
|
+
"color": "textSecondary",
|
|
72951
|
+
"collapseWhenEmpty": true
|
|
72952
|
+
}
|
|
72953
|
+
}
|
|
72954
|
+
},
|
|
72955
|
+
{
|
|
72956
|
+
"id": "product_form.description_preview",
|
|
72957
|
+
"body": {
|
|
72958
|
+
"package": "himalaya",
|
|
72959
|
+
"type": "Text",
|
|
72960
|
+
"props": {
|
|
72961
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
72962
|
+
"style": "body",
|
|
72963
|
+
"maxLines": 8,
|
|
72964
|
+
"collapseWhenEmpty": true
|
|
72965
|
+
}
|
|
72966
|
+
},
|
|
72967
|
+
"modifiers": {
|
|
72968
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
72969
|
+
}
|
|
72970
|
+
},
|
|
72942
72971
|
{
|
|
72943
72972
|
"id": "product_form.form",
|
|
72944
72973
|
"body": {
|
|
@@ -73025,6 +73054,9 @@ const SCREENS = runtimeSetting({
|
|
|
73025
73054
|
"multilineMaxLines": 3
|
|
73026
73055
|
}
|
|
73027
73056
|
}
|
|
73057
|
+
},
|
|
73058
|
+
"modifiers": {
|
|
73059
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
73028
73060
|
}
|
|
73029
73061
|
}
|
|
73030
73062
|
],
|
|
@@ -73553,6 +73585,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73553
73585
|
}
|
|
73554
73586
|
}
|
|
73555
73587
|
},
|
|
73588
|
+
{
|
|
73589
|
+
"id": "product_form.description_note",
|
|
73590
|
+
"body": {
|
|
73591
|
+
"package": "himalaya",
|
|
73592
|
+
"type": "Text",
|
|
73593
|
+
"props": {
|
|
73594
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
73595
|
+
"style": "footnote",
|
|
73596
|
+
"color": "textSecondary",
|
|
73597
|
+
"collapseWhenEmpty": true
|
|
73598
|
+
}
|
|
73599
|
+
}
|
|
73600
|
+
},
|
|
73601
|
+
{
|
|
73602
|
+
"id": "product_form.description_preview",
|
|
73603
|
+
"body": {
|
|
73604
|
+
"package": "himalaya",
|
|
73605
|
+
"type": "Text",
|
|
73606
|
+
"props": {
|
|
73607
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
73608
|
+
"style": "body",
|
|
73609
|
+
"maxLines": 8,
|
|
73610
|
+
"collapseWhenEmpty": true
|
|
73611
|
+
}
|
|
73612
|
+
},
|
|
73613
|
+
"modifiers": {
|
|
73614
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
73615
|
+
}
|
|
73616
|
+
},
|
|
73556
73617
|
{
|
|
73557
73618
|
"id": "product_form.form",
|
|
73558
73619
|
"body": {
|
|
@@ -73639,6 +73700,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73639
73700
|
"multilineMaxLines": 3
|
|
73640
73701
|
}
|
|
73641
73702
|
}
|
|
73703
|
+
},
|
|
73704
|
+
"modifiers": {
|
|
73705
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
73642
73706
|
}
|
|
73643
73707
|
}
|
|
73644
73708
|
],
|
|
@@ -73836,6 +73900,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73836
73900
|
}
|
|
73837
73901
|
}
|
|
73838
73902
|
},
|
|
73903
|
+
{
|
|
73904
|
+
"id": "event_form.description_note",
|
|
73905
|
+
"body": {
|
|
73906
|
+
"package": "himalaya",
|
|
73907
|
+
"type": "Text",
|
|
73908
|
+
"props": {
|
|
73909
|
+
"text": "$service:himiOwnerLiteEventForm.descriptionNote",
|
|
73910
|
+
"style": "footnote",
|
|
73911
|
+
"color": "textSecondary",
|
|
73912
|
+
"collapseWhenEmpty": true
|
|
73913
|
+
}
|
|
73914
|
+
}
|
|
73915
|
+
},
|
|
73916
|
+
{
|
|
73917
|
+
"id": "event_form.description_preview",
|
|
73918
|
+
"body": {
|
|
73919
|
+
"package": "himalaya",
|
|
73920
|
+
"type": "Text",
|
|
73921
|
+
"props": {
|
|
73922
|
+
"text": "$service:himiOwnerLiteEventForm.description",
|
|
73923
|
+
"style": "body",
|
|
73924
|
+
"maxLines": 8,
|
|
73925
|
+
"collapseWhenEmpty": true
|
|
73926
|
+
}
|
|
73927
|
+
},
|
|
73928
|
+
"modifiers": {
|
|
73929
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionEditable"
|
|
73930
|
+
}
|
|
73931
|
+
},
|
|
73839
73932
|
{
|
|
73840
73933
|
"id": "event_form.form",
|
|
73841
73934
|
"body": {
|
|
@@ -73880,6 +73973,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73880
73973
|
"multilineMaxLines": 3
|
|
73881
73974
|
}
|
|
73882
73975
|
}
|
|
73976
|
+
},
|
|
73977
|
+
"modifiers": {
|
|
73978
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionProtected"
|
|
73883
73979
|
}
|
|
73884
73980
|
}
|
|
73885
73981
|
],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// [rico] #3363 — may a plain owner form round-trip this body? Ricos uses owner-post-content's
|
|
2
|
+
// hasRichBody; these cover the Stores HTML description and the FAQ's legacy DraftJS answer.
|
|
3
|
+
// [rico] #3365 moves htmlIsTrivial into @himalaya/wix-rich-content with these exact semantics.
|
|
4
|
+
|
|
5
|
+
/** A tag opener (`<p`, `</p`, `<!--`), as opposed to a bare `<` in prose ("cups < 8 cm"). */
|
|
6
|
+
const TAG = /<[a-z!/?]/i;
|
|
7
|
+
|
|
8
|
+
/** True when a plain text field can hold this HTML without losing anything: empty, bare text
|
|
9
|
+
* (no tag — a lone `<` in prose is text), or exactly one attribute-less `<p>…</p>` with no
|
|
10
|
+
* inner tags. */
|
|
11
|
+
export function htmlIsTrivial(html: string | null | undefined): boolean {
|
|
12
|
+
const s = (html ?? "").trim();
|
|
13
|
+
return !s || !TAG.test(s) || /^<p>[^<>]*<\/p>$/.test(s);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ENTITIES: Record<string, string> = { " ": " ", "&": "&", "<": "<", ">": ">", """: "\"", "'": "'" };
|
|
17
|
+
|
|
18
|
+
/** Read-only preview text for an HTML body: block closes become lines, tags are stripped (a bare
|
|
19
|
+
* `<` in prose is kept), the basic entities decode, and blank lines drop. */
|
|
20
|
+
export function htmlPreviewText(html: string | null | undefined): string {
|
|
21
|
+
return (html ?? "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|h[1-6]|li|div|blockquote|pre|tr)>/gi, "\n")
|
|
22
|
+
.replace(/<\/?[a-z!?][^>]*>/gi, "").replace(/&(nbsp|amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m] ?? m)
|
|
23
|
+
.split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RicosNode = { type?: unknown; nodes?: unknown; textData?: { decorations?: unknown }; paragraphData?: unknown; [key: string]: unknown };
|
|
27
|
+
const onlyKeys = (o: object, allowed: string[]) => Object.keys(o).every((k) => allowed.includes(k));
|
|
28
|
+
const emptyObject = (v: unknown) => v === undefined || (!!v && typeof v === "object" && Object.keys(v as object).length === 0);
|
|
29
|
+
|
|
30
|
+
/** The paragraph styling a PLAIN paragraph still carries on the Ricos wire (Events v3 sends
|
|
31
|
+
* `{ textStyle: { textAlignment: "AUTO" }, indentation: 0 }` on every paragraph). */
|
|
32
|
+
function defaultParagraphData(pd: unknown): boolean {
|
|
33
|
+
if (pd === undefined) return true;
|
|
34
|
+
if (!pd || typeof pd !== "object" || !onlyKeys(pd, ["textStyle", "indentation"])) return false;
|
|
35
|
+
const { textStyle, indentation } = pd as { textStyle?: unknown; indentation?: unknown };
|
|
36
|
+
if (indentation !== undefined && indentation !== 0) return false;
|
|
37
|
+
if (textStyle === undefined) return true;
|
|
38
|
+
if (!textStyle || typeof textStyle !== "object" || !onlyKeys(textStyle, ["textAlignment"])) return false;
|
|
39
|
+
const align = (textStyle as { textAlignment?: unknown }).textAlignment;
|
|
40
|
+
return align === undefined || align === "AUTO";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True when a plain text field can round-trip this Ricos document: no document styling, and
|
|
44
|
+
* only paragraphs of undecorated text whose paragraph data is the wire default. Anything else —
|
|
45
|
+
* a heading, list, link, bold run, media node, alignment or indent — is rich. */
|
|
46
|
+
export function ricosIsPlain(doc: { nodes?: unknown; documentStyle?: unknown } | null | undefined): boolean {
|
|
47
|
+
if (!doc || !emptyObject(doc.documentStyle)) return false;
|
|
48
|
+
const nodes = Array.isArray(doc.nodes) ? (doc.nodes as RicosNode[]) : [];
|
|
49
|
+
return nodes.every((n) => !!n && typeof n === "object" && n.type === "PARAGRAPH"
|
|
50
|
+
&& onlyKeys(n, ["type", "id", "nodes", "paragraphData"]) && defaultParagraphData(n.paragraphData)
|
|
51
|
+
&& (Array.isArray(n.nodes) ? (n.nodes as RicosNode[]) : []).every((t) => !!t && typeof t === "object" && t.type === "TEXT"
|
|
52
|
+
&& onlyKeys(t, ["type", "id", "textData", "nodes"]) && (!Array.isArray(t.nodes) || t.nodes.length === 0)
|
|
53
|
+
&& !(Array.isArray(t.textData?.decorations) && t.textData.decorations.length)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Preview text for a legacy DraftJS document: its `blocks[].text` joined by a blank line, or ""
|
|
57
|
+
* when the payload is not DraftJS JSON (never a throw). */
|
|
58
|
+
export function draftjsText(raw: string | null | undefined): string {
|
|
59
|
+
try {
|
|
60
|
+
const doc = JSON.parse(raw ?? "") as { blocks?: Array<{ text?: unknown }> } | null;
|
|
61
|
+
const blocks = Array.isArray(doc?.blocks) ? doc.blocks : [];
|
|
62
|
+
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("\n\n").trim();
|
|
63
|
+
} catch { return ""; }
|
|
64
|
+
}
|