@wix/himalaya-cli 0.1145.0 → 0.1147.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/comments-v1/src/extensions.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/forum/src/extensions.d.ts +10 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +5 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-forum.mjs +13 -0
- package/dist/cli.mjs +75 -19
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/headless-loop.md +34 -0
- package/guides/himi-mcp.md +2 -0
- package/guides/sdk-reference.json +13 -1
- package/guides/tier-5-add-a-screen.md +4 -2
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +62 -3
- package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +35 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/branded-lite/dev/index.ts +273 -12
- package/templates/branded-lite/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-cms-collection/index.ts +43 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-faq/index.ts +69 -13
- package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +23 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +35 -4
- package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +29 -5
- package/templates/community-club/dev/index.ts +135 -5
- package/templates/community-club/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/community-club/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/community-club/tier5-src/himi-branded-lite-cms-collection/index.ts +43 -3
- package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +23 -3
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/events-meetups/dev/index.ts +91 -4
- package/templates/events-meetups/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +23 -3
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/gym-fitness/dev/index.ts +92 -4
- package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +35 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/nonprofit-cause/dev/index.ts +91 -4
- package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +23 -3
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/owner-lite/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/podcast-creator/dev/index.ts +91 -4
- package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +23 -3
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/portfolio-creative/dev/index.ts +92 -4
- package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +35 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/restaurant/dev/index.ts +60 -2
- package/templates/restaurant/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/restaurant/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/retail-store/dev/index.ts +62 -3
- package/templates/retail-store/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/retail-store/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +35 -4
- package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
- package/templates/yoga-studio/dev/index.ts +60 -2
- package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +12 -2
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +6 -1
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +40 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +10 -4
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +6 -1
|
@@ -309,7 +309,12 @@ async function measuredRevenue7d(
|
|
|
309
309
|
if (typeof measure.total === "number") return measure.total;
|
|
310
310
|
const values = measure.values ?? [];
|
|
311
311
|
return values.length ? values.reduce((sum, v) => sum + (v.value || 0), 0) : undefined;
|
|
312
|
-
} catch {
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// The fallback is silent on screen (Home shows the order-row sum), so say it where
|
|
314
|
+
// an agent looks: the DEV panel's Console tab and `himi logs`. The status only — a Wix
|
|
315
|
+
// failure's message carries part of the upstream body, which may hold personal data.
|
|
316
|
+
const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
|
|
317
|
+
console.warn("[home.revenue] analytics failed; showing the order-row sum", status !== undefined ? `HTTP ${String(status)}` : "");
|
|
313
318
|
return undefined;
|
|
314
319
|
}
|
|
315
320
|
}
|
|
@@ -22387,7 +22387,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
22387
22387
|
}
|
|
22388
22388
|
},
|
|
22389
22389
|
{
|
|
22390
|
-
"id": "",
|
|
22390
|
+
"id": "product_body",
|
|
22391
|
+
"body": {
|
|
22392
|
+
"package": "himalaya",
|
|
22393
|
+
"type": "RichContent",
|
|
22394
|
+
"props": {
|
|
22395
|
+
"documentJsonPath": "$service:himiStoreProduct.descriptionJson",
|
|
22396
|
+
"onLink": {
|
|
22397
|
+
"actionId": "tier5.himiStoreProduct.openLink"
|
|
22398
|
+
},
|
|
22399
|
+
"fallback": {
|
|
22400
|
+
"id": "",
|
|
22401
|
+
"body": {
|
|
22402
|
+
"package": "himalaya",
|
|
22403
|
+
"type": "Text",
|
|
22404
|
+
"props": {
|
|
22405
|
+
"text": "$service:himiStoreProduct.description",
|
|
22406
|
+
"style": "body",
|
|
22407
|
+
"color": "textSecondary",
|
|
22408
|
+
"collapseWhenEmpty": true
|
|
22409
|
+
}
|
|
22410
|
+
}
|
|
22411
|
+
}
|
|
22412
|
+
}
|
|
22413
|
+
}
|
|
22414
|
+
},
|
|
22415
|
+
{
|
|
22416
|
+
"id": "product_body_text",
|
|
22391
22417
|
"body": {
|
|
22392
22418
|
"package": "himalaya",
|
|
22393
22419
|
"type": "Text",
|
|
@@ -22397,6 +22423,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
22397
22423
|
"color": "textSecondary",
|
|
22398
22424
|
"collapseWhenEmpty": true
|
|
22399
22425
|
}
|
|
22426
|
+
},
|
|
22427
|
+
"modifiers": {
|
|
22428
|
+
"hiddenPath": "$service:himiStoreProduct.hasDescriptionJson"
|
|
22400
22429
|
}
|
|
22401
22430
|
}
|
|
22402
22431
|
]
|
|
@@ -22891,7 +22920,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
22891
22920
|
"declaredCapabilities": [
|
|
22892
22921
|
"wix_api",
|
|
22893
22922
|
"share",
|
|
22894
|
-
"local_database"
|
|
22923
|
+
"local_database",
|
|
22924
|
+
"open_url"
|
|
22895
22925
|
],
|
|
22896
22926
|
"minBinaryVersion": "0.1.0",
|
|
22897
22927
|
"bundleVersion": "0.0.1"
|
|
@@ -23477,7 +23507,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
23477
23507
|
}
|
|
23478
23508
|
},
|
|
23479
23509
|
{
|
|
23480
|
-
"id": "",
|
|
23510
|
+
"id": "event_body",
|
|
23511
|
+
"body": {
|
|
23512
|
+
"package": "himalaya",
|
|
23513
|
+
"type": "RichContent",
|
|
23514
|
+
"props": {
|
|
23515
|
+
"documentJsonPath": "$service:himiEventDetail.descriptionJson",
|
|
23516
|
+
"onLink": {
|
|
23517
|
+
"actionId": "tier5.himiEventDetail.openLink"
|
|
23518
|
+
},
|
|
23519
|
+
"fallback": {
|
|
23520
|
+
"id": "",
|
|
23521
|
+
"body": {
|
|
23522
|
+
"package": "himalaya",
|
|
23523
|
+
"type": "Text",
|
|
23524
|
+
"props": {
|
|
23525
|
+
"text": "$service:himiEventDetail.description",
|
|
23526
|
+
"style": "body",
|
|
23527
|
+
"color": "textSecondary",
|
|
23528
|
+
"collapseWhenEmpty": true
|
|
23529
|
+
}
|
|
23530
|
+
}
|
|
23531
|
+
}
|
|
23532
|
+
}
|
|
23533
|
+
}
|
|
23534
|
+
},
|
|
23535
|
+
{
|
|
23536
|
+
"id": "event_body_text",
|
|
23481
23537
|
"body": {
|
|
23482
23538
|
"package": "himalaya",
|
|
23483
23539
|
"type": "Text",
|
|
@@ -23487,6 +23543,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
23487
23543
|
"color": "textSecondary",
|
|
23488
23544
|
"collapseWhenEmpty": true
|
|
23489
23545
|
}
|
|
23546
|
+
},
|
|
23547
|
+
"modifiers": {
|
|
23548
|
+
"hiddenPath": "$service:himiEventDetail.hasDescriptionJson"
|
|
23490
23549
|
}
|
|
23491
23550
|
}
|
|
23492
23551
|
]
|
|
@@ -25381,7 +25440,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
25381
25440
|
}
|
|
25382
25441
|
},
|
|
25383
25442
|
{
|
|
25384
|
-
"id": "",
|
|
25443
|
+
"id": "comment_body_$item.id",
|
|
25444
|
+
"body": {
|
|
25445
|
+
"package": "himalaya",
|
|
25446
|
+
"type": "RichContent",
|
|
25447
|
+
"props": {
|
|
25448
|
+
"documentJsonPath": "$item.bodyJson",
|
|
25449
|
+
"onLink": {
|
|
25450
|
+
"actionId": "tier5.himiBlogPost.openLink"
|
|
25451
|
+
},
|
|
25452
|
+
"fallback": {
|
|
25453
|
+
"id": "",
|
|
25454
|
+
"body": {
|
|
25455
|
+
"package": "himalaya",
|
|
25456
|
+
"type": "Text",
|
|
25457
|
+
"props": {
|
|
25458
|
+
"text": "$item.text",
|
|
25459
|
+
"style": "body",
|
|
25460
|
+
"color": "textSecondary",
|
|
25461
|
+
"collapseWhenEmpty": true
|
|
25462
|
+
}
|
|
25463
|
+
}
|
|
25464
|
+
}
|
|
25465
|
+
}
|
|
25466
|
+
}
|
|
25467
|
+
},
|
|
25468
|
+
{
|
|
25469
|
+
"id": "comment_body_text_$item.id",
|
|
25385
25470
|
"body": {
|
|
25386
25471
|
"package": "himalaya",
|
|
25387
25472
|
"type": "Text",
|
|
@@ -25391,6 +25476,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
25391
25476
|
"color": "textSecondary",
|
|
25392
25477
|
"collapseWhenEmpty": true
|
|
25393
25478
|
}
|
|
25479
|
+
},
|
|
25480
|
+
"modifiers": {
|
|
25481
|
+
"hiddenPath": "$item.hasBodyJson"
|
|
25394
25482
|
}
|
|
25395
25483
|
}
|
|
25396
25484
|
]
|
|
@@ -14,15 +14,21 @@ export interface RichBody {
|
|
|
14
14
|
bodyText: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
|
|
18
|
+
* list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
|
|
19
|
+
export const ROW_IMAGE_WIDTH = 1000;
|
|
20
|
+
|
|
17
21
|
/** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
|
|
18
22
|
* that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
|
|
19
23
|
* gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
|
|
20
|
-
* plain-text walk has no depth limit), so `fallback` stands in for it.
|
|
21
|
-
|
|
24
|
+
* plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
|
|
25
|
+
* the 1400 px detail budget). */
|
|
26
|
+
export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
|
|
22
27
|
const doc = parseRicosJson(richContent);
|
|
23
28
|
if (!doc) return { bodyJson: "", bodyText: fallback };
|
|
24
|
-
// 1400 px — the detail-image budget, which is also the encoder's maximum
|
|
25
|
-
|
|
29
|
+
// 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
|
|
30
|
+
// ROW_IMAGE_WIDTH.
|
|
31
|
+
const { json, rejected } = encodeRichContent(doc, { imageWidth });
|
|
26
32
|
const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
|
|
27
33
|
return { bodyJson: json, bodyText: text || fallback };
|
|
28
34
|
}
|
|
@@ -8,7 +8,7 @@ import { isSavedId, toggleSavedId } from "../_shared/himi-blog-saved";
|
|
|
8
8
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
9
9
|
import { formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
|
|
10
10
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
11
|
-
import { richBody } from "../_shared/rich-body.js";
|
|
11
|
+
import { richBody, ROW_IMAGE_WIDTH } from "../_shared/rich-body.js";
|
|
12
12
|
|
|
13
13
|
// Offline-first (data-offline): the post body is a public read-through — when the
|
|
14
14
|
// live fetch fails (offline) fall back to the last-synced row the feed cached under
|
|
@@ -23,8 +23,14 @@ async function readCachedPost(id: string): Promise<DbEntity | null> {
|
|
|
23
23
|
interface CommentRow {
|
|
24
24
|
id: string;
|
|
25
25
|
authorName: string;
|
|
26
|
+
/** The comment's plain text: the row RichContent's fallback. */
|
|
26
27
|
text: string;
|
|
27
28
|
meta: string;
|
|
29
|
+
/** [rico] #3372 — the comment's Ricos document as ONE string for the row's RichContent; "" when it has none
|
|
30
|
+
* or the encoder rejected it. */
|
|
31
|
+
bodyJson: string;
|
|
32
|
+
/** bodyJson !== "" — gates the row's plain text, because RichContent draws nothing for "". */
|
|
33
|
+
hasBodyJson: boolean;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
interface S {
|
|
@@ -168,11 +174,15 @@ function commentMeta(row: WixBlogCommentRow): string {
|
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
function toCommentRow(row: WixBlogCommentRow): CommentRow {
|
|
177
|
+
// The plain text is the document's own (runs joined as written, not space-separated) when it has one.
|
|
178
|
+
const { bodyJson, bodyText } = richBody(row.richContent, row.text || "Comment", ROW_IMAGE_WIDTH);
|
|
171
179
|
return {
|
|
172
180
|
id: row.id,
|
|
173
181
|
authorName: row.authorName || "Member",
|
|
174
|
-
text:
|
|
182
|
+
text: bodyText,
|
|
175
183
|
meta: commentMeta(row),
|
|
184
|
+
bodyJson,
|
|
185
|
+
hasBodyJson: bodyJson !== "",
|
|
176
186
|
};
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -508,7 +508,12 @@ async function fetchBookingsSignal(arg: unknown): Promise<BookingsSignal> {
|
|
|
508
508
|
try {
|
|
509
509
|
const response = await queryServices({ query: { paging: { limit: 50 } } });
|
|
510
510
|
return bookingsSignal(response.services ?? []);
|
|
511
|
-
} catch {
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// The block collapses without a word on screen, so say why where an agent looks:
|
|
513
|
+
// the DEV panel's Console tab and `himi logs`. The status only — a Wix failure's
|
|
514
|
+
// message carries part of the upstream body, which may hold personal data.
|
|
515
|
+
const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
|
|
516
|
+
console.warn("[home.bookings] services query failed; showcase hidden", status !== undefined ? `HTTP ${String(status)}` : "");
|
|
512
517
|
return emptyBookingsSignal();
|
|
513
518
|
}
|
|
514
519
|
}
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
} from "../_shared/himi-events";
|
|
36
36
|
import { initials } from "../_shared/himi-text.js";
|
|
37
37
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
38
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
39
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
38
40
|
|
|
39
41
|
interface MapMarker {
|
|
40
42
|
latitude: number;
|
|
@@ -88,7 +90,13 @@ interface S {
|
|
|
88
90
|
initial: string;
|
|
89
91
|
when: string;
|
|
90
92
|
location: string;
|
|
93
|
+
/** The plain-text description: RichContent's fallback, the calendar notes, and what shows with no document. */
|
|
91
94
|
description: string;
|
|
95
|
+
/** [rico] #3372 — the v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string for
|
|
96
|
+
* RichContent; "" when the event has none or the encoder rejected it. */
|
|
97
|
+
descriptionJson: string;
|
|
98
|
+
/** descriptionJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
99
|
+
hasDescriptionJson: boolean;
|
|
92
100
|
image: string;
|
|
93
101
|
hasImage: boolean;
|
|
94
102
|
noImage: boolean;
|
|
@@ -212,6 +220,7 @@ function derive(s: S): S {
|
|
|
212
220
|
const hasAttendees = s.attendeeAvatars.length > 0;
|
|
213
221
|
return {
|
|
214
222
|
...s,
|
|
223
|
+
hasDescriptionJson: s.descriptionJson !== "",
|
|
215
224
|
noMap: !s.mapUrl,
|
|
216
225
|
hasMapMarkers,
|
|
217
226
|
noMapMarkers: !hasMapMarkers,
|
|
@@ -357,6 +366,14 @@ async function attendeeSummary(eventId: string): Promise<{ goingLabel: string; a
|
|
|
357
366
|
}
|
|
358
367
|
}
|
|
359
368
|
|
|
369
|
+
/** The description pair: the v3 Ricos `description` for RichContent, and its plain text. A string description
|
|
370
|
+
* (or none) keeps `eventDescription`'s legacy read, with no document. */
|
|
371
|
+
function eventBody(e: WixEvent): { description: string; descriptionJson: string } {
|
|
372
|
+
const legacy = eventDescription(e);
|
|
373
|
+
const { bodyJson, bodyText } = richBody((e as unknown as { description?: unknown }).description, legacy);
|
|
374
|
+
return { description: bodyText, descriptionJson: bodyJson };
|
|
375
|
+
}
|
|
376
|
+
|
|
360
377
|
/** getEvent returns the slim BASE fieldset (no `registration`); enrich from the
|
|
361
378
|
* richer query when the type is unknown — best-effort, find-by-id. */
|
|
362
379
|
async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
|
|
@@ -364,7 +381,11 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
|
|
|
364
381
|
try {
|
|
365
382
|
const list = await queryEvents({ limit: 100 });
|
|
366
383
|
const match = (list.events ?? []).find((candidate) => candidate.id === id);
|
|
367
|
-
|
|
384
|
+
if (!match) return e;
|
|
385
|
+
// [rico] #3372 — the query's rows are read without TEXTS, so they carry no Ricos `description`: keep the
|
|
386
|
+
// one this detail fetched, or the enrichment would silently drop the document on every real site.
|
|
387
|
+
const description = (e as unknown as { description?: unknown }).description;
|
|
388
|
+
return description === undefined ? match : ({ ...match, description } as unknown as WixEvent);
|
|
368
389
|
} catch {
|
|
369
390
|
return e;
|
|
370
391
|
}
|
|
@@ -377,6 +398,8 @@ const svc = defineService<S, {
|
|
|
377
398
|
openRegistration: (s: S) => Promise<S>;
|
|
378
399
|
share: (s: S) => Promise<S>;
|
|
379
400
|
addToCalendar: (s: S) => Promise<S>;
|
|
401
|
+
/** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
402
|
+
openLink: (s: S, arg?: unknown) => Promise<S>;
|
|
380
403
|
}>("himiEventDetail", {
|
|
381
404
|
state: derive({
|
|
382
405
|
status: "loading",
|
|
@@ -386,6 +409,8 @@ const svc = defineService<S, {
|
|
|
386
409
|
when: "",
|
|
387
410
|
location: "",
|
|
388
411
|
description: "",
|
|
412
|
+
descriptionJson: "",
|
|
413
|
+
hasDescriptionJson: false,
|
|
389
414
|
image: "",
|
|
390
415
|
hasImage: false,
|
|
391
416
|
noImage: true,
|
|
@@ -433,7 +458,8 @@ const svc = defineService<S, {
|
|
|
433
458
|
const id = s.id || inputId();
|
|
434
459
|
if (!id) return derive({ ...s, status: "ready", errored: true });
|
|
435
460
|
try {
|
|
436
|
-
|
|
461
|
+
// TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none).
|
|
462
|
+
const res = await getEvent(id, { fields: ["TEXTS"] });
|
|
437
463
|
let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
|
|
438
464
|
e = await enrichRegistration(id, e);
|
|
439
465
|
const attendees = await attendeeSummary(id);
|
|
@@ -460,7 +486,9 @@ const svc = defineService<S, {
|
|
|
460
486
|
initial: (title.trim()[0] || "E").toUpperCase(),
|
|
461
487
|
when: eventWhen(e),
|
|
462
488
|
location: eventLocation(e),
|
|
463
|
-
description
|
|
489
|
+
// The Ricos description when the event has one (its own text is the plain copy); the legacy
|
|
490
|
+
// string fields otherwise.
|
|
491
|
+
...eventBody(e),
|
|
464
492
|
image,
|
|
465
493
|
hasImage: !!image,
|
|
466
494
|
noImage: !image,
|
|
@@ -491,6 +519,9 @@ const svc = defineService<S, {
|
|
|
491
519
|
initial: (title.trim()[0] || "E").toUpperCase(),
|
|
492
520
|
when: cached.dateLine,
|
|
493
521
|
location: cached.location,
|
|
522
|
+
// The cached list row carries no description: an offline retry of THIS event keeps the pair it
|
|
523
|
+
// already shows, and any other event's is cleared together, so neither half can linger alone.
|
|
524
|
+
...(s.id === id ? {} : { description: "", descriptionJson: "" }),
|
|
494
525
|
image: cached.image,
|
|
495
526
|
hasImage: !!cached.image,
|
|
496
527
|
noImage: !cached.image,
|
|
@@ -665,6 +696,12 @@ const svc = defineService<S, {
|
|
|
665
696
|
return derive({ ...s, actionMessage: copyParam("event.calendarAddFailed", "Could not add this event to calendar."), isAddingCalendar: false });
|
|
666
697
|
}
|
|
667
698
|
},
|
|
699
|
+
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and a
|
|
700
|
+
// link that opens clears an earlier message.
|
|
701
|
+
openLink: async (s, arg) => derive({
|
|
702
|
+
...s,
|
|
703
|
+
actionMessage: (await openRichLink(arg)) ? "" : copyParam("event.linkOpenFailed", "This link could not be opened."),
|
|
704
|
+
}),
|
|
668
705
|
},
|
|
669
706
|
});
|
|
670
707
|
svc.load();
|
|
@@ -10,6 +10,8 @@ import { cleanTitle } from "../_shared/himi-text.js";
|
|
|
10
10
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
11
11
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
12
12
|
import { isProductSaved, toggleSavedProduct } from "../_shared/himi-store-saved.js";
|
|
13
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
14
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
13
15
|
|
|
14
16
|
// Offline-first (data-offline): the detail is a public READ-THROUGH — when the live
|
|
15
17
|
// product fetch fails (offline) fall back to the last-synced row the catalog cached
|
|
@@ -120,11 +122,27 @@ function optionsHint(groups: StoreOption[], selection: StoreOptionSelection): st
|
|
|
120
122
|
return copyTemplate("product.optionsChoose", "Choose {options}", { options: missing.join(", ") });
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/** The description pair. V3's `description` is a Ricos OBJECT (the HTML `plainText` would throw on it): it
|
|
126
|
+
* becomes the RichContent document, and `plainDescription` (or the document's own text) the plain copy. A V1
|
|
127
|
+
* `description` is an HTML string: stripped text, no document. */
|
|
128
|
+
function productBody(p: StoreProduct): { description: string; descriptionJson: string } {
|
|
129
|
+
const raw = (p as { description?: unknown }).description;
|
|
130
|
+
const html = plainText(p.plainDescription || (typeof raw === "string" ? raw : ""));
|
|
131
|
+
const { bodyJson, bodyText } = richBody(typeof raw === "string" ? null : raw, html);
|
|
132
|
+
return { description: html || bodyText, descriptionJson: bodyJson };
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
interface S {
|
|
124
136
|
status: "loading" | "ready" | "error";
|
|
125
137
|
name: string;
|
|
126
138
|
price: string;
|
|
139
|
+
/** The plain-text description: RichContent's fallback, and what shows with no document. */
|
|
127
140
|
description: string;
|
|
141
|
+
/** [rico] #3372 — a V3 product's Ricos `description` (read with `fields=DESCRIPTION`) as ONE document string
|
|
142
|
+
* for RichContent; "" for a V1 HTML description, none, or a document the encoder rejected. */
|
|
143
|
+
descriptionJson: string;
|
|
144
|
+
/** descriptionJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
145
|
+
hasDescriptionJson: boolean;
|
|
128
146
|
image: string;
|
|
129
147
|
/** FULL media gallery (media.items[] with the main image first) — the detail
|
|
130
148
|
* screen renders a Carousel over it when 2+ images exist. */
|
|
@@ -253,6 +271,7 @@ function derive(s: S): S {
|
|
|
253
271
|
const overflow = groups.length > OPTION_SLOTS;
|
|
254
272
|
return {
|
|
255
273
|
...s,
|
|
274
|
+
hasDescriptionJson: s.descriptionJson !== "",
|
|
256
275
|
...slotsOf(groups, s.selection),
|
|
257
276
|
hasOptions: groups.length > 0,
|
|
258
277
|
noOptions: groups.length === 0,
|
|
@@ -296,12 +315,16 @@ const svc = defineService<S, {
|
|
|
296
315
|
setCartCount: (s: S, arg?: unknown) => S;
|
|
297
316
|
share: (s: S) => Promise<S>;
|
|
298
317
|
toggleSave: (s: S) => S;
|
|
318
|
+
/** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
319
|
+
openLink: (s: S, arg?: unknown) => Promise<S>;
|
|
299
320
|
}>("himiStoreProduct", {
|
|
300
321
|
state: derive({
|
|
301
322
|
status: "loading",
|
|
302
323
|
name: "",
|
|
303
324
|
price: "",
|
|
304
325
|
description: "",
|
|
326
|
+
descriptionJson: "",
|
|
327
|
+
hasDescriptionJson: false,
|
|
305
328
|
image: "",
|
|
306
329
|
gallery: [],
|
|
307
330
|
hasGallery: false,
|
|
@@ -354,7 +377,7 @@ const svc = defineService<S, {
|
|
|
354
377
|
if (!id) {
|
|
355
378
|
lastProduct = undefined;
|
|
356
379
|
lastLoadedId = undefined;
|
|
357
|
-
return derive({ ...s, status: "ready", quantity: 1, optionGroups: [], selection: {}, optionsUnknown: false, emptyText: copyParam("product.notFoundText", "Product not found.") });
|
|
380
|
+
return derive({ ...s, status: "ready", quantity: 1, optionGroups: [], selection: {}, optionsUnknown: false, descriptionJson: "", emptyText: copyParam("product.notFoundText", "Product not found.") });
|
|
358
381
|
}
|
|
359
382
|
// The cart count only feeds the top-bar badge: ask for it NOW, beside the product, instead
|
|
360
383
|
// of after it — it used to add a whole cart round trip before the page could paint.
|
|
@@ -365,7 +388,8 @@ const svc = defineService<S, {
|
|
|
365
388
|
void cartCount.then((n) => { cartKnown = n; }, () => {});
|
|
366
389
|
// The live product read goes out FIRST too — the local-db read below must never delay it.
|
|
367
390
|
// Settled into a value so a failure is handled where it is awaited, never unhandled.
|
|
368
|
-
|
|
391
|
+
// DESCRIPTION widens the default fieldset with the V3 Ricos description (a V1 catalog ignores it).
|
|
392
|
+
const live = getStoreProduct(id, ["DESCRIPTION"]).then(
|
|
369
393
|
(r) => ({ ok: true as const, r }),
|
|
370
394
|
() => ({ ok: false as const }),
|
|
371
395
|
);
|
|
@@ -424,7 +448,7 @@ const svc = defineService<S, {
|
|
|
424
448
|
optionsUnknown: false,
|
|
425
449
|
name,
|
|
426
450
|
price: productPrice(p),
|
|
427
|
-
|
|
451
|
+
...productBody(p),
|
|
428
452
|
image,
|
|
429
453
|
gallery: productGallery(p),
|
|
430
454
|
initial: (name[0] || "#").toUpperCase(),
|
|
@@ -455,6 +479,7 @@ const svc = defineService<S, {
|
|
|
455
479
|
name,
|
|
456
480
|
price: String(synced.subtitle ?? ""),
|
|
457
481
|
description: "",
|
|
482
|
+
descriptionJson: "",
|
|
458
483
|
image,
|
|
459
484
|
initial: String(synced.initial ?? "") || (name[0] || "#").toUpperCase(),
|
|
460
485
|
cartHint: copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart."),
|
|
@@ -464,7 +489,7 @@ const svc = defineService<S, {
|
|
|
464
489
|
}
|
|
465
490
|
lastProduct = undefined;
|
|
466
491
|
lastLoadedId = undefined;
|
|
467
|
-
return derive({ ...s, status: "error", optionGroups: [], selection: {}, optionsUnknown: false, emptyText: copyParam("product.errorText", "Product could not load.") });
|
|
492
|
+
return derive({ ...s, status: "error", optionGroups: [], selection: {}, optionsUnknown: false, descriptionJson: "", emptyText: copyParam("product.errorText", "Product could not load.") });
|
|
468
493
|
},
|
|
469
494
|
// The stepper fires onChange with { value } on every tap; clamp 1–99 and
|
|
470
495
|
// ignore unparsable/garbage args instead of silently zeroing the quantity.
|
|
@@ -519,6 +544,7 @@ const svc = defineService<S, {
|
|
|
519
544
|
name,
|
|
520
545
|
price: String(row.subtitle ?? ""),
|
|
521
546
|
description: "",
|
|
547
|
+
descriptionJson: "",
|
|
522
548
|
image: String(row.image ?? ""),
|
|
523
549
|
gallery: [],
|
|
524
550
|
initial: String(row.initial ?? "") || (name[0] || "#").toUpperCase(),
|
|
@@ -605,6 +631,11 @@ const svc = defineService<S, {
|
|
|
605
631
|
return derive({ ...s, added: false, cartHint: copyParam("product.shareUnavailable", "Sharing is not available on this device.") });
|
|
606
632
|
}
|
|
607
633
|
},
|
|
634
|
+
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so in the
|
|
635
|
+
// hint line (`added: false`, as for share, so it never renders inside the add-to-cart confirmation).
|
|
636
|
+
openLink: async (s, arg) => ((await openRichLink(arg))
|
|
637
|
+
? s
|
|
638
|
+
: derive({ ...s, added: false, cartHint: copyParam("product.linkOpenFailed", "This link could not be opened.") })),
|
|
608
639
|
},
|
|
609
640
|
});
|
|
610
641
|
|
|
@@ -14,15 +14,21 @@ export interface RichBody {
|
|
|
14
14
|
bodyText: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
|
|
18
|
+
* list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
|
|
19
|
+
export const ROW_IMAGE_WIDTH = 1000;
|
|
20
|
+
|
|
17
21
|
/** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
|
|
18
22
|
* that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
|
|
19
23
|
* gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
|
|
20
|
-
* plain-text walk has no depth limit), so `fallback` stands in for it.
|
|
21
|
-
|
|
24
|
+
* plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
|
|
25
|
+
* the 1400 px detail budget). */
|
|
26
|
+
export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
|
|
22
27
|
const doc = parseRicosJson(richContent);
|
|
23
28
|
if (!doc) return { bodyJson: "", bodyText: fallback };
|
|
24
|
-
// 1400 px — the detail-image budget, which is also the encoder's maximum
|
|
25
|
-
|
|
29
|
+
// 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
|
|
30
|
+
// ROW_IMAGE_WIDTH.
|
|
31
|
+
const { json, rejected } = encodeRichContent(doc, { imageWidth });
|
|
26
32
|
const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
|
|
27
33
|
return { bodyJson: json, bodyText: text || fallback };
|
|
28
34
|
}
|
|
@@ -309,7 +309,12 @@ async function measuredRevenue7d(
|
|
|
309
309
|
if (typeof measure.total === "number") return measure.total;
|
|
310
310
|
const values = measure.values ?? [];
|
|
311
311
|
return values.length ? values.reduce((sum, v) => sum + (v.value || 0), 0) : undefined;
|
|
312
|
-
} catch {
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// The fallback is silent on screen (Home shows the order-row sum), so say it where
|
|
314
|
+
// an agent looks: the DEV panel's Console tab and `himi logs`. The status only — a Wix
|
|
315
|
+
// failure's message carries part of the upstream body, which may hold personal data.
|
|
316
|
+
const status = (e as { status?: unknown } | null)?.status ?? /\bHTTP (\d{3})\b/.exec(String(e))?.[1];
|
|
317
|
+
console.warn("[home.revenue] analytics failed; showing the order-row sum", status !== undefined ? `HTTP ${String(status)}` : "");
|
|
313
318
|
return undefined;
|
|
314
319
|
}
|
|
315
320
|
}
|
|
@@ -16553,7 +16553,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
16553
16553
|
}
|
|
16554
16554
|
},
|
|
16555
16555
|
{
|
|
16556
|
-
"id": "",
|
|
16556
|
+
"id": "event_body",
|
|
16557
|
+
"body": {
|
|
16558
|
+
"package": "himalaya",
|
|
16559
|
+
"type": "RichContent",
|
|
16560
|
+
"props": {
|
|
16561
|
+
"documentJsonPath": "$service:himiEventDetail.descriptionJson",
|
|
16562
|
+
"onLink": {
|
|
16563
|
+
"actionId": "tier5.himiEventDetail.openLink"
|
|
16564
|
+
},
|
|
16565
|
+
"fallback": {
|
|
16566
|
+
"id": "",
|
|
16567
|
+
"body": {
|
|
16568
|
+
"package": "himalaya",
|
|
16569
|
+
"type": "Text",
|
|
16570
|
+
"props": {
|
|
16571
|
+
"text": "$service:himiEventDetail.description",
|
|
16572
|
+
"style": "body",
|
|
16573
|
+
"color": "textSecondary",
|
|
16574
|
+
"collapseWhenEmpty": true
|
|
16575
|
+
}
|
|
16576
|
+
}
|
|
16577
|
+
}
|
|
16578
|
+
}
|
|
16579
|
+
}
|
|
16580
|
+
},
|
|
16581
|
+
{
|
|
16582
|
+
"id": "event_body_text",
|
|
16557
16583
|
"body": {
|
|
16558
16584
|
"package": "himalaya",
|
|
16559
16585
|
"type": "Text",
|
|
@@ -16563,6 +16589,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
16563
16589
|
"color": "textSecondary",
|
|
16564
16590
|
"collapseWhenEmpty": true
|
|
16565
16591
|
}
|
|
16592
|
+
},
|
|
16593
|
+
"modifiers": {
|
|
16594
|
+
"hiddenPath": "$service:himiEventDetail.hasDescriptionJson"
|
|
16566
16595
|
}
|
|
16567
16596
|
}
|
|
16568
16597
|
]
|
|
@@ -18457,7 +18486,33 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
18457
18486
|
}
|
|
18458
18487
|
},
|
|
18459
18488
|
{
|
|
18460
|
-
"id": "",
|
|
18489
|
+
"id": "comment_body_$item.id",
|
|
18490
|
+
"body": {
|
|
18491
|
+
"package": "himalaya",
|
|
18492
|
+
"type": "RichContent",
|
|
18493
|
+
"props": {
|
|
18494
|
+
"documentJsonPath": "$item.bodyJson",
|
|
18495
|
+
"onLink": {
|
|
18496
|
+
"actionId": "tier5.himiBlogPost.openLink"
|
|
18497
|
+
},
|
|
18498
|
+
"fallback": {
|
|
18499
|
+
"id": "",
|
|
18500
|
+
"body": {
|
|
18501
|
+
"package": "himalaya",
|
|
18502
|
+
"type": "Text",
|
|
18503
|
+
"props": {
|
|
18504
|
+
"text": "$item.text",
|
|
18505
|
+
"style": "body",
|
|
18506
|
+
"color": "textSecondary",
|
|
18507
|
+
"collapseWhenEmpty": true
|
|
18508
|
+
}
|
|
18509
|
+
}
|
|
18510
|
+
}
|
|
18511
|
+
}
|
|
18512
|
+
}
|
|
18513
|
+
},
|
|
18514
|
+
{
|
|
18515
|
+
"id": "comment_body_text_$item.id",
|
|
18461
18516
|
"body": {
|
|
18462
18517
|
"package": "himalaya",
|
|
18463
18518
|
"type": "Text",
|
|
@@ -18467,6 +18522,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
18467
18522
|
"color": "textSecondary",
|
|
18468
18523
|
"collapseWhenEmpty": true
|
|
18469
18524
|
}
|
|
18525
|
+
},
|
|
18526
|
+
"modifiers": {
|
|
18527
|
+
"hiddenPath": "$item.hasBodyJson"
|
|
18470
18528
|
}
|
|
18471
18529
|
}
|
|
18472
18530
|
]
|
|
@@ -14,15 +14,21 @@ export interface RichBody {
|
|
|
14
14
|
bodyText: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** [rico] #3372 — the media width for a document drawn in a LIST row (a feed post, a comment, a CMS item): the
|
|
18
|
+
* list-image size (WIX_LIST_IMAGE), so a scrolling list never downloads every image at detail size. */
|
|
19
|
+
export const ROW_IMAGE_WIDTH = 1000;
|
|
20
|
+
|
|
17
21
|
/** Prepares `richContent` (a Ricos document, as a Wix API returns it, or its JSON string). No document, or one
|
|
18
22
|
* that is not a document, gives `{ bodyJson: "", bodyText: fallback }`. A document past the viewer's limits
|
|
19
23
|
* gives bodyJson "" and its plain text — except one rejected as too deep, which is not walked again (the
|
|
20
|
-
* plain-text walk has no depth limit), so `fallback` stands in for it.
|
|
21
|
-
|
|
24
|
+
* plain-text walk has no depth limit), so `fallback` stands in for it. `imageWidth` sizes its media (default
|
|
25
|
+
* the 1400 px detail budget). */
|
|
26
|
+
export function richBody(richContent: unknown, fallback = "", imageWidth = 1400): RichBody {
|
|
22
27
|
const doc = parseRicosJson(richContent);
|
|
23
28
|
if (!doc) return { bodyJson: "", bodyText: fallback };
|
|
24
|
-
// 1400 px — the detail-image budget, which is also the encoder's maximum
|
|
25
|
-
|
|
29
|
+
// 1400 px by default — the detail-image budget, which is also the encoder's maximum; list rows pass
|
|
30
|
+
// ROW_IMAGE_WIDTH.
|
|
31
|
+
const { json, rejected } = encodeRichContent(doc, { imageWidth });
|
|
26
32
|
const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
|
|
27
33
|
return { bodyJson: json, bodyText: text || fallback };
|
|
28
34
|
}
|