@wix/himalaya-cli 0.1071.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/dist/cli.mjs +2 -0
- 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
|
@@ -73734,6 +73734,35 @@ const SCREENS = runtimeSetting({
|
|
|
73734
73734
|
}
|
|
73735
73735
|
}
|
|
73736
73736
|
},
|
|
73737
|
+
{
|
|
73738
|
+
"id": "faq_question.answer_note",
|
|
73739
|
+
"body": {
|
|
73740
|
+
"package": "himalaya",
|
|
73741
|
+
"type": "Text",
|
|
73742
|
+
"props": {
|
|
73743
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answerNote",
|
|
73744
|
+
"style": "footnote",
|
|
73745
|
+
"color": "textSecondary",
|
|
73746
|
+
"collapseWhenEmpty": true
|
|
73747
|
+
}
|
|
73748
|
+
}
|
|
73749
|
+
},
|
|
73750
|
+
{
|
|
73751
|
+
"id": "faq_question.answer_preview",
|
|
73752
|
+
"body": {
|
|
73753
|
+
"package": "himalaya",
|
|
73754
|
+
"type": "Text",
|
|
73755
|
+
"props": {
|
|
73756
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answer",
|
|
73757
|
+
"style": "body",
|
|
73758
|
+
"maxLines": 8,
|
|
73759
|
+
"collapseWhenEmpty": true
|
|
73760
|
+
}
|
|
73761
|
+
},
|
|
73762
|
+
"modifiers": {
|
|
73763
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerEditable"
|
|
73764
|
+
}
|
|
73765
|
+
},
|
|
73737
73766
|
{
|
|
73738
73767
|
"id": "faq_question.form",
|
|
73739
73768
|
"body": {
|
|
@@ -73779,6 +73808,9 @@ const SCREENS = runtimeSetting({
|
|
|
73779
73808
|
"autoCapitalization": "sentences"
|
|
73780
73809
|
}
|
|
73781
73810
|
}
|
|
73811
|
+
},
|
|
73812
|
+
"modifiers": {
|
|
73813
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerProtected"
|
|
73782
73814
|
}
|
|
73783
73815
|
},
|
|
73784
73816
|
{
|
|
@@ -73946,6 +73978,35 @@ const SCREENS = runtimeSetting({
|
|
|
73946
73978
|
}
|
|
73947
73979
|
}
|
|
73948
73980
|
},
|
|
73981
|
+
{
|
|
73982
|
+
"id": "faq_question.answer_note",
|
|
73983
|
+
"body": {
|
|
73984
|
+
"package": "himalaya",
|
|
73985
|
+
"type": "Text",
|
|
73986
|
+
"props": {
|
|
73987
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answerNote",
|
|
73988
|
+
"style": "footnote",
|
|
73989
|
+
"color": "textSecondary",
|
|
73990
|
+
"collapseWhenEmpty": true
|
|
73991
|
+
}
|
|
73992
|
+
}
|
|
73993
|
+
},
|
|
73994
|
+
{
|
|
73995
|
+
"id": "faq_question.answer_preview",
|
|
73996
|
+
"body": {
|
|
73997
|
+
"package": "himalaya",
|
|
73998
|
+
"type": "Text",
|
|
73999
|
+
"props": {
|
|
74000
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answer",
|
|
74001
|
+
"style": "body",
|
|
74002
|
+
"maxLines": 8,
|
|
74003
|
+
"collapseWhenEmpty": true
|
|
74004
|
+
}
|
|
74005
|
+
},
|
|
74006
|
+
"modifiers": {
|
|
74007
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerEditable"
|
|
74008
|
+
}
|
|
74009
|
+
},
|
|
73949
74010
|
{
|
|
73950
74011
|
"id": "faq_question.form",
|
|
73951
74012
|
"body": {
|
|
@@ -73991,6 +74052,9 @@ const SCREENS = runtimeSetting({
|
|
|
73991
74052
|
"autoCapitalization": "sentences"
|
|
73992
74053
|
}
|
|
73993
74054
|
}
|
|
74055
|
+
},
|
|
74056
|
+
"modifiers": {
|
|
74057
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerProtected"
|
|
73994
74058
|
}
|
|
73995
74059
|
},
|
|
73996
74060
|
{
|
|
@@ -78045,6 +78109,35 @@ const SCREENS = runtimeSetting({
|
|
|
78045
78109
|
}
|
|
78046
78110
|
}
|
|
78047
78111
|
},
|
|
78112
|
+
{
|
|
78113
|
+
"id": "product_form.description_note",
|
|
78114
|
+
"body": {
|
|
78115
|
+
"package": "himalaya",
|
|
78116
|
+
"type": "Text",
|
|
78117
|
+
"props": {
|
|
78118
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
78119
|
+
"style": "footnote",
|
|
78120
|
+
"color": "textSecondary",
|
|
78121
|
+
"collapseWhenEmpty": true
|
|
78122
|
+
}
|
|
78123
|
+
}
|
|
78124
|
+
},
|
|
78125
|
+
{
|
|
78126
|
+
"id": "product_form.description_preview",
|
|
78127
|
+
"body": {
|
|
78128
|
+
"package": "himalaya",
|
|
78129
|
+
"type": "Text",
|
|
78130
|
+
"props": {
|
|
78131
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
78132
|
+
"style": "body",
|
|
78133
|
+
"maxLines": 8,
|
|
78134
|
+
"collapseWhenEmpty": true
|
|
78135
|
+
}
|
|
78136
|
+
},
|
|
78137
|
+
"modifiers": {
|
|
78138
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
78139
|
+
}
|
|
78140
|
+
},
|
|
78048
78141
|
{
|
|
78049
78142
|
"id": "product_form.form",
|
|
78050
78143
|
"body": {
|
|
@@ -78131,6 +78224,9 @@ const SCREENS = runtimeSetting({
|
|
|
78131
78224
|
"multilineMaxLines": 3
|
|
78132
78225
|
}
|
|
78133
78226
|
}
|
|
78227
|
+
},
|
|
78228
|
+
"modifiers": {
|
|
78229
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
78134
78230
|
}
|
|
78135
78231
|
}
|
|
78136
78232
|
],
|
|
@@ -78658,6 +78754,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
78658
78754
|
}
|
|
78659
78755
|
}
|
|
78660
78756
|
},
|
|
78757
|
+
{
|
|
78758
|
+
"id": "product_form.description_note",
|
|
78759
|
+
"body": {
|
|
78760
|
+
"package": "himalaya",
|
|
78761
|
+
"type": "Text",
|
|
78762
|
+
"props": {
|
|
78763
|
+
"text": "$service:himiOwnerLiteProductForm.descriptionNote",
|
|
78764
|
+
"style": "footnote",
|
|
78765
|
+
"color": "textSecondary",
|
|
78766
|
+
"collapseWhenEmpty": true
|
|
78767
|
+
}
|
|
78768
|
+
}
|
|
78769
|
+
},
|
|
78770
|
+
{
|
|
78771
|
+
"id": "product_form.description_preview",
|
|
78772
|
+
"body": {
|
|
78773
|
+
"package": "himalaya",
|
|
78774
|
+
"type": "Text",
|
|
78775
|
+
"props": {
|
|
78776
|
+
"text": "$service:himiOwnerLiteProductForm.description",
|
|
78777
|
+
"style": "body",
|
|
78778
|
+
"maxLines": 8,
|
|
78779
|
+
"collapseWhenEmpty": true
|
|
78780
|
+
}
|
|
78781
|
+
},
|
|
78782
|
+
"modifiers": {
|
|
78783
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionEditable"
|
|
78784
|
+
}
|
|
78785
|
+
},
|
|
78661
78786
|
{
|
|
78662
78787
|
"id": "product_form.form",
|
|
78663
78788
|
"body": {
|
|
@@ -78744,6 +78869,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
78744
78869
|
"multilineMaxLines": 3
|
|
78745
78870
|
}
|
|
78746
78871
|
}
|
|
78872
|
+
},
|
|
78873
|
+
"modifiers": {
|
|
78874
|
+
"hiddenPath": "$service:himiOwnerLiteProductForm.descriptionProtected"
|
|
78747
78875
|
}
|
|
78748
78876
|
}
|
|
78749
78877
|
],
|
|
@@ -79182,6 +79310,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
79182
79310
|
}
|
|
79183
79311
|
}
|
|
79184
79312
|
},
|
|
79313
|
+
{
|
|
79314
|
+
"id": "event_form.description_note",
|
|
79315
|
+
"body": {
|
|
79316
|
+
"package": "himalaya",
|
|
79317
|
+
"type": "Text",
|
|
79318
|
+
"props": {
|
|
79319
|
+
"text": "$service:himiOwnerLiteEventForm.descriptionNote",
|
|
79320
|
+
"style": "footnote",
|
|
79321
|
+
"color": "textSecondary",
|
|
79322
|
+
"collapseWhenEmpty": true
|
|
79323
|
+
}
|
|
79324
|
+
}
|
|
79325
|
+
},
|
|
79326
|
+
{
|
|
79327
|
+
"id": "event_form.description_preview",
|
|
79328
|
+
"body": {
|
|
79329
|
+
"package": "himalaya",
|
|
79330
|
+
"type": "Text",
|
|
79331
|
+
"props": {
|
|
79332
|
+
"text": "$service:himiOwnerLiteEventForm.description",
|
|
79333
|
+
"style": "body",
|
|
79334
|
+
"maxLines": 8,
|
|
79335
|
+
"collapseWhenEmpty": true
|
|
79336
|
+
}
|
|
79337
|
+
},
|
|
79338
|
+
"modifiers": {
|
|
79339
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionEditable"
|
|
79340
|
+
}
|
|
79341
|
+
},
|
|
79185
79342
|
{
|
|
79186
79343
|
"id": "event_form.form",
|
|
79187
79344
|
"body": {
|
|
@@ -79226,6 +79383,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
79226
79383
|
"multilineMaxLines": 3
|
|
79227
79384
|
}
|
|
79228
79385
|
}
|
|
79386
|
+
},
|
|
79387
|
+
"modifiers": {
|
|
79388
|
+
"hiddenPath": "$service:himiOwnerLiteEventForm.descriptionProtected"
|
|
79229
79389
|
}
|
|
79230
79390
|
}
|
|
79231
79391
|
],
|
|
@@ -84882,6 +85042,35 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
84882
85042
|
}
|
|
84883
85043
|
}
|
|
84884
85044
|
},
|
|
85045
|
+
{
|
|
85046
|
+
"id": "faq_question.answer_note",
|
|
85047
|
+
"body": {
|
|
85048
|
+
"package": "himalaya",
|
|
85049
|
+
"type": "Text",
|
|
85050
|
+
"props": {
|
|
85051
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answerNote",
|
|
85052
|
+
"style": "footnote",
|
|
85053
|
+
"color": "textSecondary",
|
|
85054
|
+
"collapseWhenEmpty": true
|
|
85055
|
+
}
|
|
85056
|
+
}
|
|
85057
|
+
},
|
|
85058
|
+
{
|
|
85059
|
+
"id": "faq_question.answer_preview",
|
|
85060
|
+
"body": {
|
|
85061
|
+
"package": "himalaya",
|
|
85062
|
+
"type": "Text",
|
|
85063
|
+
"props": {
|
|
85064
|
+
"text": "$service:himiOwnerLiteFaqQuestionForm.answer",
|
|
85065
|
+
"style": "body",
|
|
85066
|
+
"maxLines": 8,
|
|
85067
|
+
"collapseWhenEmpty": true
|
|
85068
|
+
}
|
|
85069
|
+
},
|
|
85070
|
+
"modifiers": {
|
|
85071
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerEditable"
|
|
85072
|
+
}
|
|
85073
|
+
},
|
|
84885
85074
|
{
|
|
84886
85075
|
"id": "faq_question.form",
|
|
84887
85076
|
"body": {
|
|
@@ -84927,6 +85116,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
84927
85116
|
"autoCapitalization": "sentences"
|
|
84928
85117
|
}
|
|
84929
85118
|
}
|
|
85119
|
+
},
|
|
85120
|
+
"modifiers": {
|
|
85121
|
+
"hiddenPath": "$service:himiOwnerLiteFaqQuestionForm.answerProtected"
|
|
84930
85122
|
}
|
|
84931
85123
|
},
|
|
84932
85124
|
{
|
|
@@ -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
|
+
}
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { defineService } from "@himalaya/state";
|
|
10
10
|
import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
|
|
11
|
-
import { normalizeEvent
|
|
11
|
+
import { normalizeEvent } from "../_shared/himi-events.js";
|
|
12
|
+
import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
|
|
13
|
+
// Not owner-post-content's hasRichBody: Events v3 sends default paragraphData on EVERY paragraph
|
|
14
|
+
// (which hasRichBody reads as formatting) and a documentStyle it never looks at.
|
|
15
|
+
import { ricosIsPlain } from "../_shared/owner-rich-guard.js";
|
|
12
16
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
13
17
|
import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
|
|
14
18
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
@@ -35,7 +39,17 @@ interface EventFormState {
|
|
|
35
39
|
id: string;
|
|
36
40
|
heading: string;
|
|
37
41
|
title: string;
|
|
42
|
+
/** [rico] #3363 — the Events v3 Ricos `description` as text (read with `fields=TEXTS`). While
|
|
43
|
+
* `descriptionProtected`, it is a read-only preview. */
|
|
38
44
|
description: string;
|
|
45
|
+
/** The description as loaded (or last saved). A save writes `description` only when the
|
|
46
|
+
* submitted value differs from it, so an untouched body is never re-written. */
|
|
47
|
+
originalDescription: string;
|
|
48
|
+
/** A Ricos description with formatting or media the plain field would flatten: it loads locked,
|
|
49
|
+
* the descriptor shows a preview + `descriptionNote`, and an edit is refused. */
|
|
50
|
+
descriptionProtected: boolean;
|
|
51
|
+
descriptionEditable: boolean;
|
|
52
|
+
descriptionNote: string;
|
|
39
53
|
when: string;
|
|
40
54
|
location: string;
|
|
41
55
|
status: "idle" | "loading" | "saving" | "saved" | "error";
|
|
@@ -94,6 +108,10 @@ function shape(s: Partial<EventFormState>): EventFormState {
|
|
|
94
108
|
heading: s.heading ?? copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
95
109
|
title: s.title ?? "",
|
|
96
110
|
description: s.description ?? "",
|
|
111
|
+
originalDescription: s.originalDescription ?? s.description ?? "",
|
|
112
|
+
descriptionProtected: s.descriptionProtected ?? false,
|
|
113
|
+
descriptionEditable: !(s.descriptionProtected ?? false),
|
|
114
|
+
descriptionNote: s.descriptionProtected ? 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.") : "",
|
|
97
115
|
when: s.when ?? "—",
|
|
98
116
|
location: s.location ?? "—",
|
|
99
117
|
status: s.status ?? "idle",
|
|
@@ -145,7 +163,8 @@ const svc = defineService<EventFormState, {
|
|
|
145
163
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
146
164
|
}
|
|
147
165
|
try {
|
|
148
|
-
|
|
166
|
+
// `TEXTS` is the v3 fieldset that returns the Ricos `description`; a bare GET omits it.
|
|
167
|
+
const res = await getEvent(id, { fields: ["TEXTS"] });
|
|
149
168
|
const ev = (res.event ?? res) as WixEvent;
|
|
150
169
|
if (!ev || !(ev as any).id) {
|
|
151
170
|
return shape({ ...s, mode, id, status: "error", error: copyParam("ownerEventForm.errorNotFound", "Event not found") });
|
|
@@ -157,7 +176,14 @@ const svc = defineService<EventFormState, {
|
|
|
157
176
|
id,
|
|
158
177
|
heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
159
178
|
title: ev.title ?? "",
|
|
160
|
-
description:
|
|
179
|
+
// The v3 Ricos `description` only: `shortDescription` and `about` are separate fields
|
|
180
|
+
// this form never writes, so showing them here would invite an edit that lands elsewhere.
|
|
181
|
+
...(() => {
|
|
182
|
+
const raw = (ev as unknown as { description?: unknown }).description;
|
|
183
|
+
const doc = raw && typeof raw === "object" && Array.isArray((raw as { nodes?: unknown }).nodes) ? raw as Parameters<typeof postBodyText>[0] : null;
|
|
184
|
+
const text = doc ? postBodyText(doc) : typeof raw === "string" ? raw.trim() : "";
|
|
185
|
+
return { description: text, originalDescription: text, descriptionProtected: doc ? !ricosIsPlain(doc as { nodes?: unknown; documentStyle?: unknown }) : false };
|
|
186
|
+
})(),
|
|
161
187
|
when: row.dateLine || "—",
|
|
162
188
|
location: row.location || "—",
|
|
163
189
|
status: "idle",
|
|
@@ -172,6 +198,8 @@ const svc = defineService<EventFormState, {
|
|
|
172
198
|
...s, mode, id, heading: "Edit event",
|
|
173
199
|
title: String(cached.title ?? cached.name ?? ""),
|
|
174
200
|
description: String(cached.description ?? ""),
|
|
201
|
+
// An older cached row carries no verdict, so it stays locked: fail closed.
|
|
202
|
+
originalDescription: String(cached.description ?? ""), descriptionProtected: cached.descriptionProtected !== "false",
|
|
175
203
|
when: String(cached.when ?? cached.dateLine ?? "—"),
|
|
176
204
|
location: String(cached.location ?? "—"),
|
|
177
205
|
status: "idle", error: "", savedMessage: "",
|
|
@@ -195,18 +223,23 @@ const svc = defineService<EventFormState, {
|
|
|
195
223
|
const v = values ?? {};
|
|
196
224
|
const title = String(v.title ?? s.title).trim();
|
|
197
225
|
const description = String(v.description ?? s.description).trim();
|
|
226
|
+
// A queue replay boots a FRESH worker (empty baseline), so it carries the original verdict.
|
|
227
|
+
const descriptionEdited = typeof v.descriptionEdited === "string" ? v.descriptionEdited === "true" : description !== s.originalDescription.trim();
|
|
228
|
+
if (descriptionEdited && s.descriptionProtected) 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.") });
|
|
198
229
|
if (!title) return shape({ ...s, status: "error", error: copyParam("ownerEventForm.titleRequired", "Title is required"), savedMessage: "" });
|
|
199
230
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerEventForm.errorNoEvent", "No event to edit."), savedMessage: "" });
|
|
200
231
|
|
|
201
232
|
// updateEvent(id, event) wraps the body as { event } internally, so pass the
|
|
202
|
-
// event fields directly. Only the text the form can safely edit is written
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
233
|
+
// event fields directly. Only the text the form can safely edit is written: the
|
|
234
|
+
// description only when edited, and always as a Ricos document (never a string).
|
|
235
|
+
// A local structural type, not WixEventWrite: templates compile against the
|
|
236
|
+
// 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: [] } } : {}),
|
|
206
239
|
};
|
|
207
240
|
|
|
208
241
|
// Optimistic local write — the detail/list repaint from cache immediately.
|
|
209
|
-
const localRow: DbEntity = { id: s.id, name: title, title, description, when: s.when, location: s.location };
|
|
242
|
+
const localRow: DbEntity = { id: s.id, name: title, title, description, descriptionProtected: String(s.descriptionProtected), when: s.when, location: s.location };
|
|
210
243
|
await cachePut(localRow);
|
|
211
244
|
try {
|
|
212
245
|
await updateEvent(s.id, event);
|
|
@@ -217,6 +250,7 @@ const svc = defineService<EventFormState, {
|
|
|
217
250
|
heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
218
251
|
title,
|
|
219
252
|
description,
|
|
253
|
+
originalDescription: descriptionEdited ? description : s.originalDescription,
|
|
220
254
|
status: "saved",
|
|
221
255
|
error: "",
|
|
222
256
|
savedMessage: copyParam("ownerEventForm.savedMessage", "Event saved."),
|
|
@@ -230,10 +264,10 @@ const svc = defineService<EventFormState, {
|
|
|
230
264
|
if (s.demoNote) return demoRefusal(s);
|
|
231
265
|
// Real device offline: the optimistic cache row already shows; queue the
|
|
232
266
|
// write to replay (WriteQueue, idempotent) when connectivity returns.
|
|
233
|
-
await queueWrite("tier5.himiOwnerLiteEventForm.submit", { id: s.id, title, description });
|
|
267
|
+
await queueWrite("tier5.himiOwnerLiteEventForm.submit", { id: s.id, title, description, descriptionEdited: String(descriptionEdited) });
|
|
234
268
|
return shape({
|
|
235
269
|
...s, mode: "edit", id: s.id, heading: copyParam("ownerEventForm.headingEdit", "Edit event"),
|
|
236
|
-
title, description, status: "saved", error: "",
|
|
270
|
+
title, description, originalDescription: descriptionEdited ? description : s.originalDescription, status: "saved", error: "",
|
|
237
271
|
savedMessage: copyParam("ownerEventForm.savedOffline", "Saved offline — will sync when you reconnect."),
|
|
238
272
|
savedId: s.id,
|
|
239
273
|
});
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
// Paging loops carry a seen-cursor guard: a server that repeats a cursor would
|
|
2
2
|
// otherwise spin forever and leave a create/submit permanently pending.
|
|
3
3
|
import { defineService } from "@himalaya/state";
|
|
4
|
-
import { createQuestionEntry, getQuestionEntry, queryCategories, queryQuestionEntries, updateQuestionEntry, type WixCategory } from "@himalaya/wix-faq";
|
|
4
|
+
import { createQuestionEntry, getQuestionEntry, queryCategories, queryQuestionEntries, updateQuestionEntry, type WixCategory, type WixQuestionEntry } from "@himalaya/wix-faq";
|
|
5
|
+
import { postBodyText } from "../_shared/owner-post-content.js";
|
|
6
|
+
import { draftjsText } from "../_shared/owner-rich-guard.js";
|
|
5
7
|
|
|
6
|
-
interface State { id: string; revision: string; mode: "create" | "edit"; heading: string; question: string; answer: string; categoryId: string; statusValue: "VISIBLE" | "HIDDEN"; categoryOptions: Array<{id:string;label:string}>; error: string; conflict: boolean; noConflict: boolean; savedMessage: string; savedId: string; }
|
|
8
|
+
interface State { id: string; revision: string; mode: "create" | "edit"; heading: string; question: string; answer: string; categoryId: string; statusValue: "VISIBLE" | "HIDDEN"; categoryOptions: Array<{id:string;label:string}>; error: string; conflict: boolean; noConflict: boolean; savedMessage: string; savedId: string; answerFormat: "plain" | "rich" | "draftjs"; answerProtected: boolean; answerEditable: boolean; answerNote: string; }
|
|
7
9
|
const input = ((globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input) ?? {};
|
|
8
10
|
const msg = (e: unknown) => String((e as { message?: unknown })?.message ?? e);
|
|
9
11
|
const isConflict = (e: unknown) => /\b409\b|conflict|revision/i.test(msg(e));
|
|
10
12
|
const offline = (e: unknown) => /offline|network|connect|bridge|fetch/i.test(msg(e));
|
|
13
|
+
// [rico] #3363 — the answer is a one-of (draftjs | richContent | plainText) and this form edits plainText
|
|
14
|
+
// only. A rich or DraftJS answer loads LOCKED: `answer` holds its read-only preview, the required check
|
|
15
|
+
// is skipped, and plainText stays out of the PATCH and its fieldMask. #3373 replaces the lock with the editor.
|
|
16
|
+
const RICH_ANSWER_NOTE = "This answer has formatting or media. Edit it in your site dashboard to keep that content intact; the question, category and visibility can be changed here.";
|
|
17
|
+
const RICH_ANSWER_ERROR = "This answer has formatting or media. Edit it in your site dashboard to keep that content intact.";
|
|
18
|
+
// A locked answer whose only content is media/embeds has no preview text. `answer` is also the
|
|
19
|
+
// hidden (required) field's bound value, so it must never be empty while locked.
|
|
20
|
+
const MEDIA_ONLY_ANSWER = "This answer has media but no text.";
|
|
21
|
+
const answerOf = (q: WixQuestionEntry): { answerFormat: State["answerFormat"]; answer: string } =>
|
|
22
|
+
q.richContent ? { answerFormat: "rich", answer: postBodyText(q.richContent as unknown as Parameters<typeof postBodyText>[0]) || MEDIA_ONLY_ANSWER }
|
|
23
|
+
: q.draftjs ? { answerFormat: "draftjs", answer: draftjsText(q.draftjs) || MEDIA_ONLY_ANSWER } : { answerFormat: "plain", answer: q.plainText ?? "" };
|
|
11
24
|
async function categories(): Promise<WixCategory[]> { const out: WixCategory[]=[]; let cursor=""; const seen=new Set<string>(); do { const r=await queryCategories({query:{cursorPaging:{limit:100,...(cursor?{cursor}:{})},sort:[{fieldName:"sortOrder",order:"ASC"}]}}); out.push(...(r.categories??[])); const next=r.pagingMetadata?.cursors?.next??"";if(!next||seen.has(next))break;seen.add(next);cursor=next; } while(cursor); return out; }
|
|
12
25
|
async function appendOrder(): Promise<number> { let max=-1,cursor=""; const seen=new Set<string>(); do { const r=await queryQuestionEntries({contentFormat:"PLAIN_TEXT",query:{cursorPaging:{limit:100,...(cursor?{cursor}:{})}}}); for(const q of r.questionEntries??[]) max=Math.max(max,q.sortOrder??-1); const next=r.pagingMetadata?.cursors?.next??"";if(!next||seen.has(next))break;seen.add(next);cursor=next; } while(cursor); return max+1; }
|
|
13
|
-
const shape=(s:Partial<State>):State=>({ id:s.id??input.id??"", revision:s.revision??"", mode:s.mode??((s.id??input.id)?"edit":"create"), heading:s.heading??((s.id??input.id)?"Edit question":"New question"), question:s.question??"", answer:s.answer??"", categoryId:s.categoryId??"", statusValue:s.statusValue??"HIDDEN", categoryOptions:s.categoryOptions??[], error:s.error??"", conflict:s.conflict??false, noConflict:!(s.conflict??false), savedMessage:s.savedMessage??"", savedId:s.savedId??"" });
|
|
26
|
+
const shape=(s:Partial<State>):State=>({ id:s.id??input.id??"", revision:s.revision??"", mode:s.mode??((s.id??input.id)?"edit":"create"), heading:s.heading??((s.id??input.id)?"Edit question":"New question"), question:s.question??"", answer:s.answer??"", categoryId:s.categoryId??"", statusValue:s.statusValue??"HIDDEN", categoryOptions:s.categoryOptions??[], error:s.error??"", conflict:s.conflict??false, noConflict:!(s.conflict??false), savedMessage:s.savedMessage??"", savedId:s.savedId??"", ...(() => { const f = s.answerFormat ?? "plain"; return { answerFormat: f, answerProtected: f !== "plain", answerEditable: f === "plain", answerNote: f !== "plain" ? RICH_ANSWER_NOTE : "" }; })() });
|
|
14
27
|
const svc=defineService<State,{load:(s:State,a?:unknown)=>Promise<State>;submit:(s:State,v?:Record<string,unknown>)=>Promise<State>}>("himiOwnerLiteFaqQuestionForm",{state:shape({}),actions:{
|
|
15
|
-
load:async(s,a)=>{const id=input.id||String((a as {id?:unknown})?.id??s.id);try{const cats=await categories();const options=cats.map(c=>({id:c.id??"",label:c.title?.trim()||"Untitled category"}));if(!id)return shape({...s,mode:"create",categoryOptions:options,statusValue:"HIDDEN"});const q=(await getQuestionEntry(id)).questionEntry;if(!q) return shape({...s,id,error:"Question not found",categoryOptions:options});return shape({...s,id,revision:q.revision??"",mode:"edit",heading:"Edit question",question:q.question??""
|
|
16
|
-
submit:async(s,v)=>{const question=String(v?.question??s.question).trim(),answer=String(v?.answer??s.answer).trim(),categoryId=String(v?.categoryId??s.categoryId),statusValue=String(v?.status??s.statusValue)==="VISIBLE"?"VISIBLE":"HIDDEN";if(!question
|
|
28
|
+
load:async(s,a)=>{const id=input.id||String((a as {id?:unknown})?.id??s.id);try{const cats=await categories();const options=cats.map(c=>({id:c.id??"",label:c.title?.trim()||"Untitled category"}));if(!id)return shape({...s,mode:"create",categoryOptions:options,statusValue:"HIDDEN"});const q=(await getQuestionEntry(id)).questionEntry;if(!q) return shape({...s,id,error:"Question not found",categoryOptions:options});return shape({...s,id,revision:q.revision??"",mode:"edit",heading:"Edit question",question:q.question??"",...answerOf(q),categoryId:q.categoryId??"",statusValue:q.status==="VISIBLE"?"VISIBLE":"HIDDEN",categoryOptions:options,error:"",conflict:false});}catch(e){return shape({...s,id,error:offline(e)?"Connect to the internet to load this question.":msg(e)});}},
|
|
29
|
+
submit:async(s,v)=>{const question=String(v?.question??s.question).trim(),answer=String(v?.answer??s.answer).trim(),categoryId=String(v?.categoryId??s.categoryId),statusValue=String(v?.status??s.statusValue)==="VISIBLE"?"VISIBLE":"HIDDEN";const locked=s.mode==="edit"&&s.answerProtected;if(locked&&answer!==s.answer.trim())return shape({...s,error:RICH_ANSWER_ERROR,savedMessage:""});if(!question||(!locked&&!answer))return shape({...s,error:"Question and answer are required.",savedMessage:""});try{if(s.mode==="edit"){if(!s.revision)return shape({...s,error:"Refresh before saving; this question has no current revision.",conflict:true});const r=await updateQuestionEntry(s.id,{questionEntry:locked?{id:s.id,revision:s.revision,question,categoryId,status:statusValue}:{id:s.id,revision:s.revision,question,plainText:answer,categoryId,status:statusValue},fieldMask:locked?["question","categoryId","status"]:["question","plainText","categoryId","status"]});return shape({...s,revision:r.questionEntry?.revision??s.revision,question,answer:locked?s.answer:answer,categoryId,statusValue,error:"",conflict:false,savedMessage:"Question saved.",savedId:s.id});}const r=await createQuestionEntry({questionEntry:{question,plainText:answer,categoryId,status:statusValue,sortOrder:await appendOrder()}});return shape({...s,id:r.questionEntry?.id??"",revision:r.questionEntry?.revision??"",mode:"edit",heading:"Edit question",question,answer,categoryId,statusValue,error:"",savedMessage:"Question created.",savedId:r.questionEntry?.id??""});}catch(e){if(isConflict(e))return shape({...s,error:"This question changed elsewhere. Refresh before saving again.",conflict:true,savedMessage:""});return shape({...s,error:offline(e)?"Connect to the internet and try again. This change was not queued.":msg(e),savedMessage:""});}}
|
|
17
30
|
}}); export {svc,appendOrder};
|
|
@@ -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,
|