@wix/himalaya-cli 0.1311.0 → 0.1312.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/cli.mjs +55 -34
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/web-page-native-sdk.md +18 -8
- package/guides/webview-page-sdk.md +9 -3
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +25 -25
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/beauty-salon-owner/dev/index.ts +844 -510
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/branded-lite/dev/index.ts +120 -120
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/community-club/dev/index.ts +20 -20
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/community-club/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/community-club-owner/dev/index.ts +957 -595
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/community-club-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/events-meetups/dev/index.ts +15 -15
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/events-meetups-owner/dev/index.ts +726 -433
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/events-meetups-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/gym-fitness/dev/index.ts +30 -30
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/gym-fitness-owner/dev/index.ts +1041 -644
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/nonprofit-cause/dev/index.ts +20 -20
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +810 -482
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/owner-lite/dev/index.ts +1066 -648
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/owner-lite/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/podcast-creator/dev/index.ts +20 -20
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/podcast-creator-owner/dev/index.ts +957 -595
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/portfolio-creative/dev/index.ts +20 -20
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/portfolio-creative-owner/dev/index.ts +844 -510
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/restaurant/dev/index.ts +15 -15
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/restaurant-owner/dev/index.ts +1041 -644
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/restaurant-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
- package/templates/retail-store/dev/index.ts +10 -10
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/retail-store-owner/dev/index.ts +760 -461
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/retail-store-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/retail-store-owner/tier5-src/_shared/offset-page.ts +10 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +10 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/yoga-studio/dev/index.ts +30 -30
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/yoga-studio-owner/dev/index.ts +810 -482
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +10 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +3 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/detail-facts.ts +26 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/blocks.ts +7 -3
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +2 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +10 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/logic.ts +3 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +10 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-posts/index.ts +21 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/index.ts +6 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-search/vertical-sources.ts +178 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service/index.ts +10 -0
|
@@ -58,6 +58,13 @@ export interface CollectionConfig {
|
|
|
58
58
|
/** At or below this many records, with no query/filter narrowing them and no further
|
|
59
59
|
* page, the list/grid toggle, Refine, chips, search and count are noise. */
|
|
60
60
|
export const COLLECTION_CHROME_MIN_ITEMS = 4;
|
|
61
|
+
/** The accessible name of a row's overflow menu: `template` with `{title}` filled, or the
|
|
62
|
+
* generic `fallback` for a record with no title. */
|
|
63
|
+
export function collectionActionsLabel(title: string, template: string, fallback: string): string {
|
|
64
|
+
const t = title.trim();
|
|
65
|
+
return t ? template.replace('{title}', () => t) : fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
62
69
|
himiStoreShop: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', view: 'grid', backendQuery: true, countNoun: ['product', 'products'], favoriteToggle: true },
|
|
63
70
|
himiBookingServices: { rows: 'results', title: 'name', subtitle: 'subtitle', image: 'image', filter: 'category', view: 'grid', backendQuery: true, searchPlaceholder: 'Search services', countNoun: ['service', 'services'], primaryVerb: { label: 'Book', screenId: 'himi_time' } },
|
|
@@ -184,6 +191,9 @@ export function projectCollection(source: CollectionRow[], config: CollectionCon
|
|
|
184
191
|
collectionSelectLabel: prefs.selectedIds.includes(collectionId(row)) ? 'Selected' : 'Select',
|
|
185
192
|
// The save toggle names the action it WILL take (the service localizes it).
|
|
186
193
|
collectionSaveLabel: prefs.savedIds.includes(collectionId(row)) ? 'Remove from saved' : 'Save',
|
|
194
|
+
// The row's overflow menu, named for its record: out of context (a rotor, a voice
|
|
195
|
+
// command) a list of identical "Item actions" cannot be told apart.
|
|
196
|
+
collectionActionsLabel: collectionActionsLabel(collectionTitle(row, config), 'Actions for {title}', 'Item actions'),
|
|
187
197
|
collectionSubtitle: scalar(row[config.subtitle ?? '']) || scalar(row.subtitle) || scalar(row.description) || scalar(row.meta),
|
|
188
198
|
collectionImage: scalar(row[config.image ?? '']), collectionNoImage: !scalar(row[config.image ?? '']), collectionHasImage: !!scalar(row[config.image ?? '']),
|
|
189
199
|
collectionSaved: prefs.savedIds.includes(collectionId(row)), collectionNotSaved: !prefs.savedIds.includes(collectionId(row)), collectionSelected: prefs.selectedIds.includes(collectionId(row)),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineService as register, type ServiceDefinition } from '@himalaya/state';
|
|
2
|
-
import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, type CollectionRow } from './collection-contract.js';
|
|
2
|
+
import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, collectionActionsLabel, type CollectionRow } from './collection-contract.js';
|
|
3
3
|
import { localPreferences, type PreferenceRuntime } from './local-preferences.js';
|
|
4
4
|
import { copyParam, stashCopy } from './himi-copy.js';
|
|
5
5
|
import { createCollectionQuery, type CollectionQuerySource, type CollectionQueryState } from './collection-query.js';
|
|
@@ -130,6 +130,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
130
130
|
// the save toggle names the action it WILL take for this row. The
|
|
131
131
|
// old static "Save / unsave" made the owner work out the row's state.
|
|
132
132
|
row.collectionSaveLabel = row.collectionSaved ? copyParam('collection.unsave', 'Remove from saved') : copyParam('collection.save', 'Save');
|
|
133
|
+
row.collectionActionsLabel = collectionActionsLabel(String(row.collectionTitle ?? ''),
|
|
134
|
+
copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
|
|
133
135
|
});
|
|
134
136
|
if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
|
|
135
137
|
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Blank flags for a detail screen's fact rows.
|
|
2
|
+
//
|
|
3
|
+
// A detail screen lists fixed facts (Price, Status, SKU, Inventory…). A fact with no value
|
|
4
|
+
// painted its label beside nothing — "SKU" with a blank, "Registration" with a blank — which
|
|
5
|
+
// reads as a broken screen, or as a value that failed to load. `hiddenWhen` hides on a TRUE
|
|
6
|
+
// scalar only, so the worker publishes `<key>Blank` beside each fact and the descriptor hides
|
|
7
|
+
// the row (and its divider) on it. "—" counts as blank: it is the platform's canonical "no value"
|
|
8
|
+
// marker, which a `collapseWhenEmpty` Text collapses on web, iOS and Android alike — so a "—"
|
|
9
|
+
// value already painted nothing beside its label.
|
|
10
|
+
|
|
11
|
+
//
|
|
12
|
+
// Beside `<key>Blank`: `<key>NoDivider` (hide the rule above this row — it is blank, or nothing
|
|
13
|
+
// above it shows, so the card cannot open with a stray rule) and `factsBlank` (hide the card).
|
|
14
|
+
export function blankFlags(state: Record<string, unknown>, keys: readonly string[]): Record<string, boolean> {
|
|
15
|
+
const out: Record<string, boolean> = {};
|
|
16
|
+
let shownAbove = false;
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = state[key];
|
|
19
|
+
const blank = typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" || value.trim() === "" || value.trim() === "—";
|
|
20
|
+
out[`${key}Blank`] = blank;
|
|
21
|
+
out[`${key}NoDivider`] = blank || !shownAbove;
|
|
22
|
+
if (!blank) shownAbove = true;
|
|
23
|
+
}
|
|
24
|
+
out.factsBlank = !shownAbove;
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
@@ -528,11 +528,15 @@ function listBlock(block: AskBlock, key: string, binding: Binding): Node | null
|
|
|
528
528
|
},
|
|
529
529
|
pressFeedback: "scale",
|
|
530
530
|
haptic: "light",
|
|
531
|
-
|
|
531
|
+
// Its own visible verb. It sits inside a row named for its record, and a block key plus an
|
|
532
|
+
// index ("b1 delete row 1") is not a name anyone can act on.
|
|
533
|
+
accessibilityLabel: VERB_LABEL[proposeVerb] ?? proposeVerb,
|
|
532
534
|
padding: { leading: 8, trailing: 8, top: 6, bottom: 6 },
|
|
533
535
|
}));
|
|
534
536
|
}
|
|
535
|
-
|
|
537
|
+
// No label override: the row reads its own title, subtitle, badge and value. It was named
|
|
538
|
+
// "b1 row 1" — the block key and an index, an internal id spoken in place of all of them.
|
|
539
|
+
const modifiers: Record<string, unknown> = { fillMaxWidth: true };
|
|
536
540
|
if (tappable) {
|
|
537
541
|
modifiers.onTap = {
|
|
538
542
|
actionId: OPEN_ACTION,
|
|
@@ -617,7 +621,7 @@ function timelineBlock(block: AskBlock, key: string, binding: Binding): Node | n
|
|
|
617
621
|
// title barely a third of a phone's width.
|
|
618
622
|
const badge = rowBadge(result, query, i, idFor(`${key}_r${i}_b`), [titleField, block.subtitleField, timeField]);
|
|
619
623
|
if (badge) detail.push(badge);
|
|
620
|
-
const modifiers: Record<string, unknown> = { fillMaxWidth: true
|
|
624
|
+
const modifiers: Record<string, unknown> = { fillMaxWidth: true };
|
|
621
625
|
if (tappable) {
|
|
622
626
|
modifiers.onTap = { actionId: OPEN_ACTION, params: { id: result.rows[i]?.id ?? "", entity: String(entity) } };
|
|
623
627
|
modifiers.pressFeedback = "scale";
|
|
@@ -506,7 +506,7 @@ const ask = defineService<AskState, {
|
|
|
506
506
|
noAfterAnswer: state.noAnswer && recents.length === 0,
|
|
507
507
|
// A saved link whose question has aged out of the recents list can't
|
|
508
508
|
// be replayed — say so rather than opening a blank screen.
|
|
509
|
-
...(id ? { headline: "That saved answer is gone", note: "Ask the question again to bring it back." } : {}),
|
|
509
|
+
...(id ? { headline: "That saved answer is gone", note: "Ask the question again to bring it back.", customerNotice: "Ask the question again to bring it back." } : {}),
|
|
510
510
|
};
|
|
511
511
|
}
|
|
512
512
|
return askTurn({ ...state, intent }, intent, false);
|
|
@@ -614,7 +614,7 @@ const ask = defineService<AskState, {
|
|
|
614
614
|
const screenId = DETAIL_SCREEN[entity];
|
|
615
615
|
// No detail screen for this entity is a dead end, not a crash: say so
|
|
616
616
|
// rather than navigating somewhere arbitrary.
|
|
617
|
-
if (!screenId)
|
|
617
|
+
if (!screenId) { const notice = `${singular(entity)}s have no detail screen yet.`; return { ...state, note: notice, customerNotice: notice }; }
|
|
618
618
|
return navigate(state, { screenId, args: { id } });
|
|
619
619
|
},
|
|
620
620
|
},
|
|
@@ -14,6 +14,7 @@ import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
|
14
14
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
15
15
|
import { richBody } from "../_shared/rich-body.js";
|
|
16
16
|
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
17
|
+
import { blankFlags } from "../_shared/detail-facts.js";
|
|
17
18
|
|
|
18
19
|
// Offline-first (data-offline): the event DETAIL falls back to the last-synced
|
|
19
20
|
// local `owner_events` row (put by the event form's write-behind) on a failed
|
|
@@ -106,7 +107,16 @@ function registrationLabel(e: WixEvent): string {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
|
|
111
|
+
* `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
|
|
112
|
+
const FACT_KEYS = ["dateLine", "location", "statusLabel", "registration"] as const;
|
|
113
|
+
|
|
109
114
|
function shape(s: Partial<EventState>): EventState {
|
|
115
|
+
const out = shapeFields(s);
|
|
116
|
+
return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function shapeFields(s: Partial<EventState>): EventState {
|
|
110
120
|
const status = s.status ?? "ready";
|
|
111
121
|
const image = s.image ?? "";
|
|
112
122
|
const hasEntity = !!s.title && status === "ready";
|
|
@@ -50,6 +50,8 @@ export interface ManageArea {
|
|
|
50
50
|
unavailable?: boolean;
|
|
51
51
|
hideCreate?: boolean;
|
|
52
52
|
hideHandoff?: boolean;
|
|
53
|
+
/** The accessible name of this area's "Open in Wix", which carries the area ("Open in Wix: Store"). */
|
|
54
|
+
handoffLabel?: string;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export interface ManageSection {
|
|
@@ -95,6 +97,7 @@ export function parseAreas(value: unknown): ManageArea[] {
|
|
|
95
97
|
...(createScreenId && createLabel ? { createScreenId, createLabel } : {}),
|
|
96
98
|
hideCreate: !(createScreenId && createLabel),
|
|
97
99
|
hideHandoff: !DASHBOARD_PATHS[area.id],
|
|
100
|
+
handoffLabel: clean(raw.handoffLabel) || `Open in Wix: ${area.title}`,
|
|
98
101
|
metricText: "—",
|
|
99
102
|
statusText: "",
|
|
100
103
|
unavailable: false,
|
|
@@ -15,6 +15,7 @@ import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-
|
|
|
15
15
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
16
16
|
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
17
17
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
18
|
+
import { blankFlags } from "../_shared/detail-facts.js";
|
|
18
19
|
|
|
19
20
|
// Offline-first (data-offline): owner CRUD is WRITE-BEHIND against the encrypted
|
|
20
21
|
// `owner_posts` entity — publish/unpublish/delete update the local store
|
|
@@ -121,7 +122,16 @@ function statusLabel(raw?: string): string {
|
|
|
121
122
|
|
|
122
123
|
function coverUrl(raw: any): string { return postCoverUrl(raw); }
|
|
123
124
|
|
|
125
|
+
/** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
|
|
126
|
+
* `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
|
|
127
|
+
const FACT_KEYS = ["postStatus", "category", "published", "updated"] as const;
|
|
128
|
+
|
|
124
129
|
function shape(s: Partial<PostState>): PostState {
|
|
130
|
+
const out = shapeFields(s);
|
|
131
|
+
return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function shapeFields(s: Partial<PostState>): PostState {
|
|
125
135
|
const status = s.status ?? "ready";
|
|
126
136
|
const raw = s.postStatusRaw ?? "";
|
|
127
137
|
const image = s.image ?? "";
|
|
@@ -20,7 +20,19 @@ import { wixMediaUrl } from "@himalaya/wix-media";
|
|
|
20
20
|
// without the `local_database` grant behaves exactly as today (online-only).
|
|
21
21
|
const ENTITY = "owner_posts";
|
|
22
22
|
async function readCachedRows(): Promise<PostRow[]> {
|
|
23
|
-
try { return (await getAll(ENTITY)) as unknown as PostRow[]; } catch { return []; }
|
|
23
|
+
try { return ((await getAll(ENTITY)) as unknown as PostRow[]).map(withStatus); } catch { return []; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A row cached by an older bundle has no `statusLabel`. Its `dateLabel` led with the draft status
|
|
27
|
+
* ("Draft · Oct 1", "Scheduled · …") and a published row carried only a date, so derive it. */
|
|
28
|
+
function withStatus(row: PostRow): PostRow {
|
|
29
|
+
const dateLabel = String(row?.dateLabel ?? "");
|
|
30
|
+
const lead = dateLabel.split(" · ")[0];
|
|
31
|
+
const statusLabel = typeof row?.statusLabel === "string" && row.statusLabel
|
|
32
|
+
? row.statusLabel
|
|
33
|
+
: /^(Draft|Scheduled)$/i.test(lead) ? lead : copyParam("ownerPosts.published", "Published");
|
|
34
|
+
// The offline/seed row paints `dateLabel`, so it must lead with the status too.
|
|
35
|
+
return { ...row, statusLabel, dateLabel: lead === statusLabel ? dateLabel : [statusLabel, dateLabel].filter(Boolean).join(" · ") };
|
|
24
36
|
}
|
|
25
37
|
async function writeCachedRows(rows: PostRow[]): Promise<void> {
|
|
26
38
|
try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
@@ -31,6 +43,9 @@ interface PostRow {
|
|
|
31
43
|
title: string;
|
|
32
44
|
excerpt: string;
|
|
33
45
|
dateLabel: string;
|
|
46
|
+
/** Draft / Scheduled / Published. The collection row's meta line shows it, so a draft never
|
|
47
|
+
* looks like a live post. */
|
|
48
|
+
statusLabel: string;
|
|
34
49
|
image: string; // cover image (web URL, "" = none → initial badge)
|
|
35
50
|
initial: string;
|
|
36
51
|
hasImage: boolean;
|
|
@@ -81,7 +96,9 @@ function normalize(raw: { id?: string; title?: string; excerpt?: string; firstPu
|
|
|
81
96
|
id: raw.id || "",
|
|
82
97
|
title,
|
|
83
98
|
excerpt: (raw.excerpt || "").trim(),
|
|
84
|
-
|
|
99
|
+
// Status first, like a draft's — the offline/seed row shows this line, not the collection meta.
|
|
100
|
+
dateLabel: [copyParam("ownerPosts.published", "Published"), fmtDate(raw.firstPublishedDate || raw.lastPublishedDate || raw.editedDate || raw.createdDate)].filter(Boolean).join(" · "),
|
|
101
|
+
statusLabel: copyParam("ownerPosts.published", "Published"),
|
|
85
102
|
image,
|
|
86
103
|
initial: (title.trim()[0] || "?").toUpperCase(),
|
|
87
104
|
hasImage: !!image,
|
|
@@ -111,6 +128,7 @@ function normalizeDraft(raw: { id?: string; title?: string; excerpt?: string; st
|
|
|
111
128
|
title,
|
|
112
129
|
excerpt: (raw.excerpt || "").trim(),
|
|
113
130
|
dateLabel: [draftStatusLabel(raw.status), when].filter(Boolean).join(" · "),
|
|
131
|
+
statusLabel: draftStatusLabel(raw.status),
|
|
114
132
|
image,
|
|
115
133
|
initial: (title.trim()[0] || "?").toUpperCase(),
|
|
116
134
|
hasImage: !!image,
|
|
@@ -163,6 +181,7 @@ function demoList(s: PostsState): PostsState {
|
|
|
163
181
|
title: d.title,
|
|
164
182
|
excerpt: d.excerpt,
|
|
165
183
|
dateLabel: d.dateLabel,
|
|
184
|
+
statusLabel: d.postStatus,
|
|
166
185
|
image: "",
|
|
167
186
|
initial: (d.title[0] || "?").toUpperCase(),
|
|
168
187
|
hasImage: false,
|
|
@@ -11,6 +11,7 @@ import { ownerLoadError, isBridgeUnavailable, isOwnerPreviewDemo, shouldQueueOwn
|
|
|
11
11
|
import { DEMO_PRODUCTS, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-preview-demo.js";
|
|
12
12
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
13
13
|
import { get as dbGet, upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
14
|
+
import { blankFlags } from "../_shared/detail-facts.js";
|
|
14
15
|
|
|
15
16
|
// Offline-first (data-offline): owner CRUD is WRITE-BEHIND against the encrypted
|
|
16
17
|
// `owner_products` entity — the local store is updated OPTIMISTICALLY so the
|
|
@@ -109,7 +110,16 @@ function inventoryOf(p: WixProduct): string {
|
|
|
109
110
|
return status ? status.charAt(0).toUpperCase() + status.slice(1) : "—";
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
|
|
114
|
+
* `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
|
|
115
|
+
const FACT_KEYS = ["price", "statusLabel", "sku", "inventory"] as const;
|
|
116
|
+
|
|
112
117
|
function shape(s: Partial<ProductState>): ProductState {
|
|
118
|
+
const out = shapeFields(s);
|
|
119
|
+
return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shapeFields(s: Partial<ProductState>): ProductState {
|
|
113
123
|
const status = s.status ?? "ready";
|
|
114
124
|
const image = s.image ?? "";
|
|
115
125
|
const visible = s.visibleRaw ?? true;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type SearchRow,
|
|
35
35
|
type SearchState,
|
|
36
36
|
} from "./logic.js";
|
|
37
|
+
import { verticalAdapter } from "./vertical-sources.js";
|
|
37
38
|
|
|
38
39
|
/** How many loaded pages one source will walk looking for matches.
|
|
39
40
|
*
|
|
@@ -196,7 +197,12 @@ export function adaptersFrom(raw: unknown): SearchAdapter[] {
|
|
|
196
197
|
const ids = Array.isArray(raw) ? raw : [];
|
|
197
198
|
const seen = new Set<OwnerModuleId>();
|
|
198
199
|
const out: SearchAdapter[] = [];
|
|
200
|
+
const seenVertical = new Set<string>();
|
|
199
201
|
for (const value of ids) {
|
|
202
|
+
// A vertical (orders, products, services, events, posts) is its own source with its own
|
|
203
|
+
// API — not a management module — so it is admitted by its own allowlist.
|
|
204
|
+
const vertical = typeof value === "string" && !seenVertical.has(value) ? verticalAdapter(value, normalizeQuery, classifyOwnerError) : null;
|
|
205
|
+
if (vertical) { seenVertical.add(value as string); out.push(vertical); continue; }
|
|
200
206
|
const id = ownerModuleId(value);
|
|
201
207
|
if (!id || seen.has(id)) continue;
|
|
202
208
|
seen.add(id);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Record search over the __HIMI_APP_NAME__ VERTICALS — orders, products, booking services, events and
|
|
2
|
+
// blog posts. These are the dedicated screens (`himi_owner_lite_orders` …), not management
|
|
3
|
+
// modules, so `queryOwnerModule` cannot read them: each has its own Wix API and its own detail
|
|
4
|
+
// screen. The search screen promised "Search orders, contacts, members…" and never asked for an
|
|
5
|
+
// order; these adapters are what makes that promise true.
|
|
6
|
+
//
|
|
7
|
+
// Same honesty rules as the module sources: matching is over the pages actually read (title and
|
|
8
|
+
// subtitle, exactly what the row shows), the walk is bounded, and no total is claimed. The source
|
|
9
|
+
// id IS the list screen's id, and every row routes to that vertical's own detail screen — the
|
|
10
|
+
// static destination its list rows already use, never a worker-built route.
|
|
11
|
+
|
|
12
|
+
import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
13
|
+
import { queryEvents } from "@himalaya/wix-events";
|
|
14
|
+
import { queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
15
|
+
import { queryPosts, queryDraftPosts } from "@himalaya/wix-blog";
|
|
16
|
+
import { searchStoreProducts, productPrice, type StoreProduct } from "../_shared/himi-store.js";
|
|
17
|
+
import { normalizeEvent } from "../_shared/himi-events.js";
|
|
18
|
+
import { offsetPage } from "../_shared/offset-page.js";
|
|
19
|
+
import { isOwnerPreviewDemo } from "../_shared/owner-load-error.js";
|
|
20
|
+
import { DEMO_ORDERS, DEMO_PRODUCTS, DEMO_EVENTS, DEMO_POSTS, DEMO_SERVICES } from "../_shared/owner-preview-demo.js";
|
|
21
|
+
import { copyParam } from "../_shared/himi-copy.js";
|
|
22
|
+
import type { GroupProblem, SearchAdapter, SearchPage, SearchRow } from "./logic.js";
|
|
23
|
+
|
|
24
|
+
/** A vertical record before it becomes a row: what the row shows, and nothing else to match on. */
|
|
25
|
+
interface Found { id: string; title: string; subtitle: string; statusLabel?: string }
|
|
26
|
+
|
|
27
|
+
interface VerticalSource {
|
|
28
|
+
label: [key: string, fallback: string];
|
|
29
|
+
icon: string;
|
|
30
|
+
/** The vertical's detail screen — the one its own list rows open. */
|
|
31
|
+
detailScreenId: string;
|
|
32
|
+
/** One page of records. `cursor` is the source's own (an ecom cursor or an offset). */
|
|
33
|
+
page(query: string, cursor: string): Promise<{ items: Found[]; nextCursor: string }>;
|
|
34
|
+
demo(): Found[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PAGE = 50;
|
|
38
|
+
const MAX_PAGES = 3;
|
|
39
|
+
const join = (...parts: Array<string | undefined>) => parts.map((p) => (p ?? "").trim()).filter(Boolean).join(" · ");
|
|
40
|
+
|
|
41
|
+
const PAY_LABEL: Record<string, string> = {
|
|
42
|
+
PAID: "Paid", NOT_PAID: "Unpaid", PARTIALLY_PAID: "Partially paid", PENDING: "Pending",
|
|
43
|
+
FULLY_REFUNDED: "Refunded", PARTIALLY_REFUNDED: "Partly refunded",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function orderBuyer(raw: WixOrder): string {
|
|
47
|
+
const r = raw as unknown as { billingInfo?: { contactDetails?: { firstName?: string; lastName?: string } }; recipientInfo?: { contactDetails?: { firstName?: string; lastName?: string } } };
|
|
48
|
+
const c = r.billingInfo?.contactDetails ?? r.recipientInfo?.contactDetails;
|
|
49
|
+
return [c?.firstName, c?.lastName].filter(Boolean).join(" ").trim() || (raw.buyerInfo?.email ?? "").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function orderFound(raw: WixOrder): Found {
|
|
53
|
+
const number = raw.number ? `#${raw.number}` : "";
|
|
54
|
+
const pay = PAY_LABEL[(raw.paymentStatus ?? "").toUpperCase()] ?? "";
|
|
55
|
+
// The number rides the SUBTITLE so "10041" finds the order the owner sees as "#10041".
|
|
56
|
+
return { id: raw.id ?? "", title: orderBuyer(raw) || number || "Order", subtitle: join(number, raw.priceSummary?.total?.formattedAmount, pay), statusLabel: pay };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serviceFound(raw: WixService): Found {
|
|
60
|
+
const category = (raw as { category?: { name?: string } }).category?.name;
|
|
61
|
+
return { id: raw.id ?? "", title: (raw.name ?? "").trim() || "Service", subtitle: join(category) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const VERTICAL_SOURCES: Record<string, VerticalSource> = {
|
|
65
|
+
himi_owner_lite_orders: {
|
|
66
|
+
label: ["ownerSearch.source.orders", "Orders"],
|
|
67
|
+
icon: "cart",
|
|
68
|
+
detailScreenId: "himi_owner_lite_order",
|
|
69
|
+
async page(_query, cursor) {
|
|
70
|
+
const response = await searchOrders({ search: {
|
|
71
|
+
// The Orders list's own filter: an archived order is not one the owner can see there.
|
|
72
|
+
filter: { archived: false },
|
|
73
|
+
sort: [{ fieldName: "createdDate", order: "DESC" }],
|
|
74
|
+
cursorPaging: { limit: PAGE, ...(cursor ? { cursor } : {}) },
|
|
75
|
+
} });
|
|
76
|
+
const metadata = response.metadata as { cursors?: { next?: string | null } } | undefined;
|
|
77
|
+
return { items: (response.orders ?? []).filter((o) => !!o?.id).map(orderFound), nextCursor: metadata?.cursors?.next ?? "" };
|
|
78
|
+
},
|
|
79
|
+
demo: () => DEMO_ORDERS.map((d) => ({ id: d.id, title: d.buyer, subtitle: join(`#${d.number}`, d.total, d.paymentLabel), statusLabel: d.paymentLabel })),
|
|
80
|
+
},
|
|
81
|
+
himi_owner_lite_products: {
|
|
82
|
+
label: ["ownerSearch.source.products", "Store"],
|
|
83
|
+
icon: "bag",
|
|
84
|
+
detailScreenId: "himi_owner_lite_product",
|
|
85
|
+
async page(_query, cursor) {
|
|
86
|
+
// Read the catalog pages, like every other source here, and match locally on what the row
|
|
87
|
+
// shows. (V3 name search is a different endpoint with its own semantics; mixing a server
|
|
88
|
+
// match into a "loaded records" group would make its count mean two things.)
|
|
89
|
+
const response = await searchStoreProducts({ limit: PAGE, ...(cursor ? { cursor } : {}) });
|
|
90
|
+
const items = (response.products ?? []).filter((p: StoreProduct) => !!p?.id)
|
|
91
|
+
.map((p: StoreProduct) => ({ id: p.id ?? "", title: (p.name ?? "").trim() || "Untitled product", subtitle: productPrice(p) }));
|
|
92
|
+
return { items, nextCursor: response.pagingMetadata?.cursors?.next ?? "" };
|
|
93
|
+
},
|
|
94
|
+
demo: () => DEMO_PRODUCTS.map((d) => ({ id: d.id, title: d.name, subtitle: d.price })),
|
|
95
|
+
},
|
|
96
|
+
himi_owner_lite_services: {
|
|
97
|
+
label: ["ownerSearch.source.services", "Bookings"],
|
|
98
|
+
icon: "calendar",
|
|
99
|
+
detailScreenId: "himi_owner_lite_service",
|
|
100
|
+
async page(_query, cursor) {
|
|
101
|
+
const res = await queryServices({ query: { paging: { limit: PAGE, offset: Number(cursor || 0) }, sort: [{ fieldName: "name", order: "ASC" }] } });
|
|
102
|
+
const raw = res.services ?? [];
|
|
103
|
+
return { items: raw.filter((s) => !!s?.id).map(serviceFound), ...offsetPage(cursor, raw.length, PAGE, res.pagingMetadata) };
|
|
104
|
+
},
|
|
105
|
+
demo: () => DEMO_SERVICES.map((d) => ({ id: d.id, title: d.name, subtitle: join(d.category) })),
|
|
106
|
+
},
|
|
107
|
+
himi_owner_lite_events: {
|
|
108
|
+
label: ["ownerSearch.source.events", "Events"],
|
|
109
|
+
icon: "calendar",
|
|
110
|
+
detailScreenId: "himi_owner_lite_event",
|
|
111
|
+
async page(_query, cursor) {
|
|
112
|
+
const res = await queryEvents({ limit: PAGE, offset: Number(cursor || 0), query: { sort: [{ fieldName: "dateAndTimeSettings.startDate", order: "ASC" }, { fieldName: "id", order: "ASC" }] } });
|
|
113
|
+
const raw = res.events ?? [];
|
|
114
|
+
const items = raw.filter((e) => !!e?.id).map((e) => { const row = normalizeEvent(e); return { id: row.id, title: row.title, subtitle: join(row.dateLine, row.location) }; });
|
|
115
|
+
return { items, ...offsetPage(cursor, raw.length, PAGE, (res as { pagingMetadata?: { total?: number } }).pagingMetadata) };
|
|
116
|
+
},
|
|
117
|
+
demo: () => DEMO_EVENTS.map((d) => ({ id: d.id, title: d.title, subtitle: join(d.dateLine, d.location) })),
|
|
118
|
+
},
|
|
119
|
+
himi_owner_lite_posts: {
|
|
120
|
+
label: ["ownerSearch.source.posts", "Blog"],
|
|
121
|
+
icon: "newspaper",
|
|
122
|
+
detailScreenId: "himi_owner_lite_post",
|
|
123
|
+
async page(_query, cursor) {
|
|
124
|
+
// One walk over published posts; drafts ride the first page, best-effort, exactly as
|
|
125
|
+
// the Blog list reads them (a forbidden drafts resource must not fail the group).
|
|
126
|
+
const offset = Number(cursor || 0);
|
|
127
|
+
const [pub, draftPage] = await Promise.all([
|
|
128
|
+
queryPosts({ query: { paging: { limit: PAGE, offset } } }),
|
|
129
|
+
cursor ? Promise.resolve(null) : queryDraftPosts({ query: { paging: { limit: PAGE, offset: 0 } } }).catch(() => null),
|
|
130
|
+
]);
|
|
131
|
+
const posts = (pub.posts ?? []).filter((p: { id?: string }) => !!p?.id) as Array<{ id: string; title?: string; excerpt?: string }>;
|
|
132
|
+
const seen = new Set(posts.map((p) => p.id));
|
|
133
|
+
// Drafts unavailable (missing or forbidden resource) → published only.
|
|
134
|
+
const drafts = (((draftPage as { draftPosts?: unknown[] } | null)?.draftPosts ?? []) as Array<{ id?: string; status?: string; title?: string; excerpt?: string }>)
|
|
135
|
+
.filter((p) => p?.id && p.status !== "PUBLISHED" && !seen.has(p.id!)) as Array<{ id: string; title?: string; excerpt?: string }>;
|
|
136
|
+
const draft = copyParam("ownerSearch.draft", "Draft");
|
|
137
|
+
const items = [
|
|
138
|
+
...drafts.map((p) => ({ id: p.id, title: (p.title ?? "").trim() || "Untitled post", subtitle: join(draft, p.excerpt) })),
|
|
139
|
+
...posts.map((p) => ({ id: p.id, title: (p.title ?? "").trim() || "Untitled post", subtitle: join(p.excerpt) })),
|
|
140
|
+
];
|
|
141
|
+
return { items, ...offsetPage(cursor, posts.length, PAGE, (pub as { pagingMetadata?: { total?: number } }).pagingMetadata) };
|
|
142
|
+
},
|
|
143
|
+
demo: () => DEMO_POSTS.map((d) => ({ id: d.id, title: d.title, subtitle: join(/draft/i.test(d.postStatus) ? d.postStatus : "", d.excerpt) })),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const matches = (found: Found, needle: string): boolean =>
|
|
148
|
+
!!needle && `${found.title} ${found.subtitle}`.toLocaleLowerCase().includes(needle.toLocaleLowerCase());
|
|
149
|
+
|
|
150
|
+
export function verticalAdapter(id: string, normalizeQuery: (q: unknown) => string, classify: (e: unknown) => GroupProblem): SearchAdapter | null {
|
|
151
|
+
const source = Object.prototype.hasOwnProperty.call(VERTICAL_SOURCES, id) ? VERTICAL_SOURCES[id] : undefined;
|
|
152
|
+
if (!source) return null;
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
label: copyParam(source.label[0], source.label[1]),
|
|
156
|
+
icon: source.icon,
|
|
157
|
+
semantics: "loaded",
|
|
158
|
+
screenId: source.detailScreenId,
|
|
159
|
+
classify,
|
|
160
|
+
async search(query, cursor): Promise<SearchPage> {
|
|
161
|
+
const needle = normalizeQuery(query);
|
|
162
|
+
const toRow = (found: Found): SearchRow => ({
|
|
163
|
+
key: `${id}:${found.id}`, id: found.id, sourceId: id, title: found.title, subtitle: found.subtitle,
|
|
164
|
+
statusLabel: found.statusLabel ?? "", image: "", screenId: source.detailScreenId,
|
|
165
|
+
});
|
|
166
|
+
if (isOwnerPreviewDemo()) return { rows: source.demo().filter((f) => matches(f, needle)).map(toRow), nextCursor: "" };
|
|
167
|
+
const rows: SearchRow[] = [];
|
|
168
|
+
let next = cursor;
|
|
169
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
170
|
+
const result = await source.page(needle, next);
|
|
171
|
+
for (const found of result.items) if (found.id && matches(found, needle)) rows.push(toRow(found));
|
|
172
|
+
next = result.nextCursor;
|
|
173
|
+
if (!next) break;
|
|
174
|
+
}
|
|
175
|
+
return { rows, nextCursor: next };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -12,6 +12,7 @@ import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE } from "../_shared/owner-previ
|
|
|
12
12
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
13
13
|
import { get as dbGet, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "../_shared/owner-local-db.js";
|
|
14
14
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
15
|
+
import { blankFlags } from "../_shared/detail-facts.js";
|
|
15
16
|
|
|
16
17
|
// Offline-first (data-offline): the service DETAIL falls back to the last-synced
|
|
17
18
|
// local `owner_services` row (put by the service form's write-behind) on a failed
|
|
@@ -97,7 +98,16 @@ function imageOf(s: WixService): string {
|
|
|
97
98
|
return wixMediaUrl(m?.image?.url || m?.url, WIX_DETAIL_IMAGE);
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/** The fact rows this detail shows (the descriptor"s DETAIL_T5 rows): each publishes
|
|
102
|
+
* `<key>Blank`, so a fact with no value hides instead of painting a dangling label. */
|
|
103
|
+
const FACT_KEYS = ["typeLabel", "category", "capacity", "description"] as const;
|
|
104
|
+
|
|
100
105
|
function shape(s: Partial<ServiceState>): ServiceState {
|
|
106
|
+
const out = shapeFields(s);
|
|
107
|
+
return { ...out, ...blankFlags(out as unknown as Record<string, unknown>, FACT_KEYS) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function shapeFields(s: Partial<ServiceState>): ServiceState {
|
|
101
111
|
const status = s.status ?? "ready";
|
|
102
112
|
const image = s.image ?? "";
|
|
103
113
|
const hasEntity = !!s.title && status === "ready";
|