@wix/himalaya-cli 0.1151.0 → 0.1153.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/bookings-v2/src/extensions.d.ts +33 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/wix-data/src/index.d.ts +14 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bookings.mjs +71 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-data.mjs +3 -2
- package/dist/cli.mjs +141 -29
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/cms-collections.md +4 -1
- package/guides/getting-started.md +3 -1
- package/guides/himi-skill.md +3 -1
- package/guides/owner-lite-site-binding.md +26 -0
- package/guides/push-cli-onboarding.md +42 -1
- package/guides/sdk-reference.json +23 -11
- package/guides/templates.md +1 -0
- package/guides/wix-api-usage.md +5 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +6 -3
- package/templates/community-club/dev/index.ts +6 -3
- package/templates/community-club-owner/dev/index.ts +44 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/events-meetups/dev/index.ts +6 -3
- package/templates/events-meetups-owner/dev/index.ts +44 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/gym-fitness/dev/index.ts +6 -3
- package/templates/gym-fitness-owner/dev/index.ts +44 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/nonprofit-cause/dev/index.ts +6 -3
- package/templates/nonprofit-cause-owner/dev/index.ts +44 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/owner-lite/dev/index.ts +44 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/podcast-creator/dev/index.ts +6 -3
- package/templates/podcast-creator-owner/dev/index.ts +44 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/portfolio-creative/dev/index.ts +6 -3
- package/templates/restaurant/dev/index.ts +6 -3
- package/templates/restaurant-owner/dev/index.ts +44 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
- package/templates/retail-store/dev/index.ts +7 -3
- package/templates/yoga-studio/dev/index.ts +6 -3
- package/templates/yoga-studio-owner/dev/index.ts +44 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +52 -8
|
@@ -11,7 +11,9 @@ import { normalizeEvent, eventDescription } from "../_shared/himi-events.js";
|
|
|
11
11
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
12
12
|
import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
|
|
13
13
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
14
|
-
import {
|
|
14
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
15
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
16
|
+
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
15
17
|
|
|
16
18
|
// Offline-first (data-offline): the event DETAIL falls back to the last-synced
|
|
17
19
|
// local `owner_events` row (put by the event form's write-behind) on a failed
|
|
@@ -20,6 +22,7 @@ import { get as dbGet, remove as dbRemove, enqueue as dbEnqueue, type DbEntity }
|
|
|
20
22
|
// ENQUEUED for replay on reconnect. Every local-db call is wrapped so a build
|
|
21
23
|
// without the `local_database` grant behaves exactly as before (online-only).
|
|
22
24
|
const ENTITY = "owner_events";
|
|
25
|
+
async function cachePut(row: DbEntity): Promise<void> { try { await dbUpsert(ENTITY, row); } catch { /* capability not granted */ } }
|
|
23
26
|
async function cacheDrop(id: string): Promise<void> { try { await dbRemove(ENTITY, id); } catch { /* capability not granted */ } }
|
|
24
27
|
async function cacheGet(id: string): Promise<DbEntity | null> { try { return await dbGet(ENTITY, id); } catch { return null; } }
|
|
25
28
|
async function queueWrite(actionId: string, params: Record<string, string>): Promise<void> { try { await dbEnqueue(actionId, params); } catch { /* capability not granted */ } }
|
|
@@ -33,6 +36,13 @@ interface EventState {
|
|
|
33
36
|
title: string;
|
|
34
37
|
subtitle: string;
|
|
35
38
|
summary: string;
|
|
39
|
+
// Body ([rico] #3499) — RichContent binds bodyJson; the plain text shows when there is no document.
|
|
40
|
+
/** The v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string; "" when there is none. */
|
|
41
|
+
bodyJson: string;
|
|
42
|
+
/** bodyJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
43
|
+
hasBodyJson: boolean;
|
|
44
|
+
/** The description's plain text (the document's own text, else the legacy description). */
|
|
45
|
+
bodyText: string;
|
|
36
46
|
initial: string;
|
|
37
47
|
image: string;
|
|
38
48
|
dateLine: string;
|
|
@@ -55,6 +65,17 @@ interface EventState {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
68
|
+
/** [rico] #3499 — keep the description's plain text with the event's cached row, so the offline detail shows it.
|
|
69
|
+
* Merged into the row the events list (or the form) wrote rather than replacing it; with no row yet, the
|
|
70
|
+
* loaded detail's own fields become one. */
|
|
71
|
+
async function cacheBodyText(s: EventState): Promise<void> {
|
|
72
|
+
if (s.status !== "ready" || !s.id) return;
|
|
73
|
+
const prev = await cacheGet(s.id);
|
|
74
|
+
await cachePut(prev ? { ...prev, bodyText: s.bodyText } : {
|
|
75
|
+
id: s.id, title: s.title, subtitle: s.subtitle, image: s.image, dateLine: s.dateLine, location: s.location,
|
|
76
|
+
statusLabel: s.statusLabel, registration: s.registration, bodyText: s.bodyText,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
58
79
|
function idFrom(arg: unknown, fallback = ""): string {
|
|
59
80
|
return typeof arg === "string" ? arg : String((arg as any)?.id ?? fallback);
|
|
60
81
|
}
|
|
@@ -97,6 +118,9 @@ function shape(s: Partial<EventState>): EventState {
|
|
|
97
118
|
title: s.title ?? "",
|
|
98
119
|
subtitle: s.subtitle ?? "",
|
|
99
120
|
summary: s.summary ?? "",
|
|
121
|
+
bodyJson: s.bodyJson ?? "",
|
|
122
|
+
hasBodyJson: !!s.bodyJson,
|
|
123
|
+
bodyText: s.bodyText ?? "",
|
|
100
124
|
initial: s.initial ?? "?",
|
|
101
125
|
image,
|
|
102
126
|
dateLine: s.dateLine ?? "—",
|
|
@@ -128,7 +152,9 @@ function demoDetail(s: EventState, id: string): EventState | null {
|
|
|
128
152
|
status: "ready",
|
|
129
153
|
title: d.title,
|
|
130
154
|
subtitle: [d.dateLine, d.location].filter(Boolean).join(" · "),
|
|
131
|
-
summary:
|
|
155
|
+
summary: "",
|
|
156
|
+
bodyJson: "",
|
|
157
|
+
bodyText: d.description,
|
|
132
158
|
initial: (d.title[0] || "?").toUpperCase(),
|
|
133
159
|
image: "",
|
|
134
160
|
dateLine: d.dateLine,
|
|
@@ -151,7 +177,9 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
151
177
|
status: "ready",
|
|
152
178
|
title,
|
|
153
179
|
subtitle: String(row.subtitle ?? [dateLine, location].filter((x) => x && x !== "—").join(" · ")),
|
|
154
|
-
summary:
|
|
180
|
+
summary: "",
|
|
181
|
+
bodyJson: "",
|
|
182
|
+
bodyText: String(row.bodyText ?? row.summary ?? row.description ?? ""),
|
|
155
183
|
initial: String(row.initial ?? (title[0] || "?")).slice(0, 1).toUpperCase(),
|
|
156
184
|
image: String(row.image ?? ""),
|
|
157
185
|
dateLine: dateLine || "—",
|
|
@@ -164,21 +192,27 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
async function refresh(prev: EventState, id: string): Promise<EventState> {
|
|
167
|
-
|
|
195
|
+
// A bare read returns the base fieldset: DETAILS adds `shortDescription` (the teaser and the legacy body),
|
|
196
|
+
// TEXTS the Ricos `description`.
|
|
197
|
+
const res = await getEvent(id, { fields: ["DETAILS", "TEXTS"] });
|
|
168
198
|
const ev = (res.event ?? res) as WixEvent;
|
|
169
199
|
if (!ev || !(ev as any).id) {
|
|
170
200
|
return shape({ ...prev, id, status: "error", title: "", error: copyParam("ownerEvent.errorNotFound", "event not found") });
|
|
171
201
|
}
|
|
172
202
|
const row = normalizeEvent(ev); // reuses date/location/status normalization
|
|
173
203
|
const image = /^https?:\/\//.test(row.image) ? row.image : "";
|
|
174
|
-
const
|
|
204
|
+
const { bodyJson, bodyText } = richBody((ev as unknown as { description?: unknown }).description, (eventDescription(ev) || "").trim());
|
|
205
|
+
// The teaser stays above the body only when it says something the body does not.
|
|
206
|
+
const teaser = String((ev as any).shortDescription ?? "").trim();
|
|
175
207
|
return shape({
|
|
176
208
|
...prev,
|
|
177
209
|
id,
|
|
178
210
|
status: "ready",
|
|
179
211
|
title: row.title,
|
|
180
212
|
subtitle: [row.dateLine, row.location].filter((x) => x && x !== "—").join(" · "),
|
|
181
|
-
summary:
|
|
213
|
+
summary: teaser && teaser !== bodyText.trim() ? teaser : "",
|
|
214
|
+
bodyJson,
|
|
215
|
+
bodyText,
|
|
182
216
|
initial: (row.title[0] || "?").toUpperCase(),
|
|
183
217
|
image,
|
|
184
218
|
dateLine: row.dateLine || "—",
|
|
@@ -194,6 +228,7 @@ const svc = defineService<EventState, {
|
|
|
194
228
|
load: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
195
229
|
retry: (s: EventState) => Promise<EventState>;
|
|
196
230
|
delete: (s: EventState) => Promise<EventState>;
|
|
231
|
+
openLink: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
197
232
|
}>("himiOwnerLiteEvent", {
|
|
198
233
|
state: shape({ status: "loading" }),
|
|
199
234
|
actions: {
|
|
@@ -208,7 +243,9 @@ const svc = defineService<EventState, {
|
|
|
208
243
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
209
244
|
}
|
|
210
245
|
try {
|
|
211
|
-
|
|
246
|
+
const loaded = await refresh(shape({ ...s, id, status: "loading" }), id);
|
|
247
|
+
await cacheBodyText(loaded);
|
|
248
|
+
return loaded;
|
|
212
249
|
} catch (e) {
|
|
213
250
|
// Offline: fall back to the last-synced local row before the demo/error paths.
|
|
214
251
|
const cached = await cacheGet(id);
|
|
@@ -229,7 +266,9 @@ const svc = defineService<EventState, {
|
|
|
229
266
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
230
267
|
}
|
|
231
268
|
try {
|
|
232
|
-
|
|
269
|
+
const loaded = await refresh(shape({ ...s, status: "loading" }), s.id);
|
|
270
|
+
await cacheBodyText(loaded);
|
|
271
|
+
return loaded;
|
|
233
272
|
} catch (e) {
|
|
234
273
|
const cached = await cacheGet(s.id);
|
|
235
274
|
if (cached) return cachedDetail(s, cached);
|
|
@@ -256,6 +295,11 @@ const svc = defineService<EventState, {
|
|
|
256
295
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
257
296
|
}
|
|
258
297
|
},
|
|
298
|
+
// [rico] #3499 — a link tapped in the RichContent description.
|
|
299
|
+
openLink: async (s, arg) => shape({
|
|
300
|
+
...s,
|
|
301
|
+
actionMessage: (await openRichLink(arg)) ? "" : copyParam("ownerEvent.linkOpenFailed", "This link could not be opened."),
|
|
302
|
+
}),
|
|
259
303
|
},
|
|
260
304
|
});
|
|
261
305
|
|
|
@@ -2424,7 +2424,7 @@ const SCREENS = runtimeSetting({
|
|
|
2424
2424
|
"verticals": "[\"store\",\"bookings\",\"plans\",\"events\",\"blog\"]",
|
|
2425
2425
|
"homeModules": "[{\"id\":\"member_progress\",\"label\":\"This week's schedule\",\"dataSource\":\"member.bookings.schedule\",\"emptyWhen\":[\"hideMemberProgress\"]},{\"id\":\"up_next\",\"label\":\"Up next\",\"dataSource\":\"member.bookings\",\"emptyWhen\":[\"hideNextBooking\",\"hidePersonalError\"]},{\"id\":\"saved\",\"label\":\"Saved on this device\",\"dataSource\":\"device.saved_items\",\"emptyWhen\":[\"noSavedContent\"]},{\"id\":\"section_0\",\"label\":\"Discover 1\",\"dataSource\":\"trainingFirst.section0\",\"emptyWhen\":[\"hideHeroImage\"]},{\"id\":\"section_1\",\"label\":\"__HIMI_APP_NAME__\",\"dataSource\":\"trainingFirst.section1\"}]",
|
|
2426
2426
|
"ownerDefaultLayout": "trainingFirst",
|
|
2427
|
-
"copy": "{\"home.storeFeaturedTitle\":\"Shop the gear\"}"
|
|
2427
|
+
"copy": "{\"home.profileBody\":\"Sign in for orders, bookings, RSVPs, memberships and saved posts.\",\"home.storeFeaturedTitle\":\"Shop the gear\"}"
|
|
2428
2428
|
}
|
|
2429
2429
|
}
|
|
2430
2430
|
}
|
|
@@ -2475,7 +2475,7 @@ const SCREENS = runtimeSetting({
|
|
|
2475
2475
|
"package": "himalaya",
|
|
2476
2476
|
"type": "Text",
|
|
2477
2477
|
"props": {
|
|
2478
|
-
"text": "Orders, bookings, RSVPs and saved content in one place.",
|
|
2478
|
+
"text": "Orders, bookings, RSVPs, memberships and saved content in one place.",
|
|
2479
2479
|
"style": "body",
|
|
2480
2480
|
"color": "textSecondary"
|
|
2481
2481
|
}
|
|
@@ -4052,7 +4052,10 @@ const SCREENS = runtimeSetting({
|
|
|
4052
4052
|
"bottom": 28
|
|
4053
4053
|
},
|
|
4054
4054
|
"onAppear": {
|
|
4055
|
-
"actionId": "tier5.himiStoreAccount.refresh"
|
|
4055
|
+
"actionId": "tier5.himiStoreAccount.refresh",
|
|
4056
|
+
"params": {
|
|
4057
|
+
"copy": "{\"account.subtitle\":\"Orders, bookings, RSVPs, memberships and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — cart, bookings, RSVPs and memberships stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — cart, bookings, RSVPs and memberships stay with you.\"}"
|
|
4058
|
+
}
|
|
4056
4059
|
}
|
|
4057
4060
|
}
|
|
4058
4061
|
},
|
|
@@ -81260,6 +81260,48 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
81260
81260
|
}
|
|
81261
81261
|
}
|
|
81262
81262
|
},
|
|
81263
|
+
{
|
|
81264
|
+
"id": "event_body",
|
|
81265
|
+
"body": {
|
|
81266
|
+
"package": "himalaya",
|
|
81267
|
+
"type": "RichContent",
|
|
81268
|
+
"props": {
|
|
81269
|
+
"documentJsonPath": "$service:himiOwnerLiteEvent.bodyJson",
|
|
81270
|
+
"onLink": {
|
|
81271
|
+
"actionId": "tier5.himiOwnerLiteEvent.openLink"
|
|
81272
|
+
},
|
|
81273
|
+
"fallback": {
|
|
81274
|
+
"id": "",
|
|
81275
|
+
"body": {
|
|
81276
|
+
"package": "himalaya",
|
|
81277
|
+
"type": "Text",
|
|
81278
|
+
"props": {
|
|
81279
|
+
"text": "$service:himiOwnerLiteEvent.bodyText",
|
|
81280
|
+
"style": "body",
|
|
81281
|
+
"color": "textPrimary",
|
|
81282
|
+
"collapseWhenEmpty": true
|
|
81283
|
+
}
|
|
81284
|
+
}
|
|
81285
|
+
}
|
|
81286
|
+
}
|
|
81287
|
+
}
|
|
81288
|
+
},
|
|
81289
|
+
{
|
|
81290
|
+
"id": "event_body_text",
|
|
81291
|
+
"body": {
|
|
81292
|
+
"package": "himalaya",
|
|
81293
|
+
"type": "Text",
|
|
81294
|
+
"props": {
|
|
81295
|
+
"text": "$service:himiOwnerLiteEvent.bodyText",
|
|
81296
|
+
"style": "body",
|
|
81297
|
+
"color": "textPrimary",
|
|
81298
|
+
"collapseWhenEmpty": true
|
|
81299
|
+
}
|
|
81300
|
+
},
|
|
81301
|
+
"modifiers": {
|
|
81302
|
+
"hiddenPath": "$service:himiOwnerLiteEvent.hasBodyJson"
|
|
81303
|
+
}
|
|
81304
|
+
},
|
|
81263
81305
|
{
|
|
81264
81306
|
"id": "event_info_card",
|
|
81265
81307
|
"body": {
|
|
@@ -81598,7 +81640,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
81598
81640
|
"contentType": "application/javascript+ts-bundle",
|
|
81599
81641
|
"declaredCapabilities": [
|
|
81600
81642
|
"wix_api",
|
|
81601
|
-
"local_database"
|
|
81643
|
+
"local_database",
|
|
81644
|
+
"open_url"
|
|
81602
81645
|
],
|
|
81603
81646
|
"minBinaryVersion": "0.1.0",
|
|
81604
81647
|
"bundleVersion": "0.0.1"
|
|
@@ -11,7 +11,9 @@ import { normalizeEvent, eventDescription } from "../_shared/himi-events.js";
|
|
|
11
11
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
12
12
|
import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
|
|
13
13
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
14
|
-
import {
|
|
14
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
15
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
16
|
+
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
15
17
|
|
|
16
18
|
// Offline-first (data-offline): the event DETAIL falls back to the last-synced
|
|
17
19
|
// local `owner_events` row (put by the event form's write-behind) on a failed
|
|
@@ -20,6 +22,7 @@ import { get as dbGet, remove as dbRemove, enqueue as dbEnqueue, type DbEntity }
|
|
|
20
22
|
// ENQUEUED for replay on reconnect. Every local-db call is wrapped so a build
|
|
21
23
|
// without the `local_database` grant behaves exactly as before (online-only).
|
|
22
24
|
const ENTITY = "owner_events";
|
|
25
|
+
async function cachePut(row: DbEntity): Promise<void> { try { await dbUpsert(ENTITY, row); } catch { /* capability not granted */ } }
|
|
23
26
|
async function cacheDrop(id: string): Promise<void> { try { await dbRemove(ENTITY, id); } catch { /* capability not granted */ } }
|
|
24
27
|
async function cacheGet(id: string): Promise<DbEntity | null> { try { return await dbGet(ENTITY, id); } catch { return null; } }
|
|
25
28
|
async function queueWrite(actionId: string, params: Record<string, string>): Promise<void> { try { await dbEnqueue(actionId, params); } catch { /* capability not granted */ } }
|
|
@@ -33,6 +36,13 @@ interface EventState {
|
|
|
33
36
|
title: string;
|
|
34
37
|
subtitle: string;
|
|
35
38
|
summary: string;
|
|
39
|
+
// Body ([rico] #3499) — RichContent binds bodyJson; the plain text shows when there is no document.
|
|
40
|
+
/** The v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string; "" when there is none. */
|
|
41
|
+
bodyJson: string;
|
|
42
|
+
/** bodyJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
43
|
+
hasBodyJson: boolean;
|
|
44
|
+
/** The description's plain text (the document's own text, else the legacy description). */
|
|
45
|
+
bodyText: string;
|
|
36
46
|
initial: string;
|
|
37
47
|
image: string;
|
|
38
48
|
dateLine: string;
|
|
@@ -55,6 +65,17 @@ interface EventState {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
68
|
+
/** [rico] #3499 — keep the description's plain text with the event's cached row, so the offline detail shows it.
|
|
69
|
+
* Merged into the row the events list (or the form) wrote rather than replacing it; with no row yet, the
|
|
70
|
+
* loaded detail's own fields become one. */
|
|
71
|
+
async function cacheBodyText(s: EventState): Promise<void> {
|
|
72
|
+
if (s.status !== "ready" || !s.id) return;
|
|
73
|
+
const prev = await cacheGet(s.id);
|
|
74
|
+
await cachePut(prev ? { ...prev, bodyText: s.bodyText } : {
|
|
75
|
+
id: s.id, title: s.title, subtitle: s.subtitle, image: s.image, dateLine: s.dateLine, location: s.location,
|
|
76
|
+
statusLabel: s.statusLabel, registration: s.registration, bodyText: s.bodyText,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
58
79
|
function idFrom(arg: unknown, fallback = ""): string {
|
|
59
80
|
return typeof arg === "string" ? arg : String((arg as any)?.id ?? fallback);
|
|
60
81
|
}
|
|
@@ -97,6 +118,9 @@ function shape(s: Partial<EventState>): EventState {
|
|
|
97
118
|
title: s.title ?? "",
|
|
98
119
|
subtitle: s.subtitle ?? "",
|
|
99
120
|
summary: s.summary ?? "",
|
|
121
|
+
bodyJson: s.bodyJson ?? "",
|
|
122
|
+
hasBodyJson: !!s.bodyJson,
|
|
123
|
+
bodyText: s.bodyText ?? "",
|
|
100
124
|
initial: s.initial ?? "?",
|
|
101
125
|
image,
|
|
102
126
|
dateLine: s.dateLine ?? "—",
|
|
@@ -128,7 +152,9 @@ function demoDetail(s: EventState, id: string): EventState | null {
|
|
|
128
152
|
status: "ready",
|
|
129
153
|
title: d.title,
|
|
130
154
|
subtitle: [d.dateLine, d.location].filter(Boolean).join(" · "),
|
|
131
|
-
summary:
|
|
155
|
+
summary: "",
|
|
156
|
+
bodyJson: "",
|
|
157
|
+
bodyText: d.description,
|
|
132
158
|
initial: (d.title[0] || "?").toUpperCase(),
|
|
133
159
|
image: "",
|
|
134
160
|
dateLine: d.dateLine,
|
|
@@ -151,7 +177,9 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
151
177
|
status: "ready",
|
|
152
178
|
title,
|
|
153
179
|
subtitle: String(row.subtitle ?? [dateLine, location].filter((x) => x && x !== "—").join(" · ")),
|
|
154
|
-
summary:
|
|
180
|
+
summary: "",
|
|
181
|
+
bodyJson: "",
|
|
182
|
+
bodyText: String(row.bodyText ?? row.summary ?? row.description ?? ""),
|
|
155
183
|
initial: String(row.initial ?? (title[0] || "?")).slice(0, 1).toUpperCase(),
|
|
156
184
|
image: String(row.image ?? ""),
|
|
157
185
|
dateLine: dateLine || "—",
|
|
@@ -164,21 +192,27 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
async function refresh(prev: EventState, id: string): Promise<EventState> {
|
|
167
|
-
|
|
195
|
+
// A bare read returns the base fieldset: DETAILS adds `shortDescription` (the teaser and the legacy body),
|
|
196
|
+
// TEXTS the Ricos `description`.
|
|
197
|
+
const res = await getEvent(id, { fields: ["DETAILS", "TEXTS"] });
|
|
168
198
|
const ev = (res.event ?? res) as WixEvent;
|
|
169
199
|
if (!ev || !(ev as any).id) {
|
|
170
200
|
return shape({ ...prev, id, status: "error", title: "", error: copyParam("ownerEvent.errorNotFound", "event not found") });
|
|
171
201
|
}
|
|
172
202
|
const row = normalizeEvent(ev); // reuses date/location/status normalization
|
|
173
203
|
const image = /^https?:\/\//.test(row.image) ? row.image : "";
|
|
174
|
-
const
|
|
204
|
+
const { bodyJson, bodyText } = richBody((ev as unknown as { description?: unknown }).description, (eventDescription(ev) || "").trim());
|
|
205
|
+
// The teaser stays above the body only when it says something the body does not.
|
|
206
|
+
const teaser = String((ev as any).shortDescription ?? "").trim();
|
|
175
207
|
return shape({
|
|
176
208
|
...prev,
|
|
177
209
|
id,
|
|
178
210
|
status: "ready",
|
|
179
211
|
title: row.title,
|
|
180
212
|
subtitle: [row.dateLine, row.location].filter((x) => x && x !== "—").join(" · "),
|
|
181
|
-
summary:
|
|
213
|
+
summary: teaser && teaser !== bodyText.trim() ? teaser : "",
|
|
214
|
+
bodyJson,
|
|
215
|
+
bodyText,
|
|
182
216
|
initial: (row.title[0] || "?").toUpperCase(),
|
|
183
217
|
image,
|
|
184
218
|
dateLine: row.dateLine || "—",
|
|
@@ -194,6 +228,7 @@ const svc = defineService<EventState, {
|
|
|
194
228
|
load: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
195
229
|
retry: (s: EventState) => Promise<EventState>;
|
|
196
230
|
delete: (s: EventState) => Promise<EventState>;
|
|
231
|
+
openLink: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
197
232
|
}>("himiOwnerLiteEvent", {
|
|
198
233
|
state: shape({ status: "loading" }),
|
|
199
234
|
actions: {
|
|
@@ -208,7 +243,9 @@ const svc = defineService<EventState, {
|
|
|
208
243
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
209
244
|
}
|
|
210
245
|
try {
|
|
211
|
-
|
|
246
|
+
const loaded = await refresh(shape({ ...s, id, status: "loading" }), id);
|
|
247
|
+
await cacheBodyText(loaded);
|
|
248
|
+
return loaded;
|
|
212
249
|
} catch (e) {
|
|
213
250
|
// Offline: fall back to the last-synced local row before the demo/error paths.
|
|
214
251
|
const cached = await cacheGet(id);
|
|
@@ -229,7 +266,9 @@ const svc = defineService<EventState, {
|
|
|
229
266
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
230
267
|
}
|
|
231
268
|
try {
|
|
232
|
-
|
|
269
|
+
const loaded = await refresh(shape({ ...s, status: "loading" }), s.id);
|
|
270
|
+
await cacheBodyText(loaded);
|
|
271
|
+
return loaded;
|
|
233
272
|
} catch (e) {
|
|
234
273
|
const cached = await cacheGet(s.id);
|
|
235
274
|
if (cached) return cachedDetail(s, cached);
|
|
@@ -256,6 +295,11 @@ const svc = defineService<EventState, {
|
|
|
256
295
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
257
296
|
}
|
|
258
297
|
},
|
|
298
|
+
// [rico] #3499 — a link tapped in the RichContent description.
|
|
299
|
+
openLink: async (s, arg) => shape({
|
|
300
|
+
...s,
|
|
301
|
+
actionMessage: (await openRichLink(arg)) ? "" : copyParam("ownerEvent.linkOpenFailed", "This link could not be opened."),
|
|
302
|
+
}),
|
|
259
303
|
},
|
|
260
304
|
});
|
|
261
305
|
|
|
@@ -2078,7 +2078,7 @@ const SCREENS = runtimeSetting({
|
|
|
2078
2078
|
"verticals": "[\"plans\",\"feed\",\"events\",\"blog\",\"groups\"]",
|
|
2079
2079
|
"homeModules": "[{\"id\":\"saved\",\"label\":\"Saved on this device\",\"dataSource\":\"device.saved_items\",\"emptyWhen\":[\"noSavedContent\"]},{\"id\":\"section_0\",\"label\":\"Discover 1\",\"dataSource\":\"communityPulse.section0\",\"emptyWhen\":[\"noMemberAvatars\"]},{\"id\":\"section_1\",\"label\":\"From the field\",\"dataSource\":\"communityPulse.section1\",\"emptyWhen\":[\"noAnnouncement\",\"noUpcomingEvents\"]},{\"id\":\"section_2\",\"label\":\"Chapters & teams\",\"dataSource\":\"communityPulse.section2\",\"emptyWhen\":[\"noGroupCards\"]},{\"id\":\"section_3\",\"label\":\"Latest from the community\",\"dataSource\":\"communityPulse.section3\"}]",
|
|
2080
2080
|
"ownerDefaultLayout": "communityPulse",
|
|
2081
|
-
"copy": "{\"home.activityTitle\":\"Latest from the community\",\"home.announcementCaption\":\"From the field\",\"home.eventsTitle\":\"Volunteer opportunities\",\"home.groupsSubtitle\":\"Find your cause and pitch in.\",\"home.groupsTitle\":\"Chapters & teams\",\"home.joinConversationCta\":\"Share your story\",\"home.membersCaption\":\"Our volunteers\",\"home.onboardingBody\":\"Find a way to give, sign up to volunteer, and follow the mission's impact — all in one place.\"}"
|
|
2081
|
+
"copy": "{\"home.activityTitle\":\"Latest from the community\",\"home.announcementCaption\":\"From the field\",\"home.eventsTitle\":\"Volunteer opportunities\",\"home.groupsSubtitle\":\"Find your cause and pitch in.\",\"home.groupsTitle\":\"Chapters & teams\",\"home.joinConversationCta\":\"Share your story\",\"home.membersCaption\":\"Our volunteers\",\"home.onboardingBody\":\"Find a way to give, sign up to volunteer, and follow the mission's impact — all in one place.\",\"home.profileBody\":\"Sign in for RSVPs, memberships, groups and saved posts.\"}"
|
|
2082
2082
|
}
|
|
2083
2083
|
}
|
|
2084
2084
|
}
|
|
@@ -2129,7 +2129,7 @@ const SCREENS = runtimeSetting({
|
|
|
2129
2129
|
"package": "himalaya",
|
|
2130
2130
|
"type": "Text",
|
|
2131
2131
|
"props": {
|
|
2132
|
-
"text": "
|
|
2132
|
+
"text": "RSVPs, memberships, groups and saved content in one place.",
|
|
2133
2133
|
"style": "body",
|
|
2134
2134
|
"color": "textSecondary"
|
|
2135
2135
|
}
|
|
@@ -3442,7 +3442,10 @@ const SCREENS = runtimeSetting({
|
|
|
3442
3442
|
"bottom": 28
|
|
3443
3443
|
},
|
|
3444
3444
|
"onAppear": {
|
|
3445
|
-
"actionId": "tier5.himiStoreAccount.refresh"
|
|
3445
|
+
"actionId": "tier5.himiStoreAccount.refresh",
|
|
3446
|
+
"params": {
|
|
3447
|
+
"copy": "{\"account.subtitle\":\"RSVPs, memberships, groups and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — RSVPs, memberships and groups stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — RSVPs, memberships and groups stay with you.\"}"
|
|
3448
|
+
}
|
|
3446
3449
|
}
|
|
3447
3450
|
}
|
|
3448
3451
|
},
|
|
@@ -73230,6 +73230,48 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73230
73230
|
}
|
|
73231
73231
|
}
|
|
73232
73232
|
},
|
|
73233
|
+
{
|
|
73234
|
+
"id": "event_body",
|
|
73235
|
+
"body": {
|
|
73236
|
+
"package": "himalaya",
|
|
73237
|
+
"type": "RichContent",
|
|
73238
|
+
"props": {
|
|
73239
|
+
"documentJsonPath": "$service:himiOwnerLiteEvent.bodyJson",
|
|
73240
|
+
"onLink": {
|
|
73241
|
+
"actionId": "tier5.himiOwnerLiteEvent.openLink"
|
|
73242
|
+
},
|
|
73243
|
+
"fallback": {
|
|
73244
|
+
"id": "",
|
|
73245
|
+
"body": {
|
|
73246
|
+
"package": "himalaya",
|
|
73247
|
+
"type": "Text",
|
|
73248
|
+
"props": {
|
|
73249
|
+
"text": "$service:himiOwnerLiteEvent.bodyText",
|
|
73250
|
+
"style": "body",
|
|
73251
|
+
"color": "textPrimary",
|
|
73252
|
+
"collapseWhenEmpty": true
|
|
73253
|
+
}
|
|
73254
|
+
}
|
|
73255
|
+
}
|
|
73256
|
+
}
|
|
73257
|
+
}
|
|
73258
|
+
},
|
|
73259
|
+
{
|
|
73260
|
+
"id": "event_body_text",
|
|
73261
|
+
"body": {
|
|
73262
|
+
"package": "himalaya",
|
|
73263
|
+
"type": "Text",
|
|
73264
|
+
"props": {
|
|
73265
|
+
"text": "$service:himiOwnerLiteEvent.bodyText",
|
|
73266
|
+
"style": "body",
|
|
73267
|
+
"color": "textPrimary",
|
|
73268
|
+
"collapseWhenEmpty": true
|
|
73269
|
+
}
|
|
73270
|
+
},
|
|
73271
|
+
"modifiers": {
|
|
73272
|
+
"hiddenPath": "$service:himiOwnerLiteEvent.hasBodyJson"
|
|
73273
|
+
}
|
|
73274
|
+
},
|
|
73233
73275
|
{
|
|
73234
73276
|
"id": "event_info_card",
|
|
73235
73277
|
"body": {
|
|
@@ -73568,7 +73610,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
73568
73610
|
"contentType": "application/javascript+ts-bundle",
|
|
73569
73611
|
"declaredCapabilities": [
|
|
73570
73612
|
"wix_api",
|
|
73571
|
-
"local_database"
|
|
73613
|
+
"local_database",
|
|
73614
|
+
"open_url"
|
|
73572
73615
|
],
|
|
73573
73616
|
"minBinaryVersion": "0.1.0",
|
|
73574
73617
|
"bundleVersion": "0.0.1"
|
|
@@ -11,7 +11,9 @@ import { normalizeEvent, eventDescription } from "../_shared/himi-events.js";
|
|
|
11
11
|
import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwnerWrite } from "../_shared/owner-load-error.js";
|
|
12
12
|
import { DEMO_EVENTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
|
|
13
13
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
14
|
-
import {
|
|
14
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
15
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
16
|
+
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
15
17
|
|
|
16
18
|
// Offline-first (data-offline): the event DETAIL falls back to the last-synced
|
|
17
19
|
// local `owner_events` row (put by the event form's write-behind) on a failed
|
|
@@ -20,6 +22,7 @@ import { get as dbGet, remove as dbRemove, enqueue as dbEnqueue, type DbEntity }
|
|
|
20
22
|
// ENQUEUED for replay on reconnect. Every local-db call is wrapped so a build
|
|
21
23
|
// without the `local_database` grant behaves exactly as before (online-only).
|
|
22
24
|
const ENTITY = "owner_events";
|
|
25
|
+
async function cachePut(row: DbEntity): Promise<void> { try { await dbUpsert(ENTITY, row); } catch { /* capability not granted */ } }
|
|
23
26
|
async function cacheDrop(id: string): Promise<void> { try { await dbRemove(ENTITY, id); } catch { /* capability not granted */ } }
|
|
24
27
|
async function cacheGet(id: string): Promise<DbEntity | null> { try { return await dbGet(ENTITY, id); } catch { return null; } }
|
|
25
28
|
async function queueWrite(actionId: string, params: Record<string, string>): Promise<void> { try { await dbEnqueue(actionId, params); } catch { /* capability not granted */ } }
|
|
@@ -33,6 +36,13 @@ interface EventState {
|
|
|
33
36
|
title: string;
|
|
34
37
|
subtitle: string;
|
|
35
38
|
summary: string;
|
|
39
|
+
// Body ([rico] #3499) — RichContent binds bodyJson; the plain text shows when there is no document.
|
|
40
|
+
/** The v3 Ricos `description` (read with `fields=TEXTS`) as ONE document string; "" when there is none. */
|
|
41
|
+
bodyJson: string;
|
|
42
|
+
/** bodyJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
43
|
+
hasBodyJson: boolean;
|
|
44
|
+
/** The description's plain text (the document's own text, else the legacy description). */
|
|
45
|
+
bodyText: string;
|
|
36
46
|
initial: string;
|
|
37
47
|
image: string;
|
|
38
48
|
dateLine: string;
|
|
@@ -55,6 +65,17 @@ interface EventState {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
68
|
+
/** [rico] #3499 — keep the description's plain text with the event's cached row, so the offline detail shows it.
|
|
69
|
+
* Merged into the row the events list (or the form) wrote rather than replacing it; with no row yet, the
|
|
70
|
+
* loaded detail's own fields become one. */
|
|
71
|
+
async function cacheBodyText(s: EventState): Promise<void> {
|
|
72
|
+
if (s.status !== "ready" || !s.id) return;
|
|
73
|
+
const prev = await cacheGet(s.id);
|
|
74
|
+
await cachePut(prev ? { ...prev, bodyText: s.bodyText } : {
|
|
75
|
+
id: s.id, title: s.title, subtitle: s.subtitle, image: s.image, dateLine: s.dateLine, location: s.location,
|
|
76
|
+
statusLabel: s.statusLabel, registration: s.registration, bodyText: s.bodyText,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
58
79
|
function idFrom(arg: unknown, fallback = ""): string {
|
|
59
80
|
return typeof arg === "string" ? arg : String((arg as any)?.id ?? fallback);
|
|
60
81
|
}
|
|
@@ -97,6 +118,9 @@ function shape(s: Partial<EventState>): EventState {
|
|
|
97
118
|
title: s.title ?? "",
|
|
98
119
|
subtitle: s.subtitle ?? "",
|
|
99
120
|
summary: s.summary ?? "",
|
|
121
|
+
bodyJson: s.bodyJson ?? "",
|
|
122
|
+
hasBodyJson: !!s.bodyJson,
|
|
123
|
+
bodyText: s.bodyText ?? "",
|
|
100
124
|
initial: s.initial ?? "?",
|
|
101
125
|
image,
|
|
102
126
|
dateLine: s.dateLine ?? "—",
|
|
@@ -128,7 +152,9 @@ function demoDetail(s: EventState, id: string): EventState | null {
|
|
|
128
152
|
status: "ready",
|
|
129
153
|
title: d.title,
|
|
130
154
|
subtitle: [d.dateLine, d.location].filter(Boolean).join(" · "),
|
|
131
|
-
summary:
|
|
155
|
+
summary: "",
|
|
156
|
+
bodyJson: "",
|
|
157
|
+
bodyText: d.description,
|
|
132
158
|
initial: (d.title[0] || "?").toUpperCase(),
|
|
133
159
|
image: "",
|
|
134
160
|
dateLine: d.dateLine,
|
|
@@ -151,7 +177,9 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
151
177
|
status: "ready",
|
|
152
178
|
title,
|
|
153
179
|
subtitle: String(row.subtitle ?? [dateLine, location].filter((x) => x && x !== "—").join(" · ")),
|
|
154
|
-
summary:
|
|
180
|
+
summary: "",
|
|
181
|
+
bodyJson: "",
|
|
182
|
+
bodyText: String(row.bodyText ?? row.summary ?? row.description ?? ""),
|
|
155
183
|
initial: String(row.initial ?? (title[0] || "?")).slice(0, 1).toUpperCase(),
|
|
156
184
|
image: String(row.image ?? ""),
|
|
157
185
|
dateLine: dateLine || "—",
|
|
@@ -164,21 +192,27 @@ function cachedDetail(s: EventState, row: DbEntity): EventState {
|
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
async function refresh(prev: EventState, id: string): Promise<EventState> {
|
|
167
|
-
|
|
195
|
+
// A bare read returns the base fieldset: DETAILS adds `shortDescription` (the teaser and the legacy body),
|
|
196
|
+
// TEXTS the Ricos `description`.
|
|
197
|
+
const res = await getEvent(id, { fields: ["DETAILS", "TEXTS"] });
|
|
168
198
|
const ev = (res.event ?? res) as WixEvent;
|
|
169
199
|
if (!ev || !(ev as any).id) {
|
|
170
200
|
return shape({ ...prev, id, status: "error", title: "", error: copyParam("ownerEvent.errorNotFound", "event not found") });
|
|
171
201
|
}
|
|
172
202
|
const row = normalizeEvent(ev); // reuses date/location/status normalization
|
|
173
203
|
const image = /^https?:\/\//.test(row.image) ? row.image : "";
|
|
174
|
-
const
|
|
204
|
+
const { bodyJson, bodyText } = richBody((ev as unknown as { description?: unknown }).description, (eventDescription(ev) || "").trim());
|
|
205
|
+
// The teaser stays above the body only when it says something the body does not.
|
|
206
|
+
const teaser = String((ev as any).shortDescription ?? "").trim();
|
|
175
207
|
return shape({
|
|
176
208
|
...prev,
|
|
177
209
|
id,
|
|
178
210
|
status: "ready",
|
|
179
211
|
title: row.title,
|
|
180
212
|
subtitle: [row.dateLine, row.location].filter((x) => x && x !== "—").join(" · "),
|
|
181
|
-
summary:
|
|
213
|
+
summary: teaser && teaser !== bodyText.trim() ? teaser : "",
|
|
214
|
+
bodyJson,
|
|
215
|
+
bodyText,
|
|
182
216
|
initial: (row.title[0] || "?").toUpperCase(),
|
|
183
217
|
image,
|
|
184
218
|
dateLine: row.dateLine || "—",
|
|
@@ -194,6 +228,7 @@ const svc = defineService<EventState, {
|
|
|
194
228
|
load: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
195
229
|
retry: (s: EventState) => Promise<EventState>;
|
|
196
230
|
delete: (s: EventState) => Promise<EventState>;
|
|
231
|
+
openLink: (s: EventState, arg?: unknown) => Promise<EventState>;
|
|
197
232
|
}>("himiOwnerLiteEvent", {
|
|
198
233
|
state: shape({ status: "loading" }),
|
|
199
234
|
actions: {
|
|
@@ -208,7 +243,9 @@ const svc = defineService<EventState, {
|
|
|
208
243
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
209
244
|
}
|
|
210
245
|
try {
|
|
211
|
-
|
|
246
|
+
const loaded = await refresh(shape({ ...s, id, status: "loading" }), id);
|
|
247
|
+
await cacheBodyText(loaded);
|
|
248
|
+
return loaded;
|
|
212
249
|
} catch (e) {
|
|
213
250
|
// Offline: fall back to the last-synced local row before the demo/error paths.
|
|
214
251
|
const cached = await cacheGet(id);
|
|
@@ -229,7 +266,9 @@ const svc = defineService<EventState, {
|
|
|
229
266
|
return shape({ ...s, status: "error", error: "Sample item not found." });
|
|
230
267
|
}
|
|
231
268
|
try {
|
|
232
|
-
|
|
269
|
+
const loaded = await refresh(shape({ ...s, status: "loading" }), s.id);
|
|
270
|
+
await cacheBodyText(loaded);
|
|
271
|
+
return loaded;
|
|
233
272
|
} catch (e) {
|
|
234
273
|
const cached = await cacheGet(s.id);
|
|
235
274
|
if (cached) return cachedDetail(s, cached);
|
|
@@ -256,6 +295,11 @@ const svc = defineService<EventState, {
|
|
|
256
295
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
257
296
|
}
|
|
258
297
|
},
|
|
298
|
+
// [rico] #3499 — a link tapped in the RichContent description.
|
|
299
|
+
openLink: async (s, arg) => shape({
|
|
300
|
+
...s,
|
|
301
|
+
actionMessage: (await openRichLink(arg)) ? "" : copyParam("ownerEvent.linkOpenFailed", "This link could not be opened."),
|
|
302
|
+
}),
|
|
259
303
|
},
|
|
260
304
|
});
|
|
261
305
|
|