@mohasinac/appkit 4.31.1 → 4.32.1
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/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/account/components/UserBidsView.js +6 -1
- package/dist/features/account/components/UserOrdersView.js +8 -1
- package/dist/features/account/components/UserReturnsView.js +6 -1
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminAuditLogView.js +12 -1
- package/dist/features/admin/components/AdminBundlesView.js +5 -1
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminFaqsView.js +10 -1
- package/dist/features/admin/components/AdminNotificationsView.js +7 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminStoresView.js +13 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
- package/dist/features/admin/components/AdminTeamView.js +11 -1
- package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerBundlesView.js +5 -1
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerProductsView.js +10 -1
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/stores/actions/store-query-actions.js +5 -3
- package/dist/features/stores/repository/store.repository.d.ts +3 -1
- package/dist/features/stores/repository/store.repository.js +6 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +79 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -173,3 +173,84 @@ export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
|
|
|
173
173
|
thirdPartyUrl?: string | undefined;
|
|
174
174
|
}>;
|
|
175
175
|
export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
|
|
176
|
+
/**
|
|
177
|
+
* Ad SETTINGS — the provider credentials and the global consent switch.
|
|
178
|
+
*
|
|
179
|
+
* Distinct from `adminAdFormSchema`, which is one ad record. This is the
|
|
180
|
+
* platform-level configuration every ad renders under, and it was validated by
|
|
181
|
+
* a hand-written `localCredentialIssues` array that computed the same two rules
|
|
182
|
+
* on every keystroke and rendered them as a banner headed "Fix settings before
|
|
183
|
+
* saving" — never on the field that was wrong.
|
|
184
|
+
*
|
|
185
|
+
* Both credentials are optional and BLANK MEANS UNCHANGED: the route stores
|
|
186
|
+
* them encrypted and returns only a masked form, so the input starts empty on
|
|
187
|
+
* every load and an empty submit must leave the stored value alone. That is why
|
|
188
|
+
* neither field has a `.min()` — an empty string here is not an omission.
|
|
189
|
+
*/
|
|
190
|
+
export declare const adsConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
191
|
+
consentRequired: z.ZodBoolean;
|
|
192
|
+
adsenseClientId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
193
|
+
thirdPartyScriptUrl: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
194
|
+
}, "strip", z.ZodTypeAny, {
|
|
195
|
+
consentRequired: boolean;
|
|
196
|
+
adsenseClientId?: string | undefined;
|
|
197
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
198
|
+
}, {
|
|
199
|
+
consentRequired: boolean;
|
|
200
|
+
adsenseClientId?: string | undefined;
|
|
201
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
202
|
+
}>, {
|
|
203
|
+
consentRequired: boolean;
|
|
204
|
+
adsenseClientId?: string | undefined;
|
|
205
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
206
|
+
}, {
|
|
207
|
+
consentRequired: boolean;
|
|
208
|
+
adsenseClientId?: string | undefined;
|
|
209
|
+
thirdPartyScriptUrl?: string | undefined;
|
|
210
|
+
}>;
|
|
211
|
+
export type AdsConfigValues = z.infer<typeof adsConfigSchema>;
|
|
212
|
+
/**
|
|
213
|
+
* The support-ticket triage form — what an admin CHANGES about a ticket, as
|
|
214
|
+
* distinct from what they SAY on it (the reply box, which is its own write to
|
|
215
|
+
* a different endpoint and carries the ticket's new status with it).
|
|
216
|
+
*
|
|
217
|
+
* The five linked-party ids are FLAT here and reassembled into
|
|
218
|
+
* `relatedParties` at the payload boundary, the same call the ad creative and
|
|
219
|
+
* the user's public profile make: the stored shape is one nested object, and
|
|
220
|
+
* the form has always offered five discrete inputs.
|
|
221
|
+
*/
|
|
222
|
+
export declare const supportTicketUpdateSchema: z.ZodObject<{
|
|
223
|
+
status: z.ZodEnum<["open", "in_progress", "waiting_on_user", "resolved", "closed"]>;
|
|
224
|
+
priority: z.ZodEnum<["low", "normal", "high", "urgent"]>;
|
|
225
|
+
internalNotes: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
226
|
+
userId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
227
|
+
storeId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
228
|
+
orderId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
229
|
+
productId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
230
|
+
bidId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
231
|
+
}, "strip", z.ZodTypeAny, {
|
|
232
|
+
status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
|
|
233
|
+
priority: "normal" | "high" | "low" | "urgent";
|
|
234
|
+
storeId?: string | undefined;
|
|
235
|
+
userId?: string | undefined;
|
|
236
|
+
orderId?: string | undefined;
|
|
237
|
+
productId?: string | undefined;
|
|
238
|
+
bidId?: string | undefined;
|
|
239
|
+
internalNotes?: string | undefined;
|
|
240
|
+
}, {
|
|
241
|
+
status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
|
|
242
|
+
priority: "normal" | "high" | "low" | "urgent";
|
|
243
|
+
storeId?: string | undefined;
|
|
244
|
+
userId?: string | undefined;
|
|
245
|
+
orderId?: string | undefined;
|
|
246
|
+
productId?: string | undefined;
|
|
247
|
+
bidId?: string | undefined;
|
|
248
|
+
internalNotes?: string | undefined;
|
|
249
|
+
}>;
|
|
250
|
+
export type SupportTicketUpdateValues = z.infer<typeof supportTicketUpdateSchema>;
|
|
251
|
+
/**
|
|
252
|
+
* The five flat id fields, back into the nested `relatedParties` the ticket
|
|
253
|
+
* stores. Blank fields are dropped, and an entirely empty set returns
|
|
254
|
+
* `undefined` so the key is omitted rather than written as `{}`.
|
|
255
|
+
*/
|
|
256
|
+
export declare function toRelatedPartiesPayload(v: SupportTicketUpdateValues): Record<string, string> | undefined;
|
|
@@ -40,7 +40,11 @@ export const adminOrderUpdateSchema = z.object({
|
|
|
40
40
|
section: "shipping", sectionLabel: "Shipping", order: 1, row: "pair",
|
|
41
41
|
}),
|
|
42
42
|
carrier: annotate(z.string().trim().max(80).optional(), {
|
|
43
|
-
|
|
43
|
+
// `kind: "select"` is explicit because the field is a free string in the
|
|
44
|
+
// schema — the route accepts any carrier name — while the ADMIN form offers
|
|
45
|
+
// a fixed list. Without it the inferred control is a text input and the
|
|
46
|
+
// `options` passed by the view would be ignored.
|
|
47
|
+
section: "shipping", order: 2, row: "pair", kind: "select",
|
|
44
48
|
}),
|
|
45
49
|
/*
|
|
46
50
|
* 🛑 Bounded. This was `parseFloat` + `> 0` with no ceiling, so a slipped
|
|
@@ -229,3 +233,121 @@ export const adminAdFormSchema = z
|
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
});
|
|
236
|
+
/**
|
|
237
|
+
* Ad SETTINGS — the provider credentials and the global consent switch.
|
|
238
|
+
*
|
|
239
|
+
* Distinct from `adminAdFormSchema`, which is one ad record. This is the
|
|
240
|
+
* platform-level configuration every ad renders under, and it was validated by
|
|
241
|
+
* a hand-written `localCredentialIssues` array that computed the same two rules
|
|
242
|
+
* on every keystroke and rendered them as a banner headed "Fix settings before
|
|
243
|
+
* saving" — never on the field that was wrong.
|
|
244
|
+
*
|
|
245
|
+
* Both credentials are optional and BLANK MEANS UNCHANGED: the route stores
|
|
246
|
+
* them encrypted and returns only a masked form, so the input starts empty on
|
|
247
|
+
* every load and an empty submit must leave the stored value alone. That is why
|
|
248
|
+
* neither field has a `.min()` — an empty string here is not an omission.
|
|
249
|
+
*/
|
|
250
|
+
export const adsConfigSchema = z.object({
|
|
251
|
+
consentRequired: annotate(z.boolean(), {
|
|
252
|
+
section: "consent", sectionLabel: "Consent", sectionRequired: true,
|
|
253
|
+
quick: true, order: 1, row: "full",
|
|
254
|
+
label: "Require consent globally for ad rendering",
|
|
255
|
+
}),
|
|
256
|
+
adsenseClientId: annotate(z
|
|
257
|
+
.string()
|
|
258
|
+
.trim()
|
|
259
|
+
.regex(/^ca-pub-[0-9]{10,20}$/, "An AdSense client id looks like ca-pub-XXXXXXXXXX.")
|
|
260
|
+
.optional()
|
|
261
|
+
.or(z.literal("")), {
|
|
262
|
+
section: "credentials", sectionLabel: "Provider credentials",
|
|
263
|
+
order: 1, row: "pair", label: "AdSense client id",
|
|
264
|
+
help: "Leave blank to keep the stored value.",
|
|
265
|
+
}),
|
|
266
|
+
thirdPartyScriptUrl: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
|
|
267
|
+
section: "credentials", order: 2, row: "pair",
|
|
268
|
+
label: "Third-party script URL", inputType: "url",
|
|
269
|
+
help: "Must be https. Leave blank to keep the stored value.",
|
|
270
|
+
}),
|
|
271
|
+
})
|
|
272
|
+
.superRefine((v, ctx) => {
|
|
273
|
+
const url = v.thirdPartyScriptUrl?.trim();
|
|
274
|
+
if (!url)
|
|
275
|
+
return;
|
|
276
|
+
let parsed = null;
|
|
277
|
+
try {
|
|
278
|
+
parsed = new URL(url);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
void normalizeError(err);
|
|
282
|
+
parsed = null;
|
|
283
|
+
}
|
|
284
|
+
if (!parsed) {
|
|
285
|
+
ctx.addIssue({
|
|
286
|
+
code: z.ZodIssueCode.custom, path: ["thirdPartyScriptUrl"],
|
|
287
|
+
message: "That is not a valid URL.",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (parsed.protocol !== "https:") {
|
|
291
|
+
// A third-party ad script runs in the page's own origin; http would let
|
|
292
|
+
// any network hop rewrite it.
|
|
293
|
+
ctx.addIssue({
|
|
294
|
+
code: z.ZodIssueCode.custom, path: ["thirdPartyScriptUrl"],
|
|
295
|
+
message: "A third-party script URL must use https.",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* The support-ticket triage form — what an admin CHANGES about a ticket, as
|
|
301
|
+
* distinct from what they SAY on it (the reply box, which is its own write to
|
|
302
|
+
* a different endpoint and carries the ticket's new status with it).
|
|
303
|
+
*
|
|
304
|
+
* The five linked-party ids are FLAT here and reassembled into
|
|
305
|
+
* `relatedParties` at the payload boundary, the same call the ad creative and
|
|
306
|
+
* the user's public profile make: the stored shape is one nested object, and
|
|
307
|
+
* the form has always offered five discrete inputs.
|
|
308
|
+
*/
|
|
309
|
+
export const supportTicketUpdateSchema = z.object({
|
|
310
|
+
status: annotate(z.enum(["open", "in_progress", "waiting_on_user", "resolved", "closed"]), {
|
|
311
|
+
section: "triage", sectionLabel: "Triage", sectionRequired: true,
|
|
312
|
+
quick: true, order: 1, row: "pair", kind: "select",
|
|
313
|
+
}),
|
|
314
|
+
priority: annotate(z.enum(["low", "normal", "high", "urgent"]), {
|
|
315
|
+
section: "triage", quick: true, order: 2, row: "pair", kind: "select",
|
|
316
|
+
}),
|
|
317
|
+
internalNotes: annotate(z.string().trim().max(4000).optional().or(z.literal("")), {
|
|
318
|
+
section: "triage", order: 3, row: "full", kind: "textarea",
|
|
319
|
+
label: "Internal notes (staff only)",
|
|
320
|
+
help: "Visible only to admins and employees.",
|
|
321
|
+
}),
|
|
322
|
+
userId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
323
|
+
section: "parties", sectionLabel: "Linked parties", order: 1, row: "pair",
|
|
324
|
+
label: "User slug",
|
|
325
|
+
help: "Tag the buyer, store, order, product or bid this ticket concerns.",
|
|
326
|
+
}),
|
|
327
|
+
storeId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
328
|
+
section: "parties", order: 2, row: "pair", label: "Store slug",
|
|
329
|
+
}),
|
|
330
|
+
orderId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
331
|
+
section: "parties", order: 3, row: "pair", label: "Order ID",
|
|
332
|
+
}),
|
|
333
|
+
productId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
334
|
+
section: "parties", order: 4, row: "pair", label: "Product slug",
|
|
335
|
+
}),
|
|
336
|
+
bidId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
337
|
+
section: "parties", order: 5, row: "pair", label: "Bid ID",
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
/**
|
|
341
|
+
* The five flat id fields, back into the nested `relatedParties` the ticket
|
|
342
|
+
* stores. Blank fields are dropped, and an entirely empty set returns
|
|
343
|
+
* `undefined` so the key is omitted rather than written as `{}`.
|
|
344
|
+
*/
|
|
345
|
+
export function toRelatedPartiesPayload(v) {
|
|
346
|
+
const out = {};
|
|
347
|
+
for (const key of ["userId", "storeId", "orderId", "productId", "bidId"]) {
|
|
348
|
+
const value = v[key]?.trim();
|
|
349
|
+
if (value)
|
|
350
|
+
out[key] = value;
|
|
351
|
+
}
|
|
352
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
353
|
+
}
|
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { JsonValue } from "../../../schemas/types";
|
|
2
3
|
export declare const adminUserUpdateSchema: z.ZodObject<{
|
|
4
|
+
role: z.ZodOptional<z.ZodEnum<["user", "seller", "admin", "moderator"]>>;
|
|
5
|
+
emailVerified: z.ZodOptional<z.ZodBoolean>;
|
|
3
6
|
displayName: z.ZodOptional<z.ZodString>;
|
|
4
7
|
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
5
8
|
isTester: z.ZodOptional<z.ZodBoolean>;
|
|
6
9
|
canTestAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
7
10
|
adminNotes: z.ZodOptional<z.ZodString>;
|
|
8
|
-
|
|
11
|
+
bio: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
12
|
+
location: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
13
|
+
website: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
14
|
+
twitter: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
15
|
+
instagram: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
16
|
+
facebook: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
17
|
+
linkedin: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
9
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
role?: "user" | "seller" | "admin" | "moderator" | undefined;
|
|
10
20
|
displayName?: string | undefined;
|
|
21
|
+
location?: string | undefined;
|
|
11
22
|
adminNotes?: string | undefined;
|
|
23
|
+
bio?: string | undefined;
|
|
12
24
|
phoneNumber?: string | undefined;
|
|
25
|
+
emailVerified?: boolean | undefined;
|
|
26
|
+
website?: string | undefined;
|
|
13
27
|
isTester?: boolean | undefined;
|
|
14
28
|
canTestAdmin?: boolean | undefined;
|
|
15
|
-
|
|
29
|
+
instagram?: string | undefined;
|
|
30
|
+
facebook?: string | undefined;
|
|
31
|
+
twitter?: string | undefined;
|
|
32
|
+
linkedin?: string | undefined;
|
|
16
33
|
}, {
|
|
34
|
+
role?: "user" | "seller" | "admin" | "moderator" | undefined;
|
|
17
35
|
displayName?: string | undefined;
|
|
36
|
+
location?: string | undefined;
|
|
18
37
|
adminNotes?: string | undefined;
|
|
38
|
+
bio?: string | undefined;
|
|
19
39
|
phoneNumber?: string | undefined;
|
|
40
|
+
emailVerified?: boolean | undefined;
|
|
41
|
+
website?: string | undefined;
|
|
20
42
|
isTester?: boolean | undefined;
|
|
21
43
|
canTestAdmin?: boolean | undefined;
|
|
22
|
-
|
|
44
|
+
instagram?: string | undefined;
|
|
45
|
+
facebook?: string | undefined;
|
|
46
|
+
twitter?: string | undefined;
|
|
47
|
+
linkedin?: string | undefined;
|
|
23
48
|
}>;
|
|
49
|
+
/**
|
|
50
|
+
* The flat social/profile fields, reassembled into the nested shape the PATCH
|
|
51
|
+
* route stores.
|
|
52
|
+
*
|
|
53
|
+
* Empty strings are dropped rather than written: the drawer sends a partial
|
|
54
|
+
* PATCH so an untouched field must leave the stored subkey alone, and writing
|
|
55
|
+
* `""` would erase it. Returns `undefined` when nothing was filled in, so the
|
|
56
|
+
* key is omitted from the payload entirely.
|
|
57
|
+
*/
|
|
58
|
+
export declare function toPublicProfilePayload(v: {
|
|
59
|
+
bio?: string;
|
|
60
|
+
location?: string;
|
|
61
|
+
website?: string;
|
|
62
|
+
twitter?: string;
|
|
63
|
+
instagram?: string;
|
|
64
|
+
facebook?: string;
|
|
65
|
+
linkedin?: string;
|
|
66
|
+
}): Record<string, JsonValue> | undefined;
|
|
24
67
|
export type AdminUserUpdateValues = z.infer<typeof adminUserUpdateSchema>;
|
|
25
68
|
/**
|
|
26
69
|
* A store's WhatsApp Business connection.
|
|
@@ -51,6 +94,38 @@ export declare const whatsappSettingsSchema: z.ZodObject<{
|
|
|
51
94
|
catalogId?: string | undefined;
|
|
52
95
|
}>;
|
|
53
96
|
export type WhatsappSettingsValues = z.infer<typeof whatsappSettingsSchema>;
|
|
97
|
+
/**
|
|
98
|
+
* The CONNECTION form's fields — credentials only.
|
|
99
|
+
*
|
|
100
|
+
* `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
|
|
101
|
+
* card and owns its own write (`syncToggleMutation`), because the credentials
|
|
102
|
+
* form used to send it unconditionally from two cards away: a seller flipped
|
|
103
|
+
* the switch, saw it move, and nothing persisted it. A field in this schema is
|
|
104
|
+
* a field this form's Save button owns.
|
|
105
|
+
*
|
|
106
|
+
* 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
|
|
107
|
+
* carries the SAME field instances into the new shape, which is what keeps the
|
|
108
|
+
* `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
|
|
109
|
+
* transparent for the same reason; a field-level re-wrap is not.)
|
|
110
|
+
*/
|
|
111
|
+
export declare const whatsappConnectionSchema: z.ZodObject<Omit<{
|
|
112
|
+
catalogSyncEnabled: z.ZodBoolean;
|
|
113
|
+
phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
114
|
+
wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
115
|
+
catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
116
|
+
accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
117
|
+
}, "catalogSyncEnabled">, "strip", z.ZodTypeAny, {
|
|
118
|
+
phoneNumber?: string | undefined;
|
|
119
|
+
accessToken?: string | undefined;
|
|
120
|
+
wabaId?: string | undefined;
|
|
121
|
+
catalogId?: string | undefined;
|
|
122
|
+
}, {
|
|
123
|
+
phoneNumber?: string | undefined;
|
|
124
|
+
accessToken?: string | undefined;
|
|
125
|
+
wabaId?: string | undefined;
|
|
126
|
+
catalogId?: string | undefined;
|
|
127
|
+
}>;
|
|
128
|
+
export type WhatsappConnectionValues = z.infer<typeof whatsappConnectionSchema>;
|
|
54
129
|
/**
|
|
55
130
|
* A buyer's lottery entry.
|
|
56
131
|
*
|
|
@@ -30,13 +30,26 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import { annotate } from "../../shell/field-ui-meta";
|
|
33
|
-
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
34
33
|
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
35
34
|
export const adminUserUpdateSchema = z.object({
|
|
35
|
+
/*
|
|
36
|
+
* Role and email-verification lead, because they are what an admin opens this
|
|
37
|
+
* drawer for. They were controls with no schema entry at all — the form
|
|
38
|
+
* declared a schema covering six of its twelve fields, so `Role` and
|
|
39
|
+
* `Email verified` were validated by nothing.
|
|
40
|
+
*/
|
|
41
|
+
role: annotate(z.enum(["user", "seller", "admin", "moderator"]).optional(), {
|
|
42
|
+
section: "profile", sectionLabel: "Profile", sectionRequired: true, quick: true,
|
|
43
|
+
order: 1, row: "pair", kind: "select",
|
|
44
|
+
}),
|
|
45
|
+
emailVerified: annotate(z.boolean().optional(), {
|
|
46
|
+
section: "profile", quick: true, order: 2, row: "quarter",
|
|
47
|
+
label: "Email verified",
|
|
48
|
+
}),
|
|
36
49
|
displayName: annotate(z.string().trim().min(1).max(120).optional(), {
|
|
37
|
-
section: "profile",
|
|
50
|
+
section: "profile", quick: true, order: 3, row: "pair",
|
|
38
51
|
}),
|
|
39
|
-
phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "profile", order:
|
|
52
|
+
phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "profile", order: 4, row: "pair" }),
|
|
40
53
|
/*
|
|
41
54
|
* The tester flags. Kept OPTIONAL rather than defaulted, deliberately: a
|
|
42
55
|
* PATCH that omits them must leave them alone. Defaulting either to `false`
|
|
@@ -48,17 +61,76 @@ export const adminUserUpdateSchema = z.object({
|
|
|
48
61
|
}),
|
|
49
62
|
canTestAdmin: annotate(z.boolean().optional(), {
|
|
50
63
|
section: "access", order: 2, row: "quarter",
|
|
64
|
+
// The drawer already hid this behind `isTester`; stating it here is what
|
|
65
|
+
// makes `visibleValues()` drop a stale `true` when the flag is switched off.
|
|
66
|
+
when: (v) => Boolean(v.isTester),
|
|
51
67
|
}),
|
|
52
68
|
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
53
69
|
section: "access", order: 3, row: "full", kind: "textarea",
|
|
54
70
|
help: "Internal only — never shown to the user.",
|
|
55
71
|
}),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
/*
|
|
73
|
+
* The public profile, FLAT — the same call the ad form makes.
|
|
74
|
+
*
|
|
75
|
+
* The STORED shape is an open record (`publicProfileSchema` on the route),
|
|
76
|
+
* because per-user profile keys are added over time and a closed shape would
|
|
77
|
+
* strip a newly-added one. That was the right conclusion for the document and
|
|
78
|
+
* the wrong one for the FORM: as one `kind: "list"` field it renders as a
|
|
79
|
+
* disabled placeholder, while the drawer has always offered seven discrete
|
|
80
|
+
* inputs — bio, location, website and four social handles — validated by
|
|
81
|
+
* nothing. The nested shape is reassembled at the payload boundary, where the
|
|
82
|
+
* API wants it, by `toPublicProfilePayload()` below.
|
|
83
|
+
*/
|
|
84
|
+
bio: annotate(z.string().trim().max(1000).optional().or(z.literal("")), {
|
|
85
|
+
section: "public", sectionLabel: "Public profile", order: 1, row: "full",
|
|
86
|
+
kind: "textarea", help: "Shown on the user's public profile page.",
|
|
87
|
+
}),
|
|
88
|
+
location: annotate(z.string().trim().max(120).optional().or(z.literal("")), {
|
|
89
|
+
section: "public", order: 2, row: "pair",
|
|
90
|
+
}),
|
|
91
|
+
website: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
|
|
92
|
+
section: "public", order: 3, row: "pair", inputType: "url",
|
|
93
|
+
}),
|
|
94
|
+
twitter: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
95
|
+
section: "social", sectionLabel: "Social links", order: 1, row: "pair",
|
|
96
|
+
label: "Twitter / X",
|
|
97
|
+
}),
|
|
98
|
+
instagram: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
99
|
+
section: "social", order: 2, row: "pair",
|
|
100
|
+
}),
|
|
101
|
+
facebook: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
102
|
+
section: "social", order: 3, row: "pair",
|
|
103
|
+
}),
|
|
104
|
+
linkedin: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
|
|
105
|
+
section: "social", order: 4, row: "pair", label: "LinkedIn",
|
|
60
106
|
}),
|
|
61
107
|
});
|
|
108
|
+
/**
|
|
109
|
+
* The flat social/profile fields, reassembled into the nested shape the PATCH
|
|
110
|
+
* route stores.
|
|
111
|
+
*
|
|
112
|
+
* Empty strings are dropped rather than written: the drawer sends a partial
|
|
113
|
+
* PATCH so an untouched field must leave the stored subkey alone, and writing
|
|
114
|
+
* `""` would erase it. Returns `undefined` when nothing was filled in, so the
|
|
115
|
+
* key is omitted from the payload entirely.
|
|
116
|
+
*/
|
|
117
|
+
export function toPublicProfilePayload(v) {
|
|
118
|
+
const socialLinks = {};
|
|
119
|
+
for (const key of ["twitter", "instagram", "facebook", "linkedin"]) {
|
|
120
|
+
const value = v[key]?.trim();
|
|
121
|
+
if (value)
|
|
122
|
+
socialLinks[key] = value;
|
|
123
|
+
}
|
|
124
|
+
const publicProfile = {};
|
|
125
|
+
for (const key of ["bio", "location", "website"]) {
|
|
126
|
+
const value = v[key]?.trim();
|
|
127
|
+
if (value)
|
|
128
|
+
publicProfile[key] = value;
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(socialLinks).length > 0)
|
|
131
|
+
publicProfile.socialLinks = socialLinks;
|
|
132
|
+
return Object.keys(publicProfile).length > 0 ? publicProfile : undefined;
|
|
133
|
+
}
|
|
62
134
|
/**
|
|
63
135
|
* A store's WhatsApp Business connection.
|
|
64
136
|
*
|
|
@@ -79,6 +151,23 @@ export const whatsappSettingsSchema = z.object({
|
|
|
79
151
|
catalogId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A catalog ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 3, row: "pair" }),
|
|
80
152
|
accessToken: annotate(z.string().trim().min(20, "That access token looks truncated.").optional().or(z.literal("")), { section: "connection", order: 4, row: "full", help: "Leave blank to keep the current token." }),
|
|
81
153
|
});
|
|
154
|
+
/**
|
|
155
|
+
* The CONNECTION form's fields — credentials only.
|
|
156
|
+
*
|
|
157
|
+
* `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
|
|
158
|
+
* card and owns its own write (`syncToggleMutation`), because the credentials
|
|
159
|
+
* form used to send it unconditionally from two cards away: a seller flipped
|
|
160
|
+
* the switch, saw it move, and nothing persisted it. A field in this schema is
|
|
161
|
+
* a field this form's Save button owns.
|
|
162
|
+
*
|
|
163
|
+
* 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
|
|
164
|
+
* carries the SAME field instances into the new shape, which is what keeps the
|
|
165
|
+
* `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
|
|
166
|
+
* transparent for the same reason; a field-level re-wrap is not.)
|
|
167
|
+
*/
|
|
168
|
+
export const whatsappConnectionSchema = whatsappSettingsSchema.omit({
|
|
169
|
+
catalogSyncEnabled: true,
|
|
170
|
+
});
|
|
82
171
|
/**
|
|
83
172
|
* A buyer's lottery entry.
|
|
84
173
|
*
|
|
@@ -20,3 +20,12 @@ export declare const homepageSectionFormSchema: z.ZodObject<{
|
|
|
20
20
|
order?: number | undefined;
|
|
21
21
|
}>;
|
|
22
22
|
export type HomepageSectionFormValues = z.infer<typeof homepageSectionFormSchema>;
|
|
23
|
+
/**
|
|
24
|
+
* What the FORM holds, as distinct from what the schema produces.
|
|
25
|
+
*
|
|
26
|
+
* `order` is a `z.coerce.number()`, so the parsed value is a number while the
|
|
27
|
+
* control that feeds it is a text input holding a string — including the empty
|
|
28
|
+
* string, which means "auto-place". The draft is the INPUT side; only the save
|
|
29
|
+
* path sees the output.
|
|
30
|
+
*/
|
|
31
|
+
export type HomepageSectionFormInput = z.input<typeof homepageSectionFormSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PageTabDef {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PageTabsProps {
|
|
7
|
+
/** The tabs, in order. The first is the default when `?tab=` is absent or unknown. */
|
|
8
|
+
tabs: readonly PageTabDef[];
|
|
9
|
+
/** One node per tab id. A missing entry renders an empty panel, not a crash. */
|
|
10
|
+
panels: Record<string, React.ReactNode>;
|
|
11
|
+
/**
|
|
12
|
+
* Override the default tab. Omit to use the first — which is right whenever
|
|
13
|
+
* the host page's own content is the first tab, as it is on all six.
|
|
14
|
+
*/
|
|
15
|
+
defaultTabId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function PageTabs({ tabs, panels, defaultTabId }: PageTabsProps): React.JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: W8's C2 folds sibling pages that are one task into one tabbed page, and
|
|
5
|
+
* four of the six hosts are SERVER components that fetch their panels'
|
|
6
|
+
* data. Tabs need client state, so something has to sit between.
|
|
7
|
+
*
|
|
8
|
+
* WHAT: The tab strip and the panel switch, URL-synced via `?tab=`.
|
|
9
|
+
*
|
|
10
|
+
* ## 🛑 Why the tab list is DATA and the guard is derived
|
|
11
|
+
*
|
|
12
|
+
* A Server Component cannot hand a function to a Client Component — it is not
|
|
13
|
+
* serialisable, and the failure is a build error at best and a React #441 at
|
|
14
|
+
* worst (Root Cause #76). So this takes the tab ARRAY and derives the
|
|
15
|
+
* "is this a real tab id" test from it, rather than taking an `isValid`
|
|
16
|
+
* predicate the way `useTabParam` does for callers that are already clients.
|
|
17
|
+
*
|
|
18
|
+
* Panels are passed as a record of nodes, which IS serialisable: a Server
|
|
19
|
+
* Component renders both panels and hands over the results.
|
|
20
|
+
*
|
|
21
|
+
* ## Every panel is rendered, always
|
|
22
|
+
*
|
|
23
|
+
* `TabsContent` mounts all of them and shows one. That is deliberate for these
|
|
24
|
+
* six pages: the absorbed panels are small, their data was already fetched
|
|
25
|
+
* server-side by the same request, and mounting on first view would make a tab
|
|
26
|
+
* click cost a spinner for content that is already in the payload.
|
|
27
|
+
*
|
|
28
|
+
* EXPORTS: PageTabs, type PageTabDef
|
|
29
|
+
*
|
|
30
|
+
* @tag domain:layout
|
|
31
|
+
* @tag layer:component
|
|
32
|
+
* @tag pattern:none
|
|
33
|
+
* @tag access:client
|
|
34
|
+
* @tag consumers:store payouts/shipping/fulfillment/storefront,admin roles/settings
|
|
35
|
+
* @tag sideEffects:router
|
|
36
|
+
*/
|
|
37
|
+
import React from "react";
|
|
38
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui";
|
|
39
|
+
import { useTabParam } from "../../react/hooks/useTabParam";
|
|
40
|
+
export function PageTabs({ tabs, panels, defaultTabId }) {
|
|
41
|
+
const ids = React.useMemo(() => tabs.map((t) => t.id), [tabs]);
|
|
42
|
+
const isValid = React.useCallback((value) => !!value && ids.includes(value), [ids]);
|
|
43
|
+
const [activeTab, setActiveTab] = useTabParam(isValid, defaultTabId ?? tabs[0]?.id ?? "");
|
|
44
|
+
return (_jsxs(Tabs, { value: activeTab, onChange: setActiveTab, children: [_jsx(TabsList, { children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, children: tab.label }, tab.id))) }), tabs.map((tab) => (_jsx(TabsContent, { value: tab.id, children: panels[tab.id] ?? null }, tab.id)))] }));
|
|
45
|
+
}
|