@wix/himalaya-cli 0.1210.0 → 0.1212.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 +91 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/beauty-salon-owner/dev/index.ts +280 -37
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club-owner/dev/index.ts +389 -37
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/community-club-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/events-meetups-owner/dev/index.ts +280 -37
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/gym-fitness-owner/dev/index.ts +389 -37
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/nonprofit-cause-owner/dev/index.ts +280 -37
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/owner-lite/dev/index.ts +389 -37
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/owner-lite/tier5-src/himi-owner-lite-faq/index.ts +8 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/podcast-creator-owner/dev/index.ts +389 -37
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/portfolio-creative-owner/dev/index.ts +280 -37
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/restaurant-owner/dev/index.ts +389 -37
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/retail-store-owner/dev/index.ts +280 -37
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +61 -16
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/yoga-studio-owner/dev/index.ts +280 -37
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +31 -16
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +57 -16
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +105 -37
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria-history/index.ts +1 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +59 -14
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-home/index.ts +5 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +4 -2
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +63 -16
|
@@ -13,9 +13,11 @@ import { queryGiftCards, getGiftCard } from '@himalaya/wix-gift-cards';
|
|
|
13
13
|
import { queryDonationCampaigns, getDonationCampaign } from '@himalaya/wix-donations';
|
|
14
14
|
import { query as queryBadges, get as getBadge } from '@himalaya/wix-badges';
|
|
15
15
|
import { wixMediaUrl } from '@himalaya/wix-media';
|
|
16
|
+
import { getSiteTimeZone } from '@himalaya/wix-analytics';
|
|
16
17
|
import { type OwnerModuleId } from './owner-modules.js';
|
|
17
18
|
import { type CollectionPage } from './collection-query.js';
|
|
18
|
-
import { copyParam } from './himi-copy.js';
|
|
19
|
+
import { copyParam, copyTemplate } from './himi-copy.js';
|
|
20
|
+
import { calendarDay, formatDateMedium, formatMonthDay, formatWeekdayDateTime } from './engine-intl.js';
|
|
19
21
|
import { eligibleOwnerOperations } from './owner-operations.js';
|
|
20
22
|
|
|
21
23
|
export interface OwnerRecord {
|
|
@@ -33,91 +35,144 @@ const join = (...values: string[]) => values.filter(Boolean).join(' · ');
|
|
|
33
35
|
const name = (value: unknown, first: string, last: string) => [field(value, first), field(value, last)].filter(Boolean).join(' ');
|
|
34
36
|
const yesNo = (value: unknown) => typeof value === 'boolean' ? value ? 'Yes' : 'No' : '';
|
|
35
37
|
const LIMIT = 50;
|
|
38
|
+
|
|
39
|
+
/** The site's IANA zone, resolved once per worker. Wix hands every date here as a UTC instant;
|
|
40
|
+
* formatting it in the viewer's device zone (or leaving it raw) shows an owner outside UTC the
|
|
41
|
+
* wrong hour and sometimes the wrong day. A failed or unset lookup degrades to the
|
|
42
|
+
* device zone — a zone is garnish on a date, never a reason to fail the collection. The
|
|
43
|
+
* answer, failure included, is kept for the worker's life: a role that cannot read site
|
|
44
|
+
* properties would otherwise re-send the failing request for every page a search walks. */
|
|
45
|
+
let zoneLookup: Promise<string | undefined> | undefined;
|
|
46
|
+
function siteZone(): Promise<string | undefined> {
|
|
47
|
+
// `Promise.resolve().then` so a bridge that throws synchronously is caught here too.
|
|
48
|
+
zoneLookup ??= Promise.resolve().then(getSiteTimeZone).catch(() => undefined);
|
|
49
|
+
return zoneLookup;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A Wix date value as an instant. A calendar date with no time ("2026-11-07") is a DAY, not an
|
|
53
|
+
* instant: it is built at local midnight and formatted with no zone, so it never shifts across
|
|
54
|
+
* one — including on engines without Intl, where a zone could not be applied anyway. */
|
|
55
|
+
function ownerInstant(value: string): { date: Date; dayOnly: boolean } | undefined {
|
|
56
|
+
if (!value) return undefined;
|
|
57
|
+
const day = /^(\d{4})-(\d\d)-(\d\d)$/.exec(value);
|
|
58
|
+
if (day) return { date: new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])), dayOnly: true };
|
|
59
|
+
const ms = /^\d{13}$/.test(value) ? Number(value) : Date.parse(value);
|
|
60
|
+
return Number.isFinite(ms) ? { date: new Date(ms), dayOnly: false } : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Has the instant (or, for a calendar date, the whole day) passed? */
|
|
63
|
+
function ownerPast(value: string, now = Date.now()): boolean {
|
|
64
|
+
const parsed = ownerInstant(value);
|
|
65
|
+
if (!parsed) return false;
|
|
66
|
+
const { date, dayOnly } = parsed;
|
|
67
|
+
return (dayOnly ? new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1).getTime() : date.getTime()) <= now;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type DateStyle = 'date' | 'dateTime' | 'short';
|
|
71
|
+
/** A Wix date as owner copy, through the shared engine-intl shapes: `date` "Jan 5, 2026",
|
|
72
|
+
* `dateTime` "Mon, Jan 5, 3:07 PM", `short` "Jan 5" (with the year when it is not this year).
|
|
73
|
+
* A value that is not a date at all is kept as Wix sent it. */
|
|
74
|
+
function ownerDate(value: string, zone: string | undefined, style: DateStyle = 'date'): string {
|
|
75
|
+
if (!value) return '';
|
|
76
|
+
const parsed = ownerInstant(value);
|
|
77
|
+
if (!parsed) return value;
|
|
78
|
+
const { date } = parsed, tz = parsed.dayOnly ? undefined : zone;
|
|
79
|
+
if (style === 'dateTime') return formatWeekdayDateTime(date, undefined, tz);
|
|
80
|
+
if (style === 'short' && calendarDay(date, tz).slice(0, 4) === calendarDay(new Date(), tz).slice(0, 4)) return formatMonthDay(date, undefined, tz);
|
|
81
|
+
return formatDateMedium(date, undefined, tz);
|
|
82
|
+
}
|
|
36
83
|
function offsetOf(cursor: string): number {
|
|
37
84
|
if (cursor && !/^\d{1,9}$/.test(cursor)) throw new Error('Invalid collection cursor. Refresh this view.');
|
|
38
85
|
return Number(cursor || 0);
|
|
39
86
|
}
|
|
40
|
-
function page(
|
|
41
|
-
return { items: rows
|
|
87
|
+
function page(rows: unknown[], nextCursor: string, total?: number, complete?: boolean): CollectionPage<unknown> {
|
|
88
|
+
return { items: rows, nextCursor, total, ...(complete === undefined ? {} : { complete }) };
|
|
42
89
|
}
|
|
43
|
-
function offsetPage(
|
|
90
|
+
function offsetPage(rows: unknown[], offset: number, metadata: unknown) {
|
|
44
91
|
const total = at(metadata, 'total');
|
|
45
92
|
const more = rows.length > 0 && (typeof total === 'number' ? offset + rows.length < total : rows.length === LIMIT);
|
|
46
|
-
return page(
|
|
93
|
+
return page(rows, more ? String(offset + rows.length) : '', typeof total === 'number' ? total : undefined);
|
|
47
94
|
}
|
|
48
95
|
|
|
49
96
|
/** Domain query shapes stay here, shared by list and detail workers. Unsupported
|
|
50
97
|
* server name queries remain explicitly labelled searches of loaded records. */
|
|
51
98
|
export async function queryOwnerModule(module: OwnerModuleId, cursor = ''): Promise<CollectionPage<OwnerRecord>> {
|
|
99
|
+
// The zone lookup runs beside the query, not after it: the two are independent, and serial
|
|
100
|
+
// they would double the time to a first paint.
|
|
101
|
+
const [raw, zone] = await Promise.all([queryRawOwnerModule(module, cursor), siteZone()]);
|
|
102
|
+
return { ...raw, items: raw.items.map(row => ownerRecord(module, row, zone)) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function queryRawOwnerModule(module: OwnerModuleId, cursor: string): Promise<CollectionPage<unknown>> {
|
|
52
106
|
const cursorPaging = { limit: LIMIT, ...(cursor ? { cursor } : {}) };
|
|
53
107
|
switch (module) {
|
|
54
108
|
case 'invoices': {
|
|
55
109
|
const offset = offsetOf(cursor), r = await queryInvoices({ query: { paging: { limit: LIMIT, offset } } });
|
|
56
|
-
return offsetPage(
|
|
110
|
+
return offsetPage(r.invoices ?? [], offset, r.pagingMetadata);
|
|
57
111
|
}
|
|
58
112
|
case 'coupons': {
|
|
59
113
|
const offset = offsetOf(cursor), r = await queryCoupons({ paging: { limit: LIMIT, offset } });
|
|
60
|
-
return offsetPage(
|
|
114
|
+
return offsetPage(r.coupons ?? [], offset, r.metadata);
|
|
61
115
|
}
|
|
62
116
|
case 'giftCards': {
|
|
63
117
|
const r = await queryGiftCards({ query: { cursorPaging, sort: [{ fieldName: 'createdDate', order: 'DESC' }] } });
|
|
64
|
-
return page(
|
|
118
|
+
return page(r.giftCards ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
65
119
|
}
|
|
66
120
|
case 'contacts': {
|
|
67
121
|
const offset = offsetOf(cursor), r = await queryContacts({ query: { paging: { limit: LIMIT, offset } } });
|
|
68
|
-
return offsetPage(
|
|
122
|
+
return offsetPage(r.contacts ?? [], offset, r.pagingMetadata);
|
|
69
123
|
}
|
|
70
124
|
case 'members': {
|
|
71
125
|
const offset = offsetOf(cursor), r = await queryMembers({ query: { paging: { limit: LIMIT, offset } } });
|
|
72
|
-
return offsetPage(
|
|
126
|
+
return offsetPage(r.members ?? [], offset, r.metadata);
|
|
73
127
|
}
|
|
74
128
|
case 'groups': {
|
|
75
129
|
const offset = offsetOf(cursor), r = await queryGroups({ paging: { limit: LIMIT, offset } });
|
|
76
|
-
return offsetPage(
|
|
130
|
+
return offsetPage(r.groups ?? [], offset, r.metaData);
|
|
77
131
|
}
|
|
78
132
|
case 'reviews': {
|
|
79
133
|
const r = await queryReviews({ cursorPaging, sort: [{ fieldName: 'createdDate', order: 'DESC' }] }, true);
|
|
80
|
-
return page(
|
|
134
|
+
return page(r.reviews ?? r.results ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
81
135
|
}
|
|
82
136
|
case 'programs': {
|
|
83
137
|
const r = await queryPrograms({ cursorPaging });
|
|
84
|
-
return page(
|
|
138
|
+
return page(r.programs ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
85
139
|
}
|
|
86
140
|
case 'badges': {
|
|
87
141
|
const r = await queryBadges({ query: { cursorPaging } });
|
|
88
|
-
return page(
|
|
142
|
+
return page(r.badges ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
89
143
|
}
|
|
90
144
|
case 'reservations': {
|
|
91
145
|
const r = await queryReservations({ cursorPaging, sort: [{ fieldName: 'details.startDate', order: 'ASC' }] });
|
|
92
|
-
return page(
|
|
146
|
+
return page(r.reservations ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
93
147
|
}
|
|
94
148
|
case 'portfolio': {
|
|
95
149
|
const r = await listProjects(LIMIT, cursor || undefined);
|
|
96
|
-
return page(
|
|
150
|
+
return page(r.projects ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
97
151
|
}
|
|
98
152
|
case 'donationCampaigns': {
|
|
99
153
|
const r = await queryDonationCampaigns({ query: { cursorPaging, sort: [{ fieldName: 'updatedDate', order: 'DESC' }] } });
|
|
100
|
-
return page(
|
|
154
|
+
return page(r.donationCampaigns ?? [], r.pagingMetadata?.cursors?.next ?? '');
|
|
101
155
|
}
|
|
102
156
|
case 'forms': {
|
|
103
157
|
// This collection is Wix Forms. Booking/event registration forms belong
|
|
104
158
|
// to their respective operational records, rather than a mixed union
|
|
105
159
|
// that silently drops namespaces when one permission check fails.
|
|
106
160
|
const offset = offsetOf(cursor), r = await queryForms({ filter: { namespace: 'wix.form_app.form' }, paging: { limit: LIMIT, offset } });
|
|
107
|
-
return offsetPage(
|
|
161
|
+
return offsetPage(r.forms ?? [], offset, r.metaData);
|
|
108
162
|
}
|
|
109
163
|
case 'plans': {
|
|
110
164
|
const r = await listPlans();
|
|
111
|
-
return page(
|
|
165
|
+
return page(r.plans ?? [], '', r.pagingMetadata?.total, false);
|
|
112
166
|
}
|
|
113
167
|
case 'subscriptions': {
|
|
114
168
|
const r = await listPlanOrders();
|
|
115
|
-
return page(
|
|
169
|
+
return page(r.orders ?? [], '', r.pagingMetadata?.total, false);
|
|
116
170
|
}
|
|
117
171
|
}
|
|
118
172
|
}
|
|
119
173
|
|
|
120
|
-
export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
174
|
+
export function ownerRecord(module: OwnerModuleId, raw: unknown, zone?: string): OwnerRecord {
|
|
175
|
+
const when = (path: string, style?: DateStyle) => ownerDate(field(raw, path), zone, style);
|
|
121
176
|
const id = field(raw, 'id');
|
|
122
177
|
if (!id) throw new Error('Wix returned a record without an identity. Refresh this view.');
|
|
123
178
|
let title = '', subtitle = '', image = '', statusLabel = '';
|
|
@@ -127,7 +182,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
127
182
|
title = field(raw, 'specification.name') || field(raw, 'displayData.name');
|
|
128
183
|
subtitle = field(raw, 'specification.code');
|
|
129
184
|
statusLabel = at(raw, 'expired') === true ? 'Expired' : at(raw, 'specification.active') === true ? 'Active' : 'Inactive';
|
|
130
|
-
details = [['Code', subtitle], ['Uses', field(raw, 'numberOfUsages')], ['Offer', words(field(raw, 'specification.type'))], ['Expires',
|
|
185
|
+
details = [['Code', subtitle], ['Uses', field(raw, 'numberOfUsages')], ['Offer', words(field(raw, 'specification.type'))], ['Expires', when('specification.expirationTime')]];
|
|
131
186
|
break;
|
|
132
187
|
case 'giftCards': {
|
|
133
188
|
const suffix = field(raw, 'codeSuffix');
|
|
@@ -147,7 +202,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
147
202
|
['Initial value', field(raw, 'initialValue.formattedAmount') || join(field(raw, 'initialValue.amount'), field(raw, 'currency'))],
|
|
148
203
|
['Recipient', recipient],
|
|
149
204
|
['Order number', field(raw, 'orderInfo.orderNumber')],
|
|
150
|
-
['Expires',
|
|
205
|
+
['Expires', when('expirationDate')],
|
|
151
206
|
['Source', words(field(raw, 'source'))],
|
|
152
207
|
];
|
|
153
208
|
break;
|
|
@@ -156,7 +211,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
156
211
|
title = field(raw, 'number') ? `#${field(raw, 'number')}` : field(raw, 'title');
|
|
157
212
|
subtitle = field(raw, 'customerInfo.contactDetails.fullName') || name(raw, 'customerInfo.contactDetails.firstName', 'customerInfo.contactDetails.lastName') || field(raw, 'customerInfo.contactDetails.email');
|
|
158
213
|
statusLabel = words(field(raw, 'status'));
|
|
159
|
-
details = [['Customer', subtitle], ['Total', join(field(raw, 'totals.total'), field(raw, 'currency'))], ['Due',
|
|
214
|
+
details = [['Customer', subtitle], ['Total', join(field(raw, 'totals.total'), field(raw, 'currency'))], ['Due', when('dueDate')]];
|
|
160
215
|
break;
|
|
161
216
|
case 'contacts':
|
|
162
217
|
title = name(raw, 'info.name.first', 'info.name.last') || field(raw, 'primaryInfo.email');
|
|
@@ -167,12 +222,12 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
167
222
|
case 'members':
|
|
168
223
|
title = field(raw, 'profile.nickname') || name(raw, 'contact.firstName', 'contact.lastName') || field(raw, 'loginEmail');
|
|
169
224
|
subtitle = field(raw, 'loginEmail'); image = field(raw, 'profile.photo.url'); statusLabel = words(field(raw, 'status'));
|
|
170
|
-
details = [['Email', subtitle], ['Last login',
|
|
225
|
+
details = [['Email', subtitle], ['Last login', when('lastLoginDate')]];
|
|
171
226
|
break;
|
|
172
227
|
case 'groups':
|
|
173
228
|
title = field(raw, 'name'); subtitle = field(raw, 'description') || field(raw, 'teaser');
|
|
174
229
|
image = field(raw, 'coverImage.imageInfo.url'); statusLabel = words(field(raw, 'privacyStatus'));
|
|
175
|
-
details = [['Members', field(raw, 'memberCount') || field(raw, 'membersCount')], ['Last activity',
|
|
230
|
+
details = [['Members', field(raw, 'memberCount') || field(raw, 'membersCount')], ['Last activity', when('lastActivityDate')]];
|
|
176
231
|
break;
|
|
177
232
|
case 'reviews':
|
|
178
233
|
title = field(raw, 'content.title'); subtitle = field(raw, 'content.body'); statusLabel = words(field(raw, 'moderation.moderationStatus'));
|
|
@@ -184,8 +239,14 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
184
239
|
details = [['Price', join(field(raw, 'pricing.price.value'), field(raw, 'pricing.price.currency'))], ['Free trial days', field(raw, 'pricing.freeTrialDays')]];
|
|
185
240
|
break;
|
|
186
241
|
case 'subscriptions':
|
|
187
|
-
|
|
188
|
-
|
|
242
|
+
// The buyer is only a member UUID here — not something an owner can read — so the
|
|
243
|
+
// row says when the plan started instead of echoing an internal id.
|
|
244
|
+
title = field(raw, 'planName'); statusLabel = words(field(raw, 'status'));
|
|
245
|
+
{
|
|
246
|
+
const started = when('startDate', 'short');
|
|
247
|
+
subtitle = started ? copyTemplate('ownerModules.startedOn', 'Started {date}', { date: started }) : '';
|
|
248
|
+
}
|
|
249
|
+
details = [['Started', when('startDate')], ['Ends', when('endDate')], ['Payment', words(field(raw, 'lastPaymentStatus'))]];
|
|
189
250
|
break;
|
|
190
251
|
case 'programs':
|
|
191
252
|
title = field(raw, 'description.title') || field(raw, 'details.title') || field(raw, 'name');
|
|
@@ -203,13 +264,14 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
203
264
|
break;
|
|
204
265
|
}
|
|
205
266
|
case 'reservations':
|
|
206
|
-
title = name(raw, 'reservee.firstName', 'reservee.lastName'); subtitle =
|
|
267
|
+
title = name(raw, 'reservee.firstName', 'reservee.lastName'); subtitle = when('details.startDate', 'dateTime'); statusLabel = words(field(raw, 'status'));
|
|
207
268
|
details = [['Arrival', subtitle], ['Party size', field(raw, 'details.partySize')], ['Email', field(raw, 'reservee.email')], ['Phone', field(raw, 'reservee.phone')]];
|
|
208
269
|
break;
|
|
209
270
|
case 'forms':
|
|
210
|
-
|
|
271
|
+
// The list is filtered to one namespace (`wix.form_app.form`), so the namespace is an
|
|
272
|
+
// internal constant, not information — it never reaches copy.
|
|
273
|
+
title = field(raw, 'name') || copyParam('ownerModules.untitledForm', 'Untitled form');
|
|
211
274
|
statusLabel = typeof at(raw, 'enabled') === 'boolean' ? at(raw, 'enabled') ? 'Enabled' : 'Disabled' : '';
|
|
212
|
-
details = [['Namespace', subtitle]];
|
|
213
275
|
break;
|
|
214
276
|
case 'portfolio':
|
|
215
277
|
title = field(raw, 'title'); subtitle = field(raw, 'description'); image = portfolioCoverRef(at(raw, 'coverImage') as WixMediaImage | string | undefined);
|
|
@@ -217,20 +279,25 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
217
279
|
break;
|
|
218
280
|
case 'donationCampaigns':
|
|
219
281
|
title = field(raw, 'name');
|
|
220
|
-
|
|
282
|
+
{
|
|
283
|
+
const end = when('campaignGoal.endDate', 'short');
|
|
284
|
+
subtitle = join(field(raw, 'campaignGoal.targetAmount.formattedAmount'), !end ? ''
|
|
285
|
+
: ownerPast(field(raw, 'campaignGoal.endDate')) ? copyTemplate('ownerModules.endedOn', 'ended {date}', { date: end })
|
|
286
|
+
: copyTemplate('ownerModules.endsOn', 'ends {date}', { date: end }));
|
|
287
|
+
}
|
|
221
288
|
image = field(raw, 'coverImage');
|
|
222
289
|
statusLabel = at(raw, 'archived') === true ? 'Archived' : words(field(raw, 'status'));
|
|
223
290
|
details = [
|
|
224
291
|
['Goal', field(raw, 'campaignGoal.targetAmount.formattedAmount') || field(raw, 'campaignGoal.targetAmount.amount')],
|
|
225
|
-
['End date',
|
|
292
|
+
['End date', when('campaignGoal.endDate')],
|
|
226
293
|
['Frequencies', Array.isArray(at(raw, 'donationFrequencies')) ? (at(raw, 'donationFrequencies') as unknown[]).map(String).join(', ') : ''],
|
|
227
294
|
['Custom amount', yesNo(at(raw, 'customAmountEnabled'))],
|
|
228
295
|
];
|
|
229
296
|
break;
|
|
230
297
|
}
|
|
231
298
|
details.push(
|
|
232
|
-
['Created',
|
|
233
|
-
['Updated',
|
|
299
|
+
['Created', when('createdDate') || when('dateCreated') || when('createDate')],
|
|
300
|
+
['Updated', when('updatedDate') || when('updateDate')],
|
|
234
301
|
);
|
|
235
302
|
return { id, moduleId: module, title: title || copyParam('ownerModules.untitled', 'Untitled record'), subtitle, image: wixMediaUrl(image), statusLabel,
|
|
236
303
|
detailScreenId: `himi_owner_lite_record_${module}`,
|
|
@@ -240,6 +307,7 @@ export function ownerRecord(module: OwnerModuleId, raw: unknown): OwnerRecord {
|
|
|
240
307
|
|
|
241
308
|
export async function getOwnerRecord(module: OwnerModuleId, id: string): Promise<OwnerRecord> {
|
|
242
309
|
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(id)) throw new Error('This record link is invalid. Return to the collection.');
|
|
310
|
+
const zone = siteZone(); // started now, beside the record fetch; it never rejects
|
|
243
311
|
let raw: unknown;
|
|
244
312
|
switch (module) {
|
|
245
313
|
case 'coupons': raw = (await getCoupon(id)).coupon; break;
|
|
@@ -270,13 +338,13 @@ export async function getOwnerRecord(module: OwnerModuleId, id: string): Promise
|
|
|
270
338
|
seen.add(next); cursor = next;
|
|
271
339
|
}
|
|
272
340
|
if (!raw) throw new Error('This project could not be located. Open the portfolio in Wix to check its availability.');
|
|
273
|
-
const record = ownerRecord(module, raw);
|
|
341
|
+
const record = ownerRecord(module, raw, await zone);
|
|
274
342
|
const media = await listProjectItems(id, 100);
|
|
275
343
|
return { ...record, fields: [...record.fields, { id: 'media', label: 'Loaded media items', value: String(media.items?.length ?? 0) }] };
|
|
276
344
|
}
|
|
277
345
|
case 'donationCampaigns': raw = (await getDonationCampaign(id)).donationCampaign; break;
|
|
278
346
|
}
|
|
279
|
-
const record = ownerRecord(module, raw);
|
|
347
|
+
const record = ownerRecord(module, raw, await zone);
|
|
280
348
|
if (record.id !== id) throw new Error('The returned record does not match this link. Refresh the collection.');
|
|
281
349
|
return record;
|
|
282
350
|
}
|
|
@@ -26,6 +26,7 @@ const core = createAriaHistory({
|
|
|
26
26
|
last7: () => copyParam("ownerAriaHistory.last7", "Last 7 days"),
|
|
27
27
|
last30: () => copyParam("ownerAriaHistory.last30", "Last 30 days"),
|
|
28
28
|
older: () => copyParam("ownerAriaHistory.older", "Older"),
|
|
29
|
+
today: () => copyParam("ownerAriaHistory.today", "Today"),
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -84,6 +84,10 @@ interface HomeState {
|
|
|
84
84
|
notLoading: boolean;
|
|
85
85
|
notErrored: boolean;
|
|
86
86
|
hideContent: boolean;
|
|
87
|
+
/** The retry cards' list is hidden unless the dashboard is ready AND a source failed.
|
|
88
|
+
* An empty list still costs its parent stack a gap, which on iOS read as a blank
|
|
89
|
+
* band under the header; gating on `hideContent` alone left it mounted. */
|
|
90
|
+
hideSourceFailures: boolean;
|
|
87
91
|
demoNote: string;
|
|
88
92
|
notDemo: boolean;
|
|
89
93
|
}
|
|
@@ -145,6 +149,7 @@ function shape(s: Partial<HomeState>): HomeState {
|
|
|
145
149
|
notLoading: !isLoading,
|
|
146
150
|
notErrored: status !== "error",
|
|
147
151
|
hideContent: status !== "ready",
|
|
152
|
+
hideSourceFailures: status !== "ready" || !(s.sourceFailures?.length),
|
|
148
153
|
demoNote: s.demoNote ?? "",
|
|
149
154
|
notDemo: !s.demoNote,
|
|
150
155
|
};
|
|
@@ -39,6 +39,8 @@ interface State {
|
|
|
39
39
|
noEmpty: boolean;
|
|
40
40
|
hasRecents: boolean;
|
|
41
41
|
noRecents: boolean;
|
|
42
|
+
/** True while `sections` is empty — gates the list so an empty one costs no gap. */
|
|
43
|
+
noSections: boolean;
|
|
42
44
|
isLoading: boolean;
|
|
43
45
|
notLoading: boolean;
|
|
44
46
|
summaryNote: string;
|
|
@@ -200,7 +202,7 @@ function derive(state: State, areas = state.areas): State {
|
|
|
200
202
|
// areas arrive — or when this app provisions none at all — there is nothing a
|
|
201
203
|
// filter could have excluded, so "Clear filters" would be a lie.
|
|
202
204
|
const noEmpty = next.noEmpty || state.status !== 'ready' || areas.length === 0;
|
|
203
|
-
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0, businessName: currentBusinessName };
|
|
205
|
+
return { ...state, ...next, noEmpty, recents, hasRecents: recents.length > 0, noRecents: recents.length === 0, noSections: next.sections.length === 0, businessName: currentBusinessName };
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
// Loads are not serialized: on a warm reappearance the host's bare refresh and
|
|
@@ -276,7 +278,7 @@ const svc = defineService<State, {
|
|
|
276
278
|
// 'All' is always a valid filter, so the chip row paints from the first frame
|
|
277
279
|
// instead of appearing (and shifting the layout) when the areas arrive.
|
|
278
280
|
groups: [{ id: 'all', label: allLabel() }], summary: '', noEmpty: true,
|
|
279
|
-
hasRecents: false, noRecents: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
281
|
+
hasRecents: false, noRecents: true, noSections: true, isLoading: true, notLoading: false, summaryNote: '', handoffError: '',
|
|
280
282
|
},
|
|
281
283
|
actions: {
|
|
282
284
|
load: async (state, arg) => {
|
|
@@ -80,6 +80,9 @@ interface PostState {
|
|
|
80
80
|
/** Non-empty when showing owner-preview-demo sample data (web preview, no owner bridge). */
|
|
81
81
|
demoNote: string;
|
|
82
82
|
notDemo: boolean;
|
|
83
|
+
/** #3158 — Delete ARMS a confirmation; only confirmDelete reaches the API. */
|
|
84
|
+
deletePending: boolean;
|
|
85
|
+
noDeleteConfirmation: boolean;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -159,6 +162,9 @@ function shape(s: Partial<PostState>): PostState {
|
|
|
159
162
|
hideUnpublish: !published || status !== "ready",
|
|
160
163
|
demoNote: s.demoNote ?? "",
|
|
161
164
|
notDemo: !s.demoNote,
|
|
165
|
+
// Only a loaded entity can be pending deletion; every other action disarms it (`disarm`).
|
|
166
|
+
deletePending: !!s.deletePending && hasEntity,
|
|
167
|
+
noDeleteConfirmation: !(s.deletePending && hasEntity),
|
|
162
168
|
};
|
|
163
169
|
}
|
|
164
170
|
|
|
@@ -276,18 +282,35 @@ async function refresh(prev: PostState, id: string, actionMessage = ""): Promise
|
|
|
276
282
|
});
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
/** #3158 — a confirmed delete in flight. Synchronous and module-level on purpose: two
|
|
286
|
+
* confirmDelete dispatches (a double tap) both start from a state that says "armed", and only a
|
|
287
|
+
* flag set BEFORE the first await stops the second from issuing its own DELETE — whose late 404
|
|
288
|
+
* would otherwise land after, and overwrite, the first one's "deleted". */
|
|
289
|
+
let deleteInFlight = false;
|
|
290
|
+
|
|
291
|
+
/** #3158 — the confirmation describes THIS visit. Never take it from a persisted snapshot, or a
|
|
292
|
+
* reopened record comes back with "Delete this …?" already armed. */
|
|
293
|
+
const DELETE_RUN_KEYS = ["deletePending", "noDeleteConfirmation"] as const;
|
|
294
|
+
|
|
295
|
+
/** #3158 — clear an armed delete confirmation. */
|
|
296
|
+
const disarm = <T extends { deletePending: boolean }>(s: T): T => ({ ...s, deletePending: false });
|
|
297
|
+
|
|
279
298
|
const svc = defineService<PostState, {
|
|
280
299
|
load: (s: PostState, arg?: unknown) => Promise<PostState>;
|
|
281
300
|
retry: (s: PostState) => Promise<PostState>;
|
|
282
301
|
publish: (s: PostState) => Promise<PostState>;
|
|
283
302
|
unpublish: (s: PostState) => Promise<PostState>;
|
|
284
303
|
delete: (s: PostState) => Promise<PostState>;
|
|
304
|
+
cancelDelete: (s: PostState) => Promise<PostState>;
|
|
305
|
+
confirmDelete: (s: PostState) => Promise<PostState | undefined>;
|
|
285
306
|
/** [rico] #3370 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
286
307
|
openLink: (s: PostState, arg?: unknown) => Promise<PostState>;
|
|
287
308
|
}>("himiOwnerLitePost", {
|
|
288
309
|
state: shape({ status: "loading" }),
|
|
310
|
+
notHydrated: [...DELETE_RUN_KEYS],
|
|
289
311
|
actions: {
|
|
290
|
-
load: async (
|
|
312
|
+
load: async (s0, arg) => {
|
|
313
|
+
const s = disarm(s0);
|
|
291
314
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
292
315
|
const id = idFrom(arg, s.id) || inputId();
|
|
293
316
|
if (!id) return shape({ ...s, status: "error", error: copyParam("ownerPost.errorNoId", "no post id") });
|
|
@@ -312,7 +335,8 @@ const svc = defineService<PostState, {
|
|
|
312
335
|
return shape({ ...s, id, status: "error", error: ownerLoadError(e) });
|
|
313
336
|
}
|
|
314
337
|
},
|
|
315
|
-
retry: async (
|
|
338
|
+
retry: async (s0) => {
|
|
339
|
+
const s = disarm(s0);
|
|
316
340
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerPost.errorNoId", "no post id") });
|
|
317
341
|
if (isOwnerPreviewDemo()) {
|
|
318
342
|
const demo = demoDetail(s, s.id);
|
|
@@ -334,7 +358,8 @@ const svc = defineService<PostState, {
|
|
|
334
358
|
return shape({ ...s, status: "error", error: ownerLoadError(e) });
|
|
335
359
|
}
|
|
336
360
|
},
|
|
337
|
-
publish: async (
|
|
361
|
+
publish: async (s0) => {
|
|
362
|
+
const s = disarm(s0);
|
|
338
363
|
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
339
364
|
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
340
365
|
if (s.postStatusRaw === "PUBLISHED") return shape({ ...s, actionMessage: copyParam("ownerPost.alreadyPublished", "Post is already published.") });
|
|
@@ -352,7 +377,8 @@ const svc = defineService<PostState, {
|
|
|
352
377
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
353
378
|
}
|
|
354
379
|
},
|
|
355
|
-
unpublish: async (
|
|
380
|
+
unpublish: async (s0) => {
|
|
381
|
+
const s = disarm(s0);
|
|
356
382
|
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
357
383
|
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
358
384
|
if (s.postStatusRaw !== "PUBLISHED" && s.postStatusRaw !== "SCHEDULED") {
|
|
@@ -372,20 +398,35 @@ const svc = defineService<PostState, {
|
|
|
372
398
|
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
373
399
|
}
|
|
374
400
|
},
|
|
375
|
-
delete
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
401
|
+
// #3158 — destructive, so two steps. `delete` (the Manage card's Delete button)
|
|
402
|
+
// only ARMS the inline confirmation, which names the record and says it cannot
|
|
403
|
+
// be undone; `cancelDelete` disarms it; only `confirmDelete` deletes. Anything
|
|
404
|
+
// that dispatches `delete` directly therefore still asks first.
|
|
405
|
+
delete: async (s) => shape({ ...s, deletePending: true, actionMessage: "" }),
|
|
406
|
+
cancelDelete: async (s) => shape({ ...s, deletePending: false }),
|
|
407
|
+
confirmDelete: async (s0) => {
|
|
408
|
+
// Nothing armed (a stray dispatch, a rehydrated snapshot) or a delete already running
|
|
409
|
+
// (a second tap): ignore it — `undefined` leaves the state exactly as it is.
|
|
410
|
+
if (!s0.deletePending || deleteInFlight) return undefined;
|
|
411
|
+
deleteInFlight = true;
|
|
380
412
|
try {
|
|
381
|
-
|
|
382
|
-
return shape({ ...s,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
const s = disarm(s0);
|
|
414
|
+
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
415
|
+
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerPost.noIdMessage", "No post id.") });
|
|
416
|
+
// Optimistic local delete — the list drops the row immediately.
|
|
417
|
+
await cacheDrop(s.id);
|
|
418
|
+
try {
|
|
419
|
+
await deleteDraftPost(s.id);
|
|
420
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerPost.deletedEmpty", "Post deleted."), actionMessage: "" });
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (shouldQueueOwnerWrite(e)) {
|
|
423
|
+
await queueWrite("tier5.himiOwnerLitePost.delete", { id: s.id });
|
|
424
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerPost.deletedEmpty", "Post deleted."), actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
425
|
+
}
|
|
426
|
+
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
387
427
|
}
|
|
388
|
-
|
|
428
|
+
} finally {
|
|
429
|
+
deleteInFlight = false;
|
|
389
430
|
}
|
|
390
431
|
},
|
|
391
432
|
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and
|
|
@@ -397,4 +438,10 @@ const svc = defineService<PostState, {
|
|
|
397
438
|
},
|
|
398
439
|
});
|
|
399
440
|
|
|
441
|
+
// The same reset on a runtime from before `notHydrated` (an older binary on a new bundle).
|
|
442
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
443
|
+
const booted = svc.getState?.() as unknown as Record<string, unknown> | undefined;
|
|
444
|
+
if (booted && typeof booted === "object") Object.assign(booted, { deletePending: false, noDeleteConfirmation: true });
|
|
445
|
+
}
|
|
446
|
+
|
|
400
447
|
export { svc };
|
|
@@ -59,6 +59,9 @@ interface ProductState {
|
|
|
59
59
|
/** Non-empty when showing owner-preview-demo sample data (web preview, no owner bridge). */
|
|
60
60
|
demoNote: string;
|
|
61
61
|
notDemo: boolean;
|
|
62
|
+
/** #3158 — Delete ARMS a confirmation; only confirmDelete reaches the API. */
|
|
63
|
+
deletePending: boolean;
|
|
64
|
+
noDeleteConfirmation: boolean;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -143,6 +146,9 @@ function shape(s: Partial<ProductState>): ProductState {
|
|
|
143
146
|
hideHide: !visible || status !== "ready",
|
|
144
147
|
demoNote: s.demoNote ?? "",
|
|
145
148
|
notDemo: !s.demoNote,
|
|
149
|
+
// Only a loaded entity can be pending deletion; every other action disarms it (`disarm`).
|
|
150
|
+
deletePending: !!s.deletePending && hasEntity,
|
|
151
|
+
noDeleteConfirmation: !(s.deletePending && hasEntity),
|
|
146
152
|
};
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -265,16 +271,33 @@ async function setVisibility(s: ProductState, visible: boolean, msg: string): Pr
|
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
|
|
274
|
+
/** #3158 — a confirmed delete in flight. Synchronous and module-level on purpose: two
|
|
275
|
+
* confirmDelete dispatches (a double tap) both start from a state that says "armed", and only a
|
|
276
|
+
* flag set BEFORE the first await stops the second from issuing its own DELETE — whose late 404
|
|
277
|
+
* would otherwise land after, and overwrite, the first one's "deleted". */
|
|
278
|
+
let deleteInFlight = false;
|
|
279
|
+
|
|
280
|
+
/** #3158 — the confirmation describes THIS visit. Never take it from a persisted snapshot, or a
|
|
281
|
+
* reopened record comes back with "Delete this …?" already armed. */
|
|
282
|
+
const DELETE_RUN_KEYS = ["deletePending", "noDeleteConfirmation"] as const;
|
|
283
|
+
|
|
284
|
+
/** #3158 — clear an armed delete confirmation. */
|
|
285
|
+
const disarm = <T extends { deletePending: boolean }>(s: T): T => ({ ...s, deletePending: false });
|
|
286
|
+
|
|
268
287
|
const svc = defineService<ProductState, {
|
|
269
288
|
load: (s: ProductState, arg?: unknown) => Promise<ProductState>;
|
|
270
289
|
retry: (s: ProductState) => Promise<ProductState>;
|
|
271
290
|
show: (s: ProductState) => Promise<ProductState>;
|
|
272
291
|
hide: (s: ProductState) => Promise<ProductState>;
|
|
273
292
|
delete: (s: ProductState) => Promise<ProductState>;
|
|
293
|
+
cancelDelete: (s: ProductState) => Promise<ProductState>;
|
|
294
|
+
confirmDelete: (s: ProductState) => Promise<ProductState | undefined>;
|
|
274
295
|
}>("himiOwnerLiteProduct", {
|
|
275
296
|
state: shape({ status: "loading" }),
|
|
297
|
+
notHydrated: [...DELETE_RUN_KEYS],
|
|
276
298
|
actions: {
|
|
277
|
-
load: async (
|
|
299
|
+
load: async (s0, arg) => {
|
|
300
|
+
const s = disarm(s0);
|
|
278
301
|
stashCopy(arg); // absorb the descriptor's onAppear copy-pack param
|
|
279
302
|
const id = idFrom(arg, s.id) || inputId();
|
|
280
303
|
if (!id) return shape({ ...s, status: "error", error: copyParam("ownerProduct.errorNoId", "no product id") });
|
|
@@ -297,7 +320,8 @@ const svc = defineService<ProductState, {
|
|
|
297
320
|
return shape({ ...s, id, status: "error", error: ownerLoadError(e) });
|
|
298
321
|
}
|
|
299
322
|
},
|
|
300
|
-
retry: async (
|
|
323
|
+
retry: async (s0) => {
|
|
324
|
+
const s = disarm(s0);
|
|
301
325
|
if (!s.id) return shape({ ...s, status: "error", error: copyParam("ownerProduct.errorNoId", "no product id") });
|
|
302
326
|
if (isOwnerPreviewDemo()) {
|
|
303
327
|
const demo = demoDetail(s, s.id);
|
|
@@ -317,25 +341,46 @@ const svc = defineService<ProductState, {
|
|
|
317
341
|
return shape({ ...s, status: "error", error: ownerLoadError(e) });
|
|
318
342
|
}
|
|
319
343
|
},
|
|
320
|
-
show: async (
|
|
321
|
-
hide: async (
|
|
322
|
-
delete
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
344
|
+
show: async (s0) => { const s = disarm(s0); return setVisibility(s, true, copyParam("ownerProduct.shownMessage", "Product is now visible in your store.")); },
|
|
345
|
+
hide: async (s0) => { const s = disarm(s0); return setVisibility(s, false, copyParam("ownerProduct.hiddenMessage", "Product hidden from your store.")); },
|
|
346
|
+
// #3158 — destructive, so two steps. `delete` (the Manage card's Delete button)
|
|
347
|
+
// only ARMS the inline confirmation, which names the record and says it cannot
|
|
348
|
+
// be undone; `cancelDelete` disarms it; only `confirmDelete` deletes. Anything
|
|
349
|
+
// that dispatches `delete` directly therefore still asks first.
|
|
350
|
+
delete: async (s) => shape({ ...s, deletePending: true, actionMessage: "" }),
|
|
351
|
+
cancelDelete: async (s) => shape({ ...s, deletePending: false }),
|
|
352
|
+
confirmDelete: async (s0) => {
|
|
353
|
+
// Nothing armed (a stray dispatch, a rehydrated snapshot) or a delete already running
|
|
354
|
+
// (a second tap): ignore it — `undefined` leaves the state exactly as it is.
|
|
355
|
+
if (!s0.deletePending || deleteInFlight) return undefined;
|
|
356
|
+
deleteInFlight = true;
|
|
327
357
|
try {
|
|
328
|
-
|
|
329
|
-
return shape({ ...s,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
358
|
+
const s = disarm(s0);
|
|
359
|
+
if (isOwnerPreviewDemo() || s.demoNote) return shape({ ...s, actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
360
|
+
if (!s.id) return shape({ ...s, actionMessage: copyParam("ownerProduct.noIdMessage", "No product id.") });
|
|
361
|
+
// Optimistic local delete — the list drops the row immediately.
|
|
362
|
+
await cacheDrop(s.id);
|
|
363
|
+
try {
|
|
364
|
+
await deleteProduct(s.id);
|
|
365
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerProduct.deletedEmpty", "Product deleted."), actionMessage: "" });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
if (shouldQueueOwnerWrite(e)) {
|
|
368
|
+
await queueWrite("tier5.himiOwnerLiteProduct.delete", { id: s.id });
|
|
369
|
+
return shape({ ...s, status: "deleted", title: "", emptyText: copyParam("ownerProduct.deletedEmpty", "Product deleted."), actionMessage: copyParam("ownerCommon.demoSaveNote", DEMO_SAVE_NOTE) });
|
|
370
|
+
}
|
|
371
|
+
return shape({ ...s, actionMessage: ownerLoadError(e) });
|
|
334
372
|
}
|
|
335
|
-
|
|
373
|
+
} finally {
|
|
374
|
+
deleteInFlight = false;
|
|
336
375
|
}
|
|
337
376
|
},
|
|
338
377
|
},
|
|
339
378
|
});
|
|
340
379
|
|
|
380
|
+
// The same reset on a runtime from before `notHydrated` (an older binary on a new bundle).
|
|
381
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
382
|
+
const booted = svc.getState?.() as unknown as Record<string, unknown> | undefined;
|
|
383
|
+
if (booted && typeof booted === "object") Object.assign(booted, { deletePending: false, noDeleteConfirmation: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
341
386
|
export { svc };
|