@aglyn/plugins-commerce 1.0.0-beta.219 → 1.0.0-beta.221

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.
Files changed (83) hide show
  1. package/README.md +5 -0
  2. package/package.json +14 -14
  3. package/src/lib/components/console/catalog-organization-card.component.js +626 -446
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  6. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  7. package/src/lib/components/console/product-editor-dialog.component.js +1127 -846
  8. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  9. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  10. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  11. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  12. package/src/lib/components/member-signup.js +2 -2
  13. package/src/lib/components/member-signup.js.map +1 -1
  14. package/src/lib/components/newsletter-signup.js +2 -2
  15. package/src/lib/components/newsletter-signup.js.map +1 -1
  16. package/src/lib/constants/operator-alerts.js +5 -3
  17. package/src/lib/constants/operator-alerts.js.map +1 -1
  18. package/src/lib/model/commerce-io.js +1 -1
  19. package/src/lib/model/commerce-io.js.map +1 -1
  20. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  21. package/src/lib/model/commerce-record-lists.js +45 -0
  22. package/src/lib/model/commerce-record-lists.js.map +1 -0
  23. package/src/lib/model/commerce-record-routes.d.ts +18 -0
  24. package/src/lib/model/commerce-record-routes.js +1 -1
  25. package/src/lib/model/commerce-record-routes.js.map +1 -1
  26. package/src/lib/model/commerce-search-sources.js +4 -2
  27. package/src/lib/model/commerce-search-sources.js.map +1 -1
  28. package/src/lib/model/commerce-topics.d.ts +20 -0
  29. package/src/lib/model/commerce-topics.js +33 -0
  30. package/src/lib/model/commerce-topics.js.map +1 -0
  31. package/src/lib/model/index.d.ts +1 -0
  32. package/src/lib/model/index.js +1 -0
  33. package/src/lib/model/index.js.map +1 -1
  34. package/src/lib/model/product-record.d.ts +32 -0
  35. package/src/lib/model/product-record.js +82 -0
  36. package/src/lib/model/product-record.js.map +1 -0
  37. package/src/lib/model/site-member-purchases.d.ts +48 -0
  38. package/src/lib/model/site-member-purchases.js +50 -0
  39. package/src/lib/model/site-member-purchases.js.map +1 -0
  40. package/src/lib/plugin.js +12 -0
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +410 -208
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/cart-checkout.js +1 -1
  45. package/src/lib/server/cart-checkout.js.map +1 -1
  46. package/src/lib/server/gift-cards.js +7 -3
  47. package/src/lib/server/gift-cards.js.map +1 -1
  48. package/src/lib/server/low-stock.js +3 -1
  49. package/src/lib/server/low-stock.js.map +1 -1
  50. package/src/lib/server/member-post.js +2 -2
  51. package/src/lib/server/member-post.js.map +1 -1
  52. package/src/lib/server/membership-admin-password.js +4 -1
  53. package/src/lib/server/membership-admin-password.js.map +1 -1
  54. package/src/lib/server/membership-register.js +13 -2
  55. package/src/lib/server/membership-register.js.map +1 -1
  56. package/src/lib/server/newsletter.js +32 -19
  57. package/src/lib/server/newsletter.js.map +1 -1
  58. package/src/lib/server/pos-order.js +18 -10
  59. package/src/lib/server/pos-order.js.map +1 -1
  60. package/src/lib/server/process-abandoned.js +4 -2
  61. package/src/lib/server/process-abandoned.js.map +1 -1
  62. package/src/lib/server/process-restock.js +7 -5
  63. package/src/lib/server/process-restock.js.map +1 -1
  64. package/src/lib/server/product-record-index.js +5 -37
  65. package/src/lib/server/product-record-index.js.map +1 -1
  66. package/src/lib/server/reconcile-stock.js +1 -1
  67. package/src/lib/server/reconcile-stock.js.map +1 -1
  68. package/src/lib/server/reserve-stock.js +6 -1
  69. package/src/lib/server/reserve-stock.js.map +1 -1
  70. package/src/lib/server/revenue-source.js +3 -3
  71. package/src/lib/server/revenue-source.js.map +1 -1
  72. package/src/lib/server/supplier-outbox.js +16 -1
  73. package/src/lib/server/supplier-outbox.js.map +1 -1
  74. package/src/lib/server/supplier-update.js +19 -9
  75. package/src/lib/server/supplier-update.js.map +1 -1
  76. package/src/lib/starter-templates.d.ts +19 -0
  77. package/src/lib/starter-templates.js +157 -0
  78. package/src/lib/starter-templates.js.map +1 -0
  79. package/src/lib/tenant-emails.js +54 -14
  80. package/src/lib/tenant-emails.js.map +1 -1
  81. package/src/lib/testing/stand-in-record-system.d.ts +11 -2
  82. package/src/lib/testing/stand-in-record-system.js +5 -3
  83. package/src/lib/testing/stand-in-record-system.js.map +1 -1
@@ -30,6 +30,7 @@ import { useMediaPicker } from "@aglyn/aglyn";
30
30
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
31
31
  import { seoListingFieldCount, seoListingFieldTooLong } from "@aglyn/aglyn/app-utils/seo-listing-fields";
32
32
  import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
33
+ import { useContentStable } from "@aglyn/shared-ui-jsx/hooks/use-content-stable";
33
34
  import { EntitlementUpsell, useCommerceEntitlement } from "./entitlement-gate.component.js";
34
35
  import { PRODUCT_EDITOR_ZONE } from "./product-zones.js";
35
36
  import { MembersVideosField, PaidDownloadAddButton, PaidMediaProtection } from "./paid-media.js";
@@ -74,6 +75,9 @@ function comboLabel(options) {
74
75
  return values.length ? values.join(' / ') : 'Default';
75
76
  }
76
77
  /** Stand-in for a list the product does not hold yet, the same one each render. */ const NO_ITEMS = [];
78
+ const NO_OPTIONS = [];
79
+ const NO_FILES = [];
80
+ const NO_VIDEOS = [];
77
81
  const FILL_ROW = {
78
82
  flex: 1
79
83
  };
@@ -116,6 +120,945 @@ const sameRecord = (option, value)=>option.$id === value.$id;
116
120
  sx: props.fill ? FILL_ROW : undefined
117
121
  });
118
122
  });
123
+ /**
124
+ * A text field that redraws only when what it shows changes (AGL-3423).
125
+ *
126
+ * Every field in this editor writes one draft, so a keystroke in any of them
127
+ * re-renders the dialog, and drawn whole that is some forty inputs and twelve
128
+ * hundred components for one letter in a product with four variants. When a
129
+ * keystroke costs that much, typed input queues and is processed back to
130
+ * back, which is what lets a chip list's pending update climb to #185 (see
131
+ * `ProductChipsField`). The sections below are memoized on the slice of the
132
+ * draft they show, and each field in them is this, handed a handler and
133
+ * styles that keep their identity, so the keystroke redraws the field it
134
+ * lands in.
135
+ */ const MemoTextField = /*#__PURE__*/ memo(TextField);
136
+ const FIRST_ROW = {
137
+ mt: 1
138
+ };
139
+ const WIDE_ROW = {
140
+ xs: 'column',
141
+ sm: 'row'
142
+ };
143
+ const NARROW_SELECT = {
144
+ minWidth: 140
145
+ };
146
+ const SUPPLIER_SELECT = {
147
+ minWidth: 160
148
+ };
149
+ const OVERSELL_SELECT = {
150
+ minWidth: 200
151
+ };
152
+ const KIND_SELECT = {
153
+ minWidth: 130
154
+ };
155
+ const SHORT_SELECT = {
156
+ minWidth: 120
157
+ };
158
+ const BILLING_SELECT = {
159
+ minWidth: 180
160
+ };
161
+ const COUNT_FIELD = {
162
+ width: 140
163
+ };
164
+ const VERSION_FIELD = {
165
+ width: 100
166
+ };
167
+ const OPTION_FIELD = {
168
+ width: 160
169
+ };
170
+ const PRICE_CELL = {
171
+ width: 88
172
+ };
173
+ const CODE_CELL = {
174
+ width: 110
175
+ };
176
+ const STOCK_CELL = {
177
+ width: 72
178
+ };
179
+ const DECIMAL_INPUT = {
180
+ htmlInput: {
181
+ inputMode: 'decimal'
182
+ }
183
+ };
184
+ const NUMERIC_INPUT = {
185
+ htmlInput: {
186
+ inputMode: 'numeric'
187
+ }
188
+ };
189
+ const TYPE_MENU = [
190
+ /*#__PURE__*/ _jsx(MenuItem, {
191
+ value: "physical",
192
+ children: 'Physical'
193
+ }, "physical"),
194
+ /*#__PURE__*/ _jsx(MenuItem, {
195
+ value: "digital",
196
+ children: 'Digital'
197
+ }, "digital"),
198
+ /*#__PURE__*/ _jsx(MenuItem, {
199
+ value: "service",
200
+ children: 'Service'
201
+ }, "service")
202
+ ];
203
+ const STATUS_MENU = [
204
+ /*#__PURE__*/ _jsx(MenuItem, {
205
+ value: "draft",
206
+ children: 'Draft'
207
+ }, "draft"),
208
+ /*#__PURE__*/ _jsx(MenuItem, {
209
+ value: "active",
210
+ children: 'Active'
211
+ }, "active"),
212
+ /*#__PURE__*/ _jsx(MenuItem, {
213
+ value: "archived",
214
+ children: 'Archived'
215
+ }, "archived")
216
+ ];
217
+ const OVERSELL_MENU = [
218
+ /*#__PURE__*/ _jsx(MenuItem, {
219
+ value: "deny",
220
+ children: 'Stop selling (sold out)'
221
+ }, "deny"),
222
+ /*#__PURE__*/ _jsx(MenuItem, {
223
+ value: "backorder",
224
+ children: 'Keep selling (backorder)'
225
+ }, "backorder")
226
+ ];
227
+ const TAX_MENU = [
228
+ /*#__PURE__*/ _jsx(MenuItem, {
229
+ value: "taxable",
230
+ children: 'Taxable'
231
+ }, "taxable"),
232
+ /*#__PURE__*/ _jsx(MenuItem, {
233
+ value: "exempt",
234
+ children: 'Tax exempt'
235
+ }, "exempt")
236
+ ];
237
+ const INTERVAL_MENU = [
238
+ /*#__PURE__*/ _jsx(MenuItem, {
239
+ value: "month",
240
+ children: 'Monthly'
241
+ }, "month"),
242
+ /*#__PURE__*/ _jsx(MenuItem, {
243
+ value: "year",
244
+ children: 'Yearly'
245
+ }, "year")
246
+ ];
247
+ /**
248
+ * A zone hosted in the dialog, drawn again only when what it is handed
249
+ * changes (AGL-3423). The shell's renderer is not memoized: drawn straight
250
+ * into the dialog, every widget in the zone would redraw on each keystroke in
251
+ * any field.
252
+ */ const HostedZone = /*#__PURE__*/ memo(function HostedZone(props) {
253
+ const { renderer: Renderer } = props, zone = _object_without_properties_loose(props, [
254
+ "renderer"
255
+ ]);
256
+ return /*#__PURE__*/ _jsx(Renderer, _extends({}, zone));
257
+ });
258
+ /** The product's name and address, type, status, supplier and description. */ const ProductBasicsFields = /*#__PURE__*/ memo(function ProductBasicsFields(props) {
259
+ var _props_supplierId;
260
+ const { suppliers, update } = props;
261
+ const setType = useCallback((event)=>update({
262
+ type: event.target.value
263
+ }), [
264
+ update
265
+ ]);
266
+ const setStatus = useCallback((event)=>update({
267
+ status: event.target.value
268
+ }), [
269
+ update
270
+ ]);
271
+ const setSupplier = useCallback((event)=>update({
272
+ supplierId: event.target.value || undefined
273
+ }), [
274
+ update
275
+ ]);
276
+ const setDescription = useCallback((event)=>update({
277
+ description: event.target.value
278
+ }), [
279
+ update
280
+ ]);
281
+ const supplierMenu = useMemo(()=>[
282
+ /*#__PURE__*/ _jsx(MenuItem, {
283
+ value: "",
284
+ children: 'None (self-fulfilled)'
285
+ }, ""),
286
+ ...suppliers.map((supplier)=>/*#__PURE__*/ _jsx(MenuItem, {
287
+ value: supplier.$id,
288
+ children: supplier.name
289
+ }, supplier.$id))
290
+ ], [
291
+ suppliers
292
+ ]);
293
+ return /*#__PURE__*/ _jsxs(_Fragment, {
294
+ children: [
295
+ /*#__PURE__*/ _jsxs(Stack, {
296
+ direction: WIDE_ROW,
297
+ spacing: 2,
298
+ sx: FIRST_ROW,
299
+ children: [
300
+ /*#__PURE__*/ _jsx(MemoTextField, {
301
+ label: "Name",
302
+ value: props.name,
303
+ onChange: props.onName,
304
+ size: "small",
305
+ autoFocus: true,
306
+ fullWidth: true
307
+ }),
308
+ /*#__PURE__*/ _jsx(MemoTextField, {
309
+ label: "Slug",
310
+ value: props.slug,
311
+ onChange: props.onSlug,
312
+ size: "small",
313
+ fullWidth: true,
314
+ helperText: props.slug ? `/products/${props.slug}` : undefined
315
+ })
316
+ ]
317
+ }),
318
+ /*#__PURE__*/ _jsxs(Stack, {
319
+ direction: "row",
320
+ spacing: 2,
321
+ children: [
322
+ /*#__PURE__*/ _jsx(MemoTextField, {
323
+ label: "Type",
324
+ value: props.type,
325
+ onChange: setType,
326
+ size: "small",
327
+ select: true,
328
+ sx: NARROW_SELECT,
329
+ children: TYPE_MENU
330
+ }),
331
+ /*#__PURE__*/ _jsx(MemoTextField, {
332
+ label: "Status",
333
+ value: props.status,
334
+ onChange: setStatus,
335
+ size: "small",
336
+ select: true,
337
+ sx: NARROW_SELECT,
338
+ children: STATUS_MENU
339
+ }),
340
+ suppliers.length > 0 ? /*#__PURE__*/ _jsx(MemoTextField, {
341
+ label: "Supplier",
342
+ value: (_props_supplierId = props.supplierId) != null ? _props_supplierId : '',
343
+ onChange: setSupplier,
344
+ size: "small",
345
+ select: true,
346
+ sx: SUPPLIER_SELECT,
347
+ helperText: "Routes paid orders",
348
+ children: supplierMenu
349
+ }) : null
350
+ ]
351
+ }),
352
+ /*#__PURE__*/ _jsx(MemoTextField, {
353
+ label: "Description",
354
+ value: props.description,
355
+ onChange: setDescription,
356
+ size: "small",
357
+ multiline: true,
358
+ minRows: 2
359
+ })
360
+ ]
361
+ });
362
+ });
363
+ /** The product's images, each removable, and the button that adds one. */ const ProductMediaFields = /*#__PURE__*/ memo(function ProductMediaFields(props) {
364
+ const { mediaUrls, update, pick } = props;
365
+ return /*#__PURE__*/ _jsxs(Box, {
366
+ sx: {
367
+ display: 'flex',
368
+ gap: 1,
369
+ flexWrap: 'wrap'
370
+ },
371
+ children: [
372
+ mediaUrls.map((url, index)=>/*#__PURE__*/ _jsxs(Box, {
373
+ sx: {
374
+ position: 'relative'
375
+ },
376
+ children: [
377
+ /*#__PURE__*/ _jsx(Box, {
378
+ component: "img",
379
+ src: url,
380
+ alt: "",
381
+ sx: {
382
+ width: 72,
383
+ height: 72,
384
+ objectFit: 'cover',
385
+ borderRadius: 1,
386
+ border: 1,
387
+ borderColor: 'divider'
388
+ }
389
+ }),
390
+ /*#__PURE__*/ _jsx(IconButton, {
391
+ size: "small",
392
+ "aria-label": "Remove image",
393
+ onClick: ()=>update((product)=>{
394
+ var _product_mediaUrls;
395
+ return {
396
+ mediaUrls: ((_product_mediaUrls = product.mediaUrls) != null ? _product_mediaUrls : []).filter((_item, itemIndex)=>itemIndex !== index)
397
+ };
398
+ }),
399
+ sx: {
400
+ position: 'absolute',
401
+ top: -8,
402
+ right: -8,
403
+ bgcolor: 'background.paper',
404
+ border: 1,
405
+ borderColor: 'divider',
406
+ p: 0.25
407
+ },
408
+ children: '✕'
409
+ })
410
+ ]
411
+ }, `${url}-${index}`)),
412
+ /*#__PURE__*/ _jsx(Button, {
413
+ size: "small",
414
+ onClick: ()=>void pick((media)=>update((product)=>{
415
+ var _product_mediaUrls;
416
+ return {
417
+ mediaUrls: [
418
+ ...(_product_mediaUrls = product.mediaUrls) != null ? _product_mediaUrls : [],
419
+ media.url
420
+ ]
421
+ };
422
+ })),
423
+ children: 'Add image'
424
+ })
425
+ ]
426
+ });
427
+ });
428
+ const ProductOptionRow = /*#__PURE__*/ memo(function ProductOptionRow(props) {
429
+ const { index, option, onChange, onValues } = props;
430
+ const setName = useCallback((event)=>onChange(index, {
431
+ name: event.target.value
432
+ }), [
433
+ index,
434
+ onChange
435
+ ]);
436
+ return /*#__PURE__*/ _jsxs(Stack, {
437
+ direction: "row",
438
+ spacing: 1,
439
+ sx: {
440
+ alignItems: 'flex-start'
441
+ },
442
+ children: [
443
+ /*#__PURE__*/ _jsx(MemoTextField, {
444
+ label: "Option",
445
+ value: option.name,
446
+ onChange: setName,
447
+ size: "small",
448
+ sx: OPTION_FIELD,
449
+ placeholder: "Size"
450
+ }),
451
+ /*#__PURE__*/ _jsx(ProductChipsField, {
452
+ freeSolo: true,
453
+ fill: true,
454
+ label: "Values",
455
+ placeholder: "S, M, L…",
456
+ options: NO_ITEMS,
457
+ value: option.values,
458
+ index: index,
459
+ onChange: onValues
460
+ }),
461
+ /*#__PURE__*/ _jsx(Button, {
462
+ size: "small",
463
+ color: "error",
464
+ onClick: ()=>onChange(index, null),
465
+ sx: {
466
+ mt: 0.5
467
+ },
468
+ children: 'Remove'
469
+ })
470
+ ]
471
+ });
472
+ });
473
+ /** The product's options, one row each, and the button that adds one. */ const ProductOptionsFields = /*#__PURE__*/ memo(function ProductOptionsFields(props) {
474
+ const { options, onChange, onValues } = props;
475
+ return /*#__PURE__*/ _jsxs(_Fragment, {
476
+ children: [
477
+ options.map((option, index)=>/*#__PURE__*/ _jsx(ProductOptionRow, {
478
+ index: index,
479
+ option: option,
480
+ onChange: onChange,
481
+ onValues: onValues
482
+ }, index)),
483
+ options.length < CommerceModel.COMMERCE_MAX_OPTIONS ? /*#__PURE__*/ _jsx(Button, {
484
+ size: "small",
485
+ sx: {
486
+ alignSelf: 'flex-start'
487
+ },
488
+ onClick: ()=>onChange(options.length, {
489
+ name: '',
490
+ values: []
491
+ }),
492
+ children: 'Add option'
493
+ }) : null
494
+ ]
495
+ });
496
+ });
497
+ const ProductVariantRow = /*#__PURE__*/ memo(function ProductVariantRow(props) {
498
+ var _variant_priceUsd, _variant_compareAtPriceUsd, _variant_sku, _variant_barcode, _variant_inventory, _variant_weightGrams;
499
+ const { index, variant, stockApplies, onField } = props;
500
+ const label = comboLabel(variant.options);
501
+ // One handler per cell, each keeping its identity while the row's does.
502
+ const set = useMemo(()=>{
503
+ const cell = (field)=>(event)=>onField(index, field, event.target.value);
504
+ return {
505
+ priceUsd: cell('priceUsd'),
506
+ compareAtPriceUsd: cell('compareAtPriceUsd'),
507
+ sku: cell('sku'),
508
+ barcode: cell('barcode'),
509
+ inventory: cell('inventory'),
510
+ weightGrams: cell('weightGrams')
511
+ };
512
+ }, [
513
+ index,
514
+ onField
515
+ ]);
516
+ const stockInput = useMemo(()=>({
517
+ htmlInput: {
518
+ inputMode: 'numeric',
519
+ 'aria-label': `Stock — ${label}`
520
+ }
521
+ }), [
522
+ label
523
+ ]);
524
+ return /*#__PURE__*/ _jsxs(TableRow, {
525
+ children: [
526
+ /*#__PURE__*/ _jsx(TableCell, {
527
+ sx: {
528
+ whiteSpace: 'nowrap'
529
+ },
530
+ children: label
531
+ }),
532
+ /*#__PURE__*/ _jsx(TableCell, {
533
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
534
+ value: (_variant_priceUsd = variant.priceUsd) != null ? _variant_priceUsd : '',
535
+ onChange: set.priceUsd,
536
+ size: "small",
537
+ sx: PRICE_CELL,
538
+ // An empty price is marked (AGL-2916): a proposed
539
+ // product arrives with none, and Save waits for one.
540
+ error: !CommerceModel.variantHasPrice(variant),
541
+ placeholder: "Set",
542
+ slotProps: DECIMAL_INPUT
543
+ })
544
+ }),
545
+ /*#__PURE__*/ _jsx(TableCell, {
546
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
547
+ value: (_variant_compareAtPriceUsd = variant.compareAtPriceUsd) != null ? _variant_compareAtPriceUsd : '',
548
+ onChange: set.compareAtPriceUsd,
549
+ size: "small",
550
+ sx: PRICE_CELL,
551
+ slotProps: DECIMAL_INPUT
552
+ })
553
+ }),
554
+ /*#__PURE__*/ _jsx(TableCell, {
555
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
556
+ value: (_variant_sku = variant.sku) != null ? _variant_sku : '',
557
+ onChange: set.sku,
558
+ size: "small",
559
+ sx: CODE_CELL
560
+ })
561
+ }),
562
+ /*#__PURE__*/ _jsx(TableCell, {
563
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
564
+ value: (_variant_barcode = variant.barcode) != null ? _variant_barcode : '',
565
+ onChange: set.barcode,
566
+ size: "small",
567
+ sx: CODE_CELL
568
+ })
569
+ }),
570
+ /*#__PURE__*/ _jsx(TableCell, {
571
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
572
+ value: (_variant_inventory = variant.inventory) != null ? _variant_inventory : '',
573
+ placeholder: stockApplies ? '—' : 'n/a',
574
+ disabled: !stockApplies,
575
+ onChange: set.inventory,
576
+ size: "small",
577
+ sx: STOCK_CELL,
578
+ slotProps: stockInput
579
+ })
580
+ }),
581
+ /*#__PURE__*/ _jsx(TableCell, {
582
+ children: /*#__PURE__*/ _jsx(MemoTextField, {
583
+ value: (_variant_weightGrams = variant.weightGrams) != null ? _variant_weightGrams : '',
584
+ onChange: set.weightGrams,
585
+ size: "small",
586
+ sx: PRICE_CELL,
587
+ slotProps: NUMERIC_INPUT
588
+ })
589
+ })
590
+ ]
591
+ });
592
+ });
593
+ /** The matrix's column headings, one element for every render. */ const VARIANTS_HEAD = /*#__PURE__*/ _jsx(TableHead, {
594
+ children: /*#__PURE__*/ _jsxs(TableRow, {
595
+ children: [
596
+ /*#__PURE__*/ _jsx(TableCell, {
597
+ children: 'Variant'
598
+ }),
599
+ /*#__PURE__*/ _jsx(TableCell, {
600
+ children: 'Price ($)'
601
+ }),
602
+ /*#__PURE__*/ _jsx(TableCell, {
603
+ children: 'Compare-at'
604
+ }),
605
+ /*#__PURE__*/ _jsx(TableCell, {
606
+ children: 'SKU'
607
+ }),
608
+ /*#__PURE__*/ _jsx(TableCell, {
609
+ children: 'Barcode'
610
+ }),
611
+ /*#__PURE__*/ _jsx(TableCell, {
612
+ children: 'Stock'
613
+ }),
614
+ /*#__PURE__*/ _jsx(TableCell, {
615
+ children: 'Weight (g)'
616
+ })
617
+ ]
618
+ })
619
+ });
620
+ /**
621
+ * The variants matrix: one row per combination of the options above, each
622
+ * with its own price, SKU, barcode, stock and weight. A keystroke in one row
623
+ * leaves the others' props as they were.
624
+ */ const ProductVariantsTable = /*#__PURE__*/ memo(function ProductVariantsTable(props) {
625
+ const { variants, stockApplies, onField } = props;
626
+ return /*#__PURE__*/ _jsx(Box, {
627
+ children: /*#__PURE__*/ _jsxs(ScrollTable, {
628
+ size: "small",
629
+ children: [
630
+ VARIANTS_HEAD,
631
+ /*#__PURE__*/ _jsx(TableBody, {
632
+ children: variants.map((variant, index)=>/*#__PURE__*/ _jsx(ProductVariantRow, {
633
+ index: index,
634
+ variant: variant,
635
+ stockApplies: stockApplies,
636
+ onField: onField
637
+ }, variant.id))
638
+ })
639
+ ]
640
+ })
641
+ });
642
+ });
643
+ /** What happens when stock runs out, the gift-card kind, tax, and the low-stock alert. */ const ProductSellingFields = /*#__PURE__*/ memo(function ProductSellingFields(props) {
644
+ var _props_lowStockThreshold;
645
+ const { giftsLocked, giftsPlanLabel, update } = props;
646
+ const setOversellPolicy = useCallback((event)=>update({
647
+ oversellPolicy: event.target.value
648
+ }), [
649
+ update
650
+ ]);
651
+ const setKind = useCallback((event)=>update({
652
+ giftCard: event.target.value === 'gift'
653
+ }), [
654
+ update
655
+ ]);
656
+ const setTax = useCallback((event)=>update({
657
+ taxExempt: event.target.value === 'exempt'
658
+ }), [
659
+ update
660
+ ]);
661
+ const setLowStock = useCallback((event)=>{
662
+ const raw = event.target.value.trim();
663
+ update({
664
+ lowStockThreshold: raw === '' ? undefined : Math.max(0, Math.round(Number(raw)))
665
+ });
666
+ }, [
667
+ update
668
+ ]);
669
+ const kindMenu = useMemo(()=>[
670
+ /*#__PURE__*/ _jsx(MenuItem, {
671
+ value: "standard",
672
+ children: 'Standard'
673
+ }, "standard"),
674
+ // Disabled, never removed (AGL-2080). A product saved as a gift
675
+ // card before the plan lapsed still has `giftCard: true`, and a
676
+ // MUI select whose value is absent from its options renders
677
+ // blank — the next edit would silently rewrite the product to
678
+ // Standard. Locking the option refuses NEW configuration
679
+ // without rewriting existing data.
680
+ /*#__PURE__*/ _jsx(MenuItem, {
681
+ value: "gift",
682
+ disabled: giftsLocked,
683
+ children: giftsLocked ? `Gift card — ${giftsPlanLabel != null ? giftsPlanLabel : 'upgrade'} plan` : 'Gift card'
684
+ }, "gift")
685
+ ], [
686
+ giftsLocked,
687
+ giftsPlanLabel
688
+ ]);
689
+ return /*#__PURE__*/ _jsxs(Stack, {
690
+ direction: "row",
691
+ spacing: 2,
692
+ children: [
693
+ /*#__PURE__*/ _jsx(MemoTextField, {
694
+ label: "When out of stock",
695
+ value: props.oversellPolicy,
696
+ onChange: setOversellPolicy,
697
+ size: "small",
698
+ select: true,
699
+ sx: OVERSELL_SELECT,
700
+ children: OVERSELL_MENU
701
+ }),
702
+ /*#__PURE__*/ _jsx(MemoTextField, {
703
+ label: "Kind",
704
+ value: props.giftCard ? 'gift' : 'standard',
705
+ onChange: setKind,
706
+ size: "small",
707
+ select: true,
708
+ sx: KIND_SELECT,
709
+ helperText: giftsLocked ? `Gift cards are on ${giftsPlanLabel != null ? giftsPlanLabel : 'a higher plan'}` : 'Gift cards issue a code',
710
+ children: kindMenu
711
+ }),
712
+ /*#__PURE__*/ _jsx(MemoTextField, {
713
+ label: "Tax",
714
+ value: props.taxExempt ? 'exempt' : 'taxable',
715
+ onChange: setTax,
716
+ size: "small",
717
+ select: true,
718
+ sx: SHORT_SELECT,
719
+ children: TAX_MENU
720
+ }),
721
+ /*#__PURE__*/ _jsx(MemoTextField, {
722
+ label: "Low-stock alert at",
723
+ value: (_props_lowStockThreshold = props.lowStockThreshold) != null ? _props_lowStockThreshold : '',
724
+ placeholder: "Off",
725
+ disabled: !props.stockApplies,
726
+ onChange: setLowStock,
727
+ size: "small",
728
+ sx: COUNT_FIELD,
729
+ slotProps: NUMERIC_INPUT,
730
+ helperText: "Notifies managers"
731
+ })
732
+ ]
733
+ });
734
+ });
735
+ /** One-time or subscription billing, its interval, and a free trial. */ const ProductBillingFields = /*#__PURE__*/ memo(function ProductBillingFields(props) {
736
+ var _props_subsPlanLabel, _subscription_trialDays;
737
+ const { subscription, subscriptionOptional, subsLocked, update, replaceDraft } = props;
738
+ const setBilling = useCallback((event)=>{
739
+ const value = event.target.value;
740
+ // `replaceDraft`, not `update`: a patch is spread over the draft,
741
+ // which would resurrect the keys deleted below.
742
+ replaceDraft((product)=>{
743
+ // "Both" (AGL-545): the PDP offers one-time OR subscribe at
744
+ // the same price; the interval field beside picks the cadence.
745
+ // Cleared keys are deleted (not set undefined) so the
746
+ // client-direct setDoc on save never sees undefined values.
747
+ const next = _extends({}, product);
748
+ if (value === 'once') {
749
+ delete next.subscription;
750
+ } else {
751
+ var _product_subscription, _ref;
752
+ var _product_subscription1;
753
+ next.subscription = _extends({}, (_product_subscription = product.subscription) != null ? _product_subscription : {}, {
754
+ interval: value === 'both' ? (_ref = (_product_subscription1 = product.subscription) == null ? void 0 : _product_subscription1.interval) != null ? _ref : 'month' : value
755
+ });
756
+ }
757
+ if (value === 'both') next.subscriptionOptional = true;
758
+ else delete next.subscriptionOptional;
759
+ return next;
760
+ });
761
+ }, [
762
+ replaceDraft
763
+ ]);
764
+ const setBillingInterval = useCallback((event)=>{
765
+ const interval = event.target.value;
766
+ update((product)=>({
767
+ subscription: _extends({}, product.subscription, {
768
+ interval
769
+ })
770
+ }));
771
+ }, [
772
+ update
773
+ ]);
774
+ const setTrialDays = useCallback((event)=>{
775
+ const raw = event.target.value.trim();
776
+ update((product)=>({
777
+ subscription: _extends({}, product.subscription, raw === '' ? {
778
+ trialDays: undefined
779
+ } : {
780
+ trialDays: Math.max(1, Math.round(Number(raw)))
781
+ })
782
+ }));
783
+ }, [
784
+ update
785
+ ]);
786
+ const billingMenu = useMemo(()=>[
787
+ /*#__PURE__*/ _jsx(MenuItem, {
788
+ value: "once",
789
+ children: 'One-time purchase'
790
+ }, "once"),
791
+ // Same rule as the gift-card option: disabled, not removed, so an
792
+ // existing subscription product keeps its interval instead of being
793
+ // silently reset to one-time.
794
+ /*#__PURE__*/ _jsx(MenuItem, {
795
+ value: "month",
796
+ disabled: subsLocked,
797
+ children: 'Monthly subscription'
798
+ }, "month"),
799
+ /*#__PURE__*/ _jsx(MenuItem, {
800
+ value: "year",
801
+ disabled: subsLocked,
802
+ children: 'Yearly subscription'
803
+ }, "year"),
804
+ /*#__PURE__*/ _jsx(MenuItem, {
805
+ value: "both",
806
+ disabled: subsLocked,
807
+ children: 'Both — buyer chooses'
808
+ }, "both")
809
+ ], [
810
+ subsLocked
811
+ ]);
812
+ return /*#__PURE__*/ _jsxs(Stack, {
813
+ direction: "row",
814
+ spacing: 2,
815
+ children: [
816
+ /*#__PURE__*/ _jsx(MemoTextField, {
817
+ label: "Billing",
818
+ value: subscription ? subscriptionOptional ? 'both' : subscription.interval : 'once',
819
+ onChange: setBilling,
820
+ size: "small",
821
+ select: true,
822
+ sx: BILLING_SELECT,
823
+ helperText: subsLocked ? `Subscriptions are on ${(_props_subsPlanLabel = props.subsPlanLabel) != null ? _props_subsPlanLabel : 'a higher plan'}` : 'Subscriptions bill until canceled',
824
+ children: billingMenu
825
+ }),
826
+ subscription && subscriptionOptional ? /*#__PURE__*/ _jsx(MemoTextField, {
827
+ label: "Interval",
828
+ value: subscription.interval,
829
+ onChange: setBillingInterval,
830
+ size: "small",
831
+ select: true,
832
+ sx: SHORT_SELECT,
833
+ children: INTERVAL_MENU
834
+ }) : null,
835
+ subscription ? /*#__PURE__*/ _jsx(MemoTextField, {
836
+ label: "Free trial (days)",
837
+ placeholder: "None",
838
+ value: (_subscription_trialDays = subscription.trialDays) != null ? _subscription_trialDays : '',
839
+ onChange: setTrialDays,
840
+ size: "small",
841
+ sx: COUNT_FIELD,
842
+ slotProps: NUMERIC_INPUT
843
+ }) : null
844
+ ]
845
+ });
846
+ });
847
+ const DigitalFileRow = /*#__PURE__*/ memo(function DigitalFileRow(props) {
848
+ var _file_version;
849
+ const { index, file, update } = props;
850
+ const setVersion = useCallback((event)=>{
851
+ const version = event.target.value.slice(0, 20);
852
+ update((product)=>{
853
+ var _product_digitalFiles;
854
+ const digitalFiles = [
855
+ ...(_product_digitalFiles = product.digitalFiles) != null ? _product_digitalFiles : []
856
+ ];
857
+ digitalFiles[index] = _extends({}, file, {
858
+ version
859
+ });
860
+ return {
861
+ digitalFiles
862
+ };
863
+ });
864
+ }, [
865
+ file,
866
+ index,
867
+ update
868
+ ]);
869
+ return /*#__PURE__*/ _jsxs(Stack, {
870
+ direction: "row",
871
+ spacing: 1,
872
+ sx: {
873
+ alignItems: 'center'
874
+ },
875
+ children: [
876
+ /*#__PURE__*/ _jsxs(Typography, {
877
+ variant: "body2",
878
+ sx: {
879
+ flex: 1
880
+ },
881
+ noWrap: true,
882
+ children: [
883
+ file.fileName,
884
+ file.version ? ` · v${file.version}` : ''
885
+ ]
886
+ }),
887
+ /*#__PURE__*/ _jsx(PaidMediaProtection, {
888
+ url: file.url,
889
+ hostId: props.hostId,
890
+ productId: props.productId,
891
+ kind: "download"
892
+ }),
893
+ /*#__PURE__*/ _jsx(MemoTextField, {
894
+ label: "Version",
895
+ value: (_file_version = file.version) != null ? _file_version : '',
896
+ onChange: setVersion,
897
+ size: "small",
898
+ sx: VERSION_FIELD
899
+ }),
900
+ /*#__PURE__*/ _jsx(Button, {
901
+ size: "small",
902
+ color: "error",
903
+ onClick: ()=>update((product)=>{
904
+ var _product_digitalFiles;
905
+ return {
906
+ digitalFiles: ((_product_digitalFiles = product.digitalFiles) != null ? _product_digitalFiles : []).filter((_item, itemIndex)=>itemIndex !== index)
907
+ };
908
+ }),
909
+ children: '✕'
910
+ })
911
+ ]
912
+ });
913
+ });
914
+ /*
915
+ * The paid-media controls beside the digital fields, drawn again only when
916
+ * what they are handed changes: a keystroke in the download limit leaves the
917
+ * videos, and the protection check on each, as they were.
918
+ */ const MemoMembersVideosField = /*#__PURE__*/ memo(MembersVideosField);
919
+ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
920
+ /** A digital product's files, its download limit and its members videos. */ const ProductDigitalFields = /*#__PURE__*/ memo(function ProductDigitalFields(props) {
921
+ var _props_downloadLimit;
922
+ const { hostId, productId, update } = props;
923
+ const addFile = useCallback((file)=>update((product)=>{
924
+ var _product_digitalFiles;
925
+ return {
926
+ digitalFiles: [
927
+ ...(_product_digitalFiles = product.digitalFiles) != null ? _product_digitalFiles : [],
928
+ file
929
+ ]
930
+ };
931
+ }), [
932
+ update
933
+ ]);
934
+ const setDownloadLimit = useCallback((event)=>{
935
+ const raw = event.target.value.trim();
936
+ update({
937
+ downloadLimit: raw === '' ? undefined : Math.max(1, Math.round(Number(raw)))
938
+ });
939
+ }, [
940
+ update
941
+ ]);
942
+ const setVideos = useCallback((gatedVideos)=>update({
943
+ gatedVideos
944
+ }), [
945
+ update
946
+ ]);
947
+ return /*#__PURE__*/ _jsxs(_Fragment, {
948
+ children: [
949
+ /*#__PURE__*/ _jsx(Divider, {
950
+ textAlign: "left",
951
+ children: 'Digital delivery'
952
+ }),
953
+ props.digitalFiles.map((file, index)=>/*#__PURE__*/ _jsx(DigitalFileRow, {
954
+ index: index,
955
+ file: file,
956
+ hostId: hostId,
957
+ productId: productId,
958
+ update: update
959
+ }, index)),
960
+ /*#__PURE__*/ _jsxs(Stack, {
961
+ direction: "row",
962
+ spacing: 2,
963
+ children: [
964
+ /*#__PURE__*/ _jsx(MemoPaidDownloadAddButton, {
965
+ hostId: hostId,
966
+ productId: productId,
967
+ onAdd: addFile
968
+ }),
969
+ /*#__PURE__*/ _jsx(MemoTextField, {
970
+ label: "Download limit",
971
+ placeholder: "Unlimited",
972
+ value: (_props_downloadLimit = props.downloadLimit) != null ? _props_downloadLimit : '',
973
+ onChange: setDownloadLimit,
974
+ size: "small",
975
+ sx: COUNT_FIELD,
976
+ slotProps: NUMERIC_INPUT,
977
+ helperText: "Attempts per order"
978
+ })
979
+ ]
980
+ }),
981
+ /*#__PURE__*/ _jsx(Typography, {
982
+ variant: "caption",
983
+ color: "text.secondary",
984
+ children: 'Buyers always download the current files — uploading a new ' + 'version re-delivers to everyone.'
985
+ }),
986
+ /*#__PURE__*/ _jsx(MemoMembersVideosField, {
987
+ hostId: hostId,
988
+ productId: productId,
989
+ videos: props.gatedVideos,
990
+ onChange: setVideos
991
+ })
992
+ ]
993
+ });
994
+ });
995
+ /** The product's search listing: its title, description and share image. */ const ProductSeoFields = /*#__PURE__*/ memo(function ProductSeoFields(props) {
996
+ const { title, description, update, pick } = props;
997
+ const setTitle = useCallback((event)=>{
998
+ const next = event.target.value;
999
+ update((product)=>({
1000
+ seo: _extends({}, product.seo, {
1001
+ title: next
1002
+ })
1003
+ }));
1004
+ }, [
1005
+ update
1006
+ ]);
1007
+ const setDescription = useCallback((event)=>{
1008
+ const next = event.target.value;
1009
+ update((product)=>({
1010
+ seo: _extends({}, product.seo, {
1011
+ description: next
1012
+ })
1013
+ }));
1014
+ }, [
1015
+ update
1016
+ ]);
1017
+ return /*#__PURE__*/ _jsxs(_Fragment, {
1018
+ children: [
1019
+ /*#__PURE__*/ _jsx(Divider, {
1020
+ textAlign: "left",
1021
+ children: 'Search engine listing'
1022
+ }),
1023
+ /*#__PURE__*/ _jsxs(Stack, {
1024
+ direction: WIDE_ROW,
1025
+ spacing: 2,
1026
+ children: [
1027
+ /*#__PURE__*/ _jsx(MemoTextField, {
1028
+ label: "SEO title",
1029
+ value: title != null ? title : '',
1030
+ onChange: setTitle,
1031
+ // The counts every search listing editor prints (AGL-2910): what a
1032
+ // search result shows before it truncates.
1033
+ helperText: seoListingFieldCount('title', title),
1034
+ error: seoListingFieldTooLong('title', title),
1035
+ size: "small",
1036
+ fullWidth: true
1037
+ }),
1038
+ /*#__PURE__*/ _jsx(Button, {
1039
+ size: "small",
1040
+ onClick: ()=>void pick((media)=>update((product)=>({
1041
+ seo: _extends({}, product.seo, {
1042
+ imageUrl: media.url
1043
+ })
1044
+ }))),
1045
+ children: props.hasImage ? 'Change OG image' : 'OG image'
1046
+ })
1047
+ ]
1048
+ }),
1049
+ /*#__PURE__*/ _jsx(MemoTextField, {
1050
+ label: "SEO description",
1051
+ value: description != null ? description : '',
1052
+ onChange: setDescription,
1053
+ helperText: seoListingFieldCount('description', description),
1054
+ error: seoListingFieldTooLong('description', description),
1055
+ size: "small",
1056
+ multiline: true,
1057
+ minRows: 2
1058
+ })
1059
+ ]
1060
+ });
1061
+ });
119
1062
  /**
120
1063
  * Products hub editor (AGL-279): the full catalog editor — basics,
121
1064
  * media, tags, options → variants matrix, per-variant pricing/stock, and
@@ -123,8 +1066,8 @@ const sameRecord = (option, value)=>option.$id === value.$id;
123
1066
  * first variant so the legacy Product block + checkout API (AGL-90) keep
124
1067
  * charging correctly without reading the variants array.
125
1068
  */ export function ProductEditorDialog(props) {
126
- var _ref, _ref1, _current_description, _current_tags, _current_categoryIds, _current_options, _current_mediaUrls, _ref2, _ref3, _current_supplierId, _current_description1, _current_tags1, _current_mediaUrls1, _current_options1, _ref4, _subs_planLabel, _gifts_planLabel, _subs_planLabel1, _gifts_planLabel1, _current_oversellPolicy, _gifts_planLabel2, _gifts_planLabel3, _current_lowStockThreshold, _subs_planLabel2, _current_subscription_trialDays, _current_digitalFiles, _current_downloadLimit, _current_gatedVideos, _ref5, _ref6, _ref7, _current_description2, _ref8, _ref9;
127
- var _current_seo, _current_seo1, _current_options2, _current_seo2, _current_seo3, _current_seo4, _current_seo5, _current_seo6, _current_seo7, _current_seo8, _current_seo9, _current_seo10, _current_seo11;
1069
+ var _ref, _ref1, _current_description, _current_tags, _current_categoryIds, _current_options, _current_mediaUrls, _ref2, _ref3, _ref4, _current_description1, _ref5, _ref6, _current_description2, _current_tags1, _current_mediaUrls1, _current_options1, _subs_planLabel, _gifts_planLabel, _subs_planLabel1, _gifts_planLabel1, _current_oversellPolicy, _current_digitalFiles, _current_gatedVideos;
1070
+ var _current_seo, _current_seo1, _current_seo2, _current_seo3, _current_seo4, _current_seo5, _current_seo6, _current_seo7;
128
1071
  const { hostId, product, seedFromCache, seedUnreadable, open, onClose } = props;
129
1072
  /**
130
1073
  * Field-level entitlement gates (AGL-2080). Both of these were fully
@@ -204,27 +1147,34 @@ const sameRecord = (option, value)=>option.$id === value.$id;
204
1147
  ]);
205
1148
  // Lazy-init per open; parent remounts via `key` on product change.
206
1149
  const current = (_ref = draft != null ? draft : lifted) != null ? _ref : blankProduct();
207
- const update = (patch)=>setDraft(_extends({}, current, patch));
208
1150
  /*
209
- * `update` for the memoized chip lists: the same write from the last
210
- * rendered draft, from a callback that keeps its identity across renders.
1151
+ * The two writes every field goes through, each keeping its identity for
1152
+ * the life of the dialog so a memoized section redraws only when its own
1153
+ * slice of the draft does (AGL-3423). Both write from the draft as last
1154
+ * rendered, which `latest` holds: `update` spreads a patch over it, and a
1155
+ * patch that is a function is handed it, so no field holds a copy of the
1156
+ * product to spread. A write that lands after a picker closes lands on the
1157
+ * draft as it is then.
211
1158
  */ const latest = useRef(current);
212
1159
  latest.current = current;
213
- const updateLatest = useCallback((patch)=>setDraft(_extends({}, latest.current, patch)), []);
214
- const setTags = useCallback((tags)=>updateLatest({
1160
+ const replaceDraft = useCallback((build)=>setDraft(build(latest.current)), []);
1161
+ const update = useCallback((patch)=>replaceDraft((at)=>_extends({}, at, typeof patch === 'function' ? patch(at) : patch)), [
1162
+ replaceDraft
1163
+ ]);
1164
+ const setTags = useCallback((tags)=>update({
215
1165
  tags: tags.map((tag)=>String(tag).trim()).filter(Boolean)
216
1166
  }), [
217
- updateLatest
1167
+ update
218
1168
  ]);
219
- const setCategories = useCallback((picked)=>updateLatest({
1169
+ const setCategories = useCallback((picked)=>update({
220
1170
  categoryIds: picked.map((category)=>category.$id)
221
1171
  }), [
222
- updateLatest
1172
+ update
223
1173
  ]);
224
- const setRelatedProducts = useCallback((picked)=>updateLatest({
1174
+ const setRelatedProducts = useCallback((picked)=>update({
225
1175
  relatedProductIds: picked.map((item)=>item.$id)
226
1176
  }), [
227
- updateLatest
1177
+ update
228
1178
  ]);
229
1179
  const categoryIds = current.categoryIds;
230
1180
  const pickedCategories = useMemo(()=>categories.rows.filter((category)=>(categoryIds != null ? categoryIds : []).includes(category.$id)), [
@@ -287,7 +1237,10 @@ const sameRecord = (option, value)=>option.$id === value.$id;
287
1237
  }), [
288
1238
  categories.rows
289
1239
  ]);
290
- const zoneProduct = {
1240
+ /*
1241
+ * What the two zones are handed, held by content: a keystroke in a field
1242
+ * neither zone reads (a SKU, a price) leaves both as they were.
1243
+ */ const zoneProduct = useContentStable({
291
1244
  id: (_ref1 = product == null ? void 0 : product.$id) != null ? _ref1 : null,
292
1245
  name: current.name,
293
1246
  type: current.type,
@@ -301,7 +1254,17 @@ const sameRecord = (option, value)=>option.$id === value.$id;
301
1254
  mediaUrls: (_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : [],
302
1255
  seoTitle: (_ref2 = (_current_seo = current.seo) == null ? void 0 : _current_seo.title) != null ? _ref2 : '',
303
1256
  seoDescription: (_ref3 = (_current_seo1 = current.seo) == null ? void 0 : _current_seo1.description) != null ? _ref3 : ''
304
- };
1257
+ });
1258
+ const seoSubject = useContentStable({
1259
+ kind: 'product',
1260
+ id: (_ref4 = product == null ? void 0 : product.$id) != null ? _ref4 : null,
1261
+ name: current.name,
1262
+ description: String((_current_description1 = current.description) != null ? _current_description1 : '')
1263
+ });
1264
+ const seoValues = useContentStable({
1265
+ title: (_ref5 = (_current_seo2 = current.seo) == null ? void 0 : _current_seo2.title) != null ? _ref5 : '',
1266
+ description: (_ref6 = (_current_seo3 = current.seo) == null ? void 0 : _current_seo3.description) != null ? _ref6 : ''
1267
+ });
305
1268
  const error = current.name ? CommerceModel.validateProduct(current) : null;
306
1269
  /**
307
1270
  * Stock tracking is inert on a subscription-only product (AGL-1744) —
@@ -324,90 +1287,106 @@ const sameRecord = (option, value)=>option.$id === value.$id;
324
1287
  * count this just cleared. Keys are DELETED rather than set undefined; the
325
1288
  * save is a client-direct `setDoc`, which rejects undefined values.
326
1289
  */ const handleClearStockTracking = ()=>{
327
- update({
328
- variants: current.variants.map((variant)=>{
329
- const { inventoryByLocation: _perLocation } = variant, rest = _object_without_properties_loose(variant, [
330
- "inventoryByLocation"
331
- ]);
332
- return _extends({}, rest, {
333
- inventory: null
334
- });
335
- })
336
- });
1290
+ update((at)=>({
1291
+ variants: at.variants.map((variant)=>{
1292
+ const { inventoryByLocation: _perLocation } = variant, rest = _object_without_properties_loose(variant, [
1293
+ "inventoryByLocation"
1294
+ ]);
1295
+ return _extends({}, rest, {
1296
+ inventory: null
1297
+ });
1298
+ })
1299
+ }));
337
1300
  };
338
- const handleName = (name)=>update(_extends({
1301
+ const handleName = useCallback((event)=>{
1302
+ const name = event.target.value;
1303
+ update(_extends({
339
1304
  name
340
1305
  }, !product && !slugTouched ? {
341
1306
  slug: CommerceModel.commerceSlug(name)
342
1307
  } : {}));
343
- const handleOptionsChange = useCallback((index, patch)=>{
344
- var _current_options;
345
- var _current_options1;
346
- // A RENAME moves each variant's selection to the new name (AGL-3066).
347
- // Rebuilding the matrix matches variants by name and value, so under a
348
- // new name it finds none and replaces every variant, its id, price, SKU
349
- // and stock with them. Only a change of values rebuilds.
350
- if (patch && ((_current_options1 = current.options) == null ? void 0 : _current_options1[index]) && Object.keys(patch).length === 1 && typeof patch.name === 'string') {
351
- update(CommerceModel.renameProductOptions(current, current.options.map((option, at)=>at === index ? patch.name : option.name)));
352
- return;
353
- }
354
- const options = [
355
- ...(_current_options = current.options) != null ? _current_options : []
356
- ];
357
- if (patch === null) options.splice(index, 1);
358
- else options[index] = _extends({
359
- name: '',
360
- values: []
361
- }, options[index], patch);
362
- // Regenerate the matrix, carrying data over by option-combo key so
363
- // edits to prices/SKUs survive option tweaks.
364
- const previous = new Map(current.variants.map((variant)=>[
365
- comboKey(variant.options),
366
- variant
367
- ]));
368
- const fallback = current.variants[0];
369
- const variants = CommerceModel.expandVariantMatrix(options).map((combo, comboIndex)=>{
370
- var _ref, _ref1;
371
- const existing = previous.get(comboKey(combo));
372
- return existing != null ? existing : {
373
- id: `v${Date.now().toString(36)}${comboIndex}`,
374
- options: combo,
375
- priceUsd: (_ref = fallback == null ? void 0 : fallback.priceUsd) != null ? _ref : 0,
376
- inventory: (_ref1 = fallback == null ? void 0 : fallback.inventory) != null ? _ref1 : null
377
- };
378
- });
1308
+ }, [
1309
+ product,
1310
+ slugTouched,
1311
+ update
1312
+ ]);
1313
+ const handleSlug = useCallback((event)=>{
1314
+ setSlugTouched(true);
379
1315
  update({
380
- options,
381
- variants
1316
+ slug: CommerceModel.commerceSlug(event.target.value)
382
1317
  });
383
1318
  }, [
384
- current
1319
+ update
385
1320
  ]);
386
- const latestOptionsChange = useRef(handleOptionsChange);
387
- latestOptionsChange.current = handleOptionsChange;
388
- const setOptionValues = useCallback((values, index)=>latestOptionsChange.current(index, {
1321
+ const handleOptionsChange = useCallback((index, patch)=>update((at)=>{
1322
+ var _at_options;
1323
+ var _at_options1;
1324
+ // A RENAME moves each variant's selection to the new name (AGL-3066).
1325
+ // Rebuilding the matrix matches variants by name and value, so under a
1326
+ // new name it finds none and replaces every variant, its id, price, SKU
1327
+ // and stock with them. Only a change of values rebuilds.
1328
+ if (patch && ((_at_options1 = at.options) == null ? void 0 : _at_options1[index]) && Object.keys(patch).length === 1 && typeof patch.name === 'string') {
1329
+ return CommerceModel.renameProductOptions(at, at.options.map((option, optionIndex)=>optionIndex === index ? patch.name : option.name));
1330
+ }
1331
+ const options = [
1332
+ ...(_at_options = at.options) != null ? _at_options : []
1333
+ ];
1334
+ if (patch === null) options.splice(index, 1);
1335
+ else options[index] = _extends({
1336
+ name: '',
1337
+ values: []
1338
+ }, options[index], patch);
1339
+ // Regenerate the matrix, carrying data over by option-combo key so
1340
+ // edits to prices/SKUs survive option tweaks.
1341
+ const previous = new Map(at.variants.map((variant)=>[
1342
+ comboKey(variant.options),
1343
+ variant
1344
+ ]));
1345
+ const fallback = at.variants[0];
1346
+ const variants = CommerceModel.expandVariantMatrix(options).map((combo, comboIndex)=>{
1347
+ var _ref, _ref1;
1348
+ const existing = previous.get(comboKey(combo));
1349
+ return existing != null ? existing : {
1350
+ id: `v${Date.now().toString(36)}${comboIndex}`,
1351
+ options: combo,
1352
+ priceUsd: (_ref = fallback == null ? void 0 : fallback.priceUsd) != null ? _ref : 0,
1353
+ inventory: (_ref1 = fallback == null ? void 0 : fallback.inventory) != null ? _ref1 : null
1354
+ };
1355
+ });
1356
+ return {
1357
+ options,
1358
+ variants
1359
+ };
1360
+ }), [
1361
+ update
1362
+ ]);
1363
+ const setOptionValues = useCallback((values, index)=>handleOptionsChange(index, {
389
1364
  values: values.map((value)=>String(value).trim()).filter(Boolean)
390
- }), []);
391
- const handleVariantField = (index, field, raw)=>{
392
- const variants = [
393
- ...current.variants
394
- ];
395
- const numeric = [
396
- 'priceUsd',
397
- 'compareAtPriceUsd',
398
- 'weightGrams'
399
- ];
400
- const value = field === 'inventory' ? raw.trim() === '' ? null : Math.max(0, Math.round(Number(raw))) : numeric.includes(field) ? raw.trim() === '' ? undefined : Number(raw) : raw;
401
- variants[index] = _extends({}, variants[index], {
402
- [field]: value
403
- });
404
- if (value === undefined && (field === 'compareAtPriceUsd' || field === 'weightGrams')) {
405
- delete variants[index][field];
406
- }
407
- update({
408
- variants
409
- });
410
- };
1365
+ }), [
1366
+ handleOptionsChange
1367
+ ]);
1368
+ const handleVariantField = useCallback((index, field, raw)=>update((at)=>{
1369
+ const variants = [
1370
+ ...at.variants
1371
+ ];
1372
+ const numeric = [
1373
+ 'priceUsd',
1374
+ 'compareAtPriceUsd',
1375
+ 'weightGrams'
1376
+ ];
1377
+ const value = field === 'inventory' ? raw.trim() === '' ? null : Math.max(0, Math.round(Number(raw))) : numeric.includes(field) ? raw.trim() === '' ? undefined : Number(raw) : raw;
1378
+ variants[index] = _extends({}, variants[index], {
1379
+ [field]: value
1380
+ });
1381
+ if (value === undefined && (field === 'compareAtPriceUsd' || field === 'weightGrams')) {
1382
+ delete variants[index][field];
1383
+ }
1384
+ return {
1385
+ variants
1386
+ };
1387
+ }), [
1388
+ update
1389
+ ]);
411
1390
  const handleSave = useCallback(async ()=>{
412
1391
  var _ref, _ref1, _ref2;
413
1392
  var _current_mediaUrls;
@@ -594,129 +1573,17 @@ const sameRecord = (option, value)=>option.$id === value.$id;
594
1573
  gap: 2
595
1574
  },
596
1575
  children: [
597
- /*#__PURE__*/ _jsxs(Stack, {
598
- direction: {
599
- xs: 'column',
600
- sm: 'row'
601
- },
602
- spacing: 2,
603
- sx: {
604
- mt: 1
605
- },
606
- children: [
607
- /*#__PURE__*/ _jsx(TextField, {
608
- label: "Name",
609
- value: current.name,
610
- onChange: (event)=>handleName(event.target.value),
611
- size: "small",
612
- autoFocus: true,
613
- fullWidth: true
614
- }),
615
- /*#__PURE__*/ _jsx(TextField, {
616
- label: "Slug",
617
- value: current.slug,
618
- onChange: (event)=>{
619
- setSlugTouched(true);
620
- update({
621
- slug: CommerceModel.commerceSlug(event.target.value)
622
- });
623
- },
624
- size: "small",
625
- fullWidth: true,
626
- helperText: current.slug ? `/products/${current.slug}` : undefined
627
- })
628
- ]
629
- }),
630
- /*#__PURE__*/ _jsxs(Stack, {
631
- direction: "row",
632
- spacing: 2,
633
- children: [
634
- /*#__PURE__*/ _jsxs(TextField, {
635
- label: "Type",
636
- value: current.type,
637
- onChange: (event)=>update({
638
- type: event.target.value
639
- }),
640
- size: "small",
641
- select: true,
642
- sx: {
643
- minWidth: 140
644
- },
645
- children: [
646
- /*#__PURE__*/ _jsx(MenuItem, {
647
- value: "physical",
648
- children: 'Physical'
649
- }),
650
- /*#__PURE__*/ _jsx(MenuItem, {
651
- value: "digital",
652
- children: 'Digital'
653
- }),
654
- /*#__PURE__*/ _jsx(MenuItem, {
655
- value: "service",
656
- children: 'Service'
657
- })
658
- ]
659
- }),
660
- /*#__PURE__*/ _jsxs(TextField, {
661
- label: "Status",
662
- value: current.status,
663
- onChange: (event)=>update({
664
- status: event.target.value
665
- }),
666
- size: "small",
667
- select: true,
668
- sx: {
669
- minWidth: 140
670
- },
671
- children: [
672
- /*#__PURE__*/ _jsx(MenuItem, {
673
- value: "draft",
674
- children: 'Draft'
675
- }),
676
- /*#__PURE__*/ _jsx(MenuItem, {
677
- value: "active",
678
- children: 'Active'
679
- }),
680
- /*#__PURE__*/ _jsx(MenuItem, {
681
- value: "archived",
682
- children: 'Archived'
683
- })
684
- ]
685
- }),
686
- suppliers.rows.length > 0 ? /*#__PURE__*/ _jsxs(TextField, {
687
- label: "Supplier",
688
- value: (_current_supplierId = current.supplierId) != null ? _current_supplierId : '',
689
- onChange: (event)=>update({
690
- supplierId: event.target.value || undefined
691
- }),
692
- size: "small",
693
- select: true,
694
- sx: {
695
- minWidth: 160
696
- },
697
- helperText: "Routes paid orders",
698
- children: [
699
- /*#__PURE__*/ _jsx(MenuItem, {
700
- value: "",
701
- children: 'None (self-fulfilled)'
702
- }),
703
- suppliers.rows.map((supplier)=>/*#__PURE__*/ _jsx(MenuItem, {
704
- value: supplier.$id,
705
- children: supplier.name
706
- }, supplier.$id))
707
- ]
708
- }) : null
709
- ]
710
- }),
711
- /*#__PURE__*/ _jsx(TextField, {
712
- label: "Description",
713
- value: (_current_description1 = current.description) != null ? _current_description1 : '',
714
- onChange: (event)=>update({
715
- description: event.target.value
716
- }),
717
- size: "small",
718
- multiline: true,
719
- minRows: 2
1576
+ /*#__PURE__*/ _jsx(ProductBasicsFields, {
1577
+ name: current.name,
1578
+ slug: current.slug,
1579
+ type: current.type,
1580
+ status: current.status,
1581
+ supplierId: current.supplierId,
1582
+ description: (_current_description2 = current.description) != null ? _current_description2 : '',
1583
+ suppliers: suppliers.rows,
1584
+ onName: handleName,
1585
+ onSlug: handleSlug,
1586
+ update: update
720
1587
  }),
721
1588
  /*#__PURE__*/ _jsx(ProductChipsField, {
722
1589
  freeSolo: true,
@@ -734,7 +1601,8 @@ const sameRecord = (option, value)=>option.$id === value.$id;
734
1601
  value: pickedCategories,
735
1602
  onChange: setCategories
736
1603
  }) : null,
737
- WidgetSlot ? /*#__PURE__*/ _jsx(WidgetSlot, {
1604
+ WidgetSlot ? /*#__PURE__*/ _jsx(HostedZone, {
1605
+ renderer: WidgetSlot,
738
1606
  slot: PRODUCT_EDITOR_ZONE.id,
739
1607
  hostId: hostId,
740
1608
  orgId: undefined,
@@ -746,262 +1614,24 @@ const sameRecord = (option, value)=>option.$id === value.$id;
746
1614
  textAlign: "left",
747
1615
  children: 'Media'
748
1616
  }),
749
- /*#__PURE__*/ _jsxs(Box, {
750
- sx: {
751
- display: 'flex',
752
- gap: 1,
753
- flexWrap: 'wrap'
754
- },
755
- children: [
756
- ((_current_mediaUrls1 = current.mediaUrls) != null ? _current_mediaUrls1 : []).map((url, index)=>/*#__PURE__*/ _jsxs(Box, {
757
- sx: {
758
- position: 'relative'
759
- },
760
- children: [
761
- /*#__PURE__*/ _jsx(Box, {
762
- component: "img",
763
- src: url,
764
- alt: "",
765
- sx: {
766
- width: 72,
767
- height: 72,
768
- objectFit: 'cover',
769
- borderRadius: 1,
770
- border: 1,
771
- borderColor: 'divider'
772
- }
773
- }),
774
- /*#__PURE__*/ _jsx(IconButton, {
775
- size: "small",
776
- "aria-label": "Remove image",
777
- onClick: ()=>{
778
- var _current_mediaUrls;
779
- return update({
780
- mediaUrls: ((_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : []).filter((_item, itemIndex)=>itemIndex !== index)
781
- });
782
- },
783
- sx: {
784
- position: 'absolute',
785
- top: -8,
786
- right: -8,
787
- bgcolor: 'background.paper',
788
- border: 1,
789
- borderColor: 'divider',
790
- p: 0.25
791
- },
792
- children: '✕'
793
- })
794
- ]
795
- }, `${url}-${index}`)),
796
- /*#__PURE__*/ _jsx(Button, {
797
- size: "small",
798
- onClick: ()=>void pick((media)=>{
799
- var _current_mediaUrls;
800
- return update({
801
- mediaUrls: [
802
- ...(_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : [],
803
- media.url
804
- ]
805
- });
806
- }),
807
- children: 'Add image'
808
- })
809
- ]
1617
+ /*#__PURE__*/ _jsx(ProductMediaFields, {
1618
+ mediaUrls: (_current_mediaUrls1 = current.mediaUrls) != null ? _current_mediaUrls1 : NO_ITEMS,
1619
+ update: update,
1620
+ pick: pick
810
1621
  }),
811
1622
  /*#__PURE__*/ _jsx(Divider, {
812
1623
  textAlign: "left",
813
1624
  children: 'Options & variants'
814
1625
  }),
815
- ((_current_options1 = current.options) != null ? _current_options1 : []).map((option, index)=>/*#__PURE__*/ _jsxs(Stack, {
816
- direction: "row",
817
- spacing: 1,
818
- sx: {
819
- alignItems: 'flex-start'
820
- },
821
- children: [
822
- /*#__PURE__*/ _jsx(TextField, {
823
- label: "Option",
824
- value: option.name,
825
- onChange: (event)=>handleOptionsChange(index, {
826
- name: event.target.value
827
- }),
828
- size: "small",
829
- sx: {
830
- width: 160
831
- },
832
- placeholder: "Size"
833
- }),
834
- /*#__PURE__*/ _jsx(ProductChipsField, {
835
- freeSolo: true,
836
- fill: true,
837
- label: "Values",
838
- placeholder: "S, M, L…",
839
- options: NO_ITEMS,
840
- value: option.values,
841
- index: index,
842
- onChange: setOptionValues
843
- }),
844
- /*#__PURE__*/ _jsx(Button, {
845
- size: "small",
846
- color: "error",
847
- onClick: ()=>handleOptionsChange(index, null),
848
- sx: {
849
- mt: 0.5
850
- },
851
- children: 'Remove'
852
- })
853
- ]
854
- }, index)),
855
- ((_ref4 = (_current_options2 = current.options) == null ? void 0 : _current_options2.length) != null ? _ref4 : 0) < CommerceModel.COMMERCE_MAX_OPTIONS ? /*#__PURE__*/ _jsx(Button, {
856
- size: "small",
857
- sx: {
858
- alignSelf: 'flex-start'
859
- },
860
- onClick: ()=>{
861
- var _ref;
862
- var _current_options;
863
- return handleOptionsChange((_ref = (_current_options = current.options) == null ? void 0 : _current_options.length) != null ? _ref : 0, {
864
- name: '',
865
- values: []
866
- });
867
- },
868
- children: 'Add option'
869
- }) : null,
870
- /*#__PURE__*/ _jsx(Box, {
871
- children: /*#__PURE__*/ _jsxs(ScrollTable, {
872
- size: "small",
873
- children: [
874
- /*#__PURE__*/ _jsx(TableHead, {
875
- children: /*#__PURE__*/ _jsxs(TableRow, {
876
- children: [
877
- /*#__PURE__*/ _jsx(TableCell, {
878
- children: 'Variant'
879
- }),
880
- /*#__PURE__*/ _jsx(TableCell, {
881
- children: 'Price ($)'
882
- }),
883
- /*#__PURE__*/ _jsx(TableCell, {
884
- children: 'Compare-at'
885
- }),
886
- /*#__PURE__*/ _jsx(TableCell, {
887
- children: 'SKU'
888
- }),
889
- /*#__PURE__*/ _jsx(TableCell, {
890
- children: 'Barcode'
891
- }),
892
- /*#__PURE__*/ _jsx(TableCell, {
893
- children: 'Stock'
894
- }),
895
- /*#__PURE__*/ _jsx(TableCell, {
896
- children: 'Weight (g)'
897
- })
898
- ]
899
- })
900
- }),
901
- /*#__PURE__*/ _jsx(TableBody, {
902
- children: current.variants.map((variant, index)=>{
903
- var _variant_priceUsd, _variant_compareAtPriceUsd, _variant_sku, _variant_barcode, _variant_inventory, _variant_weightGrams;
904
- return /*#__PURE__*/ _jsxs(TableRow, {
905
- children: [
906
- /*#__PURE__*/ _jsx(TableCell, {
907
- sx: {
908
- whiteSpace: 'nowrap'
909
- },
910
- children: comboLabel(variant.options)
911
- }),
912
- /*#__PURE__*/ _jsx(TableCell, {
913
- children: /*#__PURE__*/ _jsx(TextField, {
914
- value: (_variant_priceUsd = variant.priceUsd) != null ? _variant_priceUsd : '',
915
- onChange: (event)=>handleVariantField(index, 'priceUsd', event.target.value),
916
- size: "small",
917
- sx: {
918
- width: 88
919
- },
920
- // An empty price is marked (AGL-2916): a proposed
921
- // product arrives with none, and Save waits for one.
922
- error: !CommerceModel.variantHasPrice(variant),
923
- placeholder: "Set",
924
- slotProps: {
925
- htmlInput: {
926
- inputMode: 'decimal'
927
- }
928
- }
929
- })
930
- }),
931
- /*#__PURE__*/ _jsx(TableCell, {
932
- children: /*#__PURE__*/ _jsx(TextField, {
933
- value: (_variant_compareAtPriceUsd = variant.compareAtPriceUsd) != null ? _variant_compareAtPriceUsd : '',
934
- onChange: (event)=>handleVariantField(index, 'compareAtPriceUsd', event.target.value),
935
- size: "small",
936
- sx: {
937
- width: 88
938
- },
939
- slotProps: {
940
- htmlInput: {
941
- inputMode: 'decimal'
942
- }
943
- }
944
- })
945
- }),
946
- /*#__PURE__*/ _jsx(TableCell, {
947
- children: /*#__PURE__*/ _jsx(TextField, {
948
- value: (_variant_sku = variant.sku) != null ? _variant_sku : '',
949
- onChange: (event)=>handleVariantField(index, 'sku', event.target.value),
950
- size: "small",
951
- sx: {
952
- width: 110
953
- }
954
- })
955
- }),
956
- /*#__PURE__*/ _jsx(TableCell, {
957
- children: /*#__PURE__*/ _jsx(TextField, {
958
- value: (_variant_barcode = variant.barcode) != null ? _variant_barcode : '',
959
- onChange: (event)=>handleVariantField(index, 'barcode', event.target.value),
960
- size: "small",
961
- sx: {
962
- width: 110
963
- }
964
- })
965
- }),
966
- /*#__PURE__*/ _jsx(TableCell, {
967
- children: /*#__PURE__*/ _jsx(TextField, {
968
- value: (_variant_inventory = variant.inventory) != null ? _variant_inventory : '',
969
- placeholder: stockApplies ? '—' : 'n/a',
970
- disabled: !stockApplies,
971
- onChange: (event)=>handleVariantField(index, 'inventory', event.target.value),
972
- size: "small",
973
- sx: {
974
- width: 72
975
- },
976
- slotProps: {
977
- htmlInput: {
978
- inputMode: 'numeric',
979
- 'aria-label': `Stock — ${comboLabel(variant.options)}`
980
- }
981
- }
982
- })
983
- }),
984
- /*#__PURE__*/ _jsx(TableCell, {
985
- children: /*#__PURE__*/ _jsx(TextField, {
986
- value: (_variant_weightGrams = variant.weightGrams) != null ? _variant_weightGrams : '',
987
- onChange: (event)=>handleVariantField(index, 'weightGrams', event.target.value),
988
- size: "small",
989
- sx: {
990
- width: 88
991
- },
992
- slotProps: {
993
- htmlInput: {
994
- inputMode: 'numeric'
995
- }
996
- }
997
- })
998
- })
999
- ]
1000
- }, variant.id);
1001
- })
1002
- })
1003
- ]
1004
- })
1626
+ /*#__PURE__*/ _jsx(ProductOptionsFields, {
1627
+ options: (_current_options1 = current.options) != null ? _current_options1 : NO_OPTIONS,
1628
+ onChange: handleOptionsChange,
1629
+ onValues: setOptionValues
1630
+ }),
1631
+ /*#__PURE__*/ _jsx(ProductVariantsTable, {
1632
+ variants: current.variants,
1633
+ stockApplies: stockApplies,
1634
+ onField: handleVariantField
1005
1635
  }),
1006
1636
  /*#__PURE__*/ _jsx(Typography, {
1007
1637
  variant: "caption",
@@ -1022,329 +1652,31 @@ const sameRecord = (option, value)=>option.$id === value.$id;
1022
1652
  upgradeHref: subsLocked ? subs.upgradeHref : gifts.upgradeHref,
1023
1653
  children: subsLocked && giftsLocked ? `Subscription products (${(_subs_planLabel = subs.planLabel) != null ? _subs_planLabel : 'higher plan'}) ` + `and gift cards (${(_gifts_planLabel = gifts.planLabel) != null ? _gifts_planLabel : 'higher plan'}) are ` + 'not on your plan. You can still save this product — those ' + 'two options stay locked until you upgrade.' : subsLocked ? `Subscription products are on ${(_subs_planLabel1 = subs.planLabel) != null ? _subs_planLabel1 : 'a higher plan'}. ` + 'A subscription saved without it is refused at checkout, ' + 'so the option stays locked here instead.' : `Gift cards are on ${(_gifts_planLabel1 = gifts.planLabel) != null ? _gifts_planLabel1 : 'a higher plan'}. ` + 'A gift-card product saved without it is refused at ' + 'checkout, so the option stays locked here instead.'
1024
1654
  }) : null,
1025
- /*#__PURE__*/ _jsxs(Stack, {
1026
- direction: "row",
1027
- spacing: 2,
1028
- children: [
1029
- /*#__PURE__*/ _jsxs(TextField, {
1030
- label: "When out of stock",
1031
- value: (_current_oversellPolicy = current.oversellPolicy) != null ? _current_oversellPolicy : 'deny',
1032
- onChange: (event)=>update({
1033
- oversellPolicy: event.target.value
1034
- }),
1035
- size: "small",
1036
- select: true,
1037
- sx: {
1038
- minWidth: 200
1039
- },
1040
- children: [
1041
- /*#__PURE__*/ _jsx(MenuItem, {
1042
- value: "deny",
1043
- children: 'Stop selling (sold out)'
1044
- }),
1045
- /*#__PURE__*/ _jsx(MenuItem, {
1046
- value: "backorder",
1047
- children: 'Keep selling (backorder)'
1048
- })
1049
- ]
1050
- }),
1051
- /*#__PURE__*/ _jsxs(TextField, {
1052
- label: "Kind",
1053
- value: current.giftCard ? 'gift' : 'standard',
1054
- onChange: (event)=>update({
1055
- giftCard: event.target.value === 'gift'
1056
- }),
1057
- size: "small",
1058
- select: true,
1059
- sx: {
1060
- minWidth: 130
1061
- },
1062
- helperText: giftsLocked ? `Gift cards are on ${(_gifts_planLabel2 = gifts.planLabel) != null ? _gifts_planLabel2 : 'a higher plan'}` : 'Gift cards issue a code',
1063
- children: [
1064
- /*#__PURE__*/ _jsx(MenuItem, {
1065
- value: "standard",
1066
- children: 'Standard'
1067
- }),
1068
- /*#__PURE__*/ _jsx(MenuItem, {
1069
- value: "gift",
1070
- disabled: giftsLocked,
1071
- children: giftsLocked ? `Gift card — ${(_gifts_planLabel3 = gifts.planLabel) != null ? _gifts_planLabel3 : 'upgrade'} plan` : 'Gift card'
1072
- })
1073
- ]
1074
- }),
1075
- /*#__PURE__*/ _jsxs(TextField, {
1076
- label: "Tax",
1077
- value: current.taxExempt ? 'exempt' : 'taxable',
1078
- onChange: (event)=>update({
1079
- taxExempt: event.target.value === 'exempt'
1080
- }),
1081
- size: "small",
1082
- select: true,
1083
- sx: {
1084
- minWidth: 120
1085
- },
1086
- children: [
1087
- /*#__PURE__*/ _jsx(MenuItem, {
1088
- value: "taxable",
1089
- children: 'Taxable'
1090
- }),
1091
- /*#__PURE__*/ _jsx(MenuItem, {
1092
- value: "exempt",
1093
- children: 'Tax exempt'
1094
- })
1095
- ]
1096
- }),
1097
- /*#__PURE__*/ _jsx(TextField, {
1098
- label: "Low-stock alert at",
1099
- value: (_current_lowStockThreshold = current.lowStockThreshold) != null ? _current_lowStockThreshold : '',
1100
- placeholder: "Off",
1101
- disabled: !stockApplies,
1102
- onChange: (event)=>{
1103
- const raw = event.target.value.trim();
1104
- update({
1105
- lowStockThreshold: raw === '' ? undefined : Math.max(0, Math.round(Number(raw)))
1106
- });
1107
- },
1108
- size: "small",
1109
- sx: {
1110
- width: 140
1111
- },
1112
- slotProps: {
1113
- htmlInput: {
1114
- inputMode: 'numeric'
1115
- }
1116
- },
1117
- helperText: "Notifies managers"
1118
- })
1119
- ]
1655
+ /*#__PURE__*/ _jsx(ProductSellingFields, {
1656
+ oversellPolicy: (_current_oversellPolicy = current.oversellPolicy) != null ? _current_oversellPolicy : 'deny',
1657
+ giftCard: Boolean(current.giftCard),
1658
+ taxExempt: Boolean(current.taxExempt),
1659
+ lowStockThreshold: current.lowStockThreshold,
1660
+ stockApplies: stockApplies,
1661
+ giftsLocked: giftsLocked,
1662
+ giftsPlanLabel: gifts.planLabel,
1663
+ update: update
1120
1664
  }),
1121
- /*#__PURE__*/ _jsxs(Stack, {
1122
- direction: "row",
1123
- spacing: 2,
1124
- children: [
1125
- /*#__PURE__*/ _jsxs(TextField, {
1126
- label: "Billing",
1127
- value: current.subscription ? current.subscriptionOptional ? 'both' : current.subscription.interval : 'once',
1128
- onChange: (event)=>{
1129
- const value = event.target.value;
1130
- // "Both" (AGL-545): the PDP offers one-time OR subscribe at
1131
- // the same price; the interval field beside picks the cadence.
1132
- // Cleared keys are deleted (not set undefined) so the
1133
- // client-direct setDoc on save never sees undefined values.
1134
- const next = _extends({}, current);
1135
- if (value === 'once') {
1136
- delete next.subscription;
1137
- } else {
1138
- var _current_subscription, _ref;
1139
- var _current_subscription1;
1140
- next.subscription = _extends({}, (_current_subscription = current.subscription) != null ? _current_subscription : {}, {
1141
- interval: value === 'both' ? (_ref = (_current_subscription1 = current.subscription) == null ? void 0 : _current_subscription1.interval) != null ? _ref : 'month' : value
1142
- });
1143
- }
1144
- if (value === 'both') next.subscriptionOptional = true;
1145
- else delete next.subscriptionOptional;
1146
- // setDraft directly: update() spreads the patch over
1147
- // `current`, which would resurrect the deleted keys.
1148
- setDraft(next);
1149
- },
1150
- size: "small",
1151
- select: true,
1152
- sx: {
1153
- minWidth: 180
1154
- },
1155
- helperText: subsLocked ? `Subscriptions are on ${(_subs_planLabel2 = subs.planLabel) != null ? _subs_planLabel2 : 'a higher plan'}` : 'Subscriptions bill until canceled',
1156
- children: [
1157
- /*#__PURE__*/ _jsx(MenuItem, {
1158
- value: "once",
1159
- children: 'One-time purchase'
1160
- }),
1161
- /*#__PURE__*/ _jsx(MenuItem, {
1162
- value: "month",
1163
- disabled: subsLocked,
1164
- children: 'Monthly subscription'
1165
- }),
1166
- /*#__PURE__*/ _jsx(MenuItem, {
1167
- value: "year",
1168
- disabled: subsLocked,
1169
- children: 'Yearly subscription'
1170
- }),
1171
- /*#__PURE__*/ _jsx(MenuItem, {
1172
- value: "both",
1173
- disabled: subsLocked,
1174
- children: 'Both — buyer chooses'
1175
- })
1176
- ]
1177
- }),
1178
- current.subscription && current.subscriptionOptional ? /*#__PURE__*/ _jsxs(TextField, {
1179
- label: "Interval",
1180
- value: current.subscription.interval,
1181
- onChange: (event)=>update({
1182
- subscription: _extends({}, current.subscription, {
1183
- interval: event.target.value
1184
- })
1185
- }),
1186
- size: "small",
1187
- select: true,
1188
- sx: {
1189
- minWidth: 120
1190
- },
1191
- children: [
1192
- /*#__PURE__*/ _jsx(MenuItem, {
1193
- value: "month",
1194
- children: 'Monthly'
1195
- }),
1196
- /*#__PURE__*/ _jsx(MenuItem, {
1197
- value: "year",
1198
- children: 'Yearly'
1199
- })
1200
- ]
1201
- }) : null,
1202
- current.subscription ? /*#__PURE__*/ _jsx(TextField, {
1203
- label: "Free trial (days)",
1204
- placeholder: "None",
1205
- value: (_current_subscription_trialDays = current.subscription.trialDays) != null ? _current_subscription_trialDays : '',
1206
- onChange: (event)=>{
1207
- const raw = event.target.value.trim();
1208
- update({
1209
- subscription: _extends({}, current.subscription, raw === '' ? {
1210
- trialDays: undefined
1211
- } : {
1212
- trialDays: Math.max(1, Math.round(Number(raw)))
1213
- })
1214
- });
1215
- },
1216
- size: "small",
1217
- sx: {
1218
- width: 140
1219
- },
1220
- slotProps: {
1221
- htmlInput: {
1222
- inputMode: 'numeric'
1223
- }
1224
- }
1225
- }) : null
1226
- ]
1665
+ /*#__PURE__*/ _jsx(ProductBillingFields, {
1666
+ subscription: current.subscription,
1667
+ subscriptionOptional: current.subscriptionOptional,
1668
+ subsLocked: subsLocked,
1669
+ subsPlanLabel: subs.planLabel,
1670
+ update: update,
1671
+ replaceDraft: replaceDraft
1227
1672
  }),
1228
- current.type === 'digital' ? /*#__PURE__*/ _jsxs(_Fragment, {
1229
- children: [
1230
- /*#__PURE__*/ _jsx(Divider, {
1231
- textAlign: "left",
1232
- children: 'Digital delivery'
1233
- }),
1234
- ((_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []).map((file, index)=>{
1235
- var _file_version;
1236
- return /*#__PURE__*/ _jsxs(Stack, {
1237
- direction: "row",
1238
- spacing: 1,
1239
- sx: {
1240
- alignItems: 'center'
1241
- },
1242
- children: [
1243
- /*#__PURE__*/ _jsxs(Typography, {
1244
- variant: "body2",
1245
- sx: {
1246
- flex: 1
1247
- },
1248
- noWrap: true,
1249
- children: [
1250
- file.fileName,
1251
- file.version ? ` · v${file.version}` : ''
1252
- ]
1253
- }),
1254
- /*#__PURE__*/ _jsx(PaidMediaProtection, {
1255
- url: file.url,
1256
- hostId: hostId,
1257
- productId: product == null ? void 0 : product.$id,
1258
- kind: "download"
1259
- }),
1260
- /*#__PURE__*/ _jsx(TextField, {
1261
- label: "Version",
1262
- value: (_file_version = file.version) != null ? _file_version : '',
1263
- onChange: (event)=>{
1264
- var _current_digitalFiles;
1265
- const digitalFiles = [
1266
- ...(_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []
1267
- ];
1268
- digitalFiles[index] = _extends({}, file, {
1269
- version: event.target.value.slice(0, 20)
1270
- });
1271
- update({
1272
- digitalFiles
1273
- });
1274
- },
1275
- size: "small",
1276
- sx: {
1277
- width: 100
1278
- }
1279
- }),
1280
- /*#__PURE__*/ _jsx(Button, {
1281
- size: "small",
1282
- color: "error",
1283
- onClick: ()=>{
1284
- var _current_digitalFiles;
1285
- return update({
1286
- digitalFiles: ((_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []).filter((_item, itemIndex)=>itemIndex !== index)
1287
- });
1288
- },
1289
- children: '✕'
1290
- })
1291
- ]
1292
- }, index);
1293
- }),
1294
- /*#__PURE__*/ _jsxs(Stack, {
1295
- direction: "row",
1296
- spacing: 2,
1297
- children: [
1298
- /*#__PURE__*/ _jsx(PaidDownloadAddButton, {
1299
- hostId: hostId,
1300
- productId: product == null ? void 0 : product.$id,
1301
- onAdd: (file)=>{
1302
- var _current_digitalFiles;
1303
- return update({
1304
- digitalFiles: [
1305
- ...(_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : [],
1306
- file
1307
- ]
1308
- });
1309
- }
1310
- }),
1311
- /*#__PURE__*/ _jsx(TextField, {
1312
- label: "Download limit",
1313
- placeholder: "Unlimited",
1314
- value: (_current_downloadLimit = current.downloadLimit) != null ? _current_downloadLimit : '',
1315
- onChange: (event)=>{
1316
- const raw = event.target.value.trim();
1317
- update({
1318
- downloadLimit: raw === '' ? undefined : Math.max(1, Math.round(Number(raw)))
1319
- });
1320
- },
1321
- size: "small",
1322
- sx: {
1323
- width: 140
1324
- },
1325
- slotProps: {
1326
- htmlInput: {
1327
- inputMode: 'numeric'
1328
- }
1329
- },
1330
- helperText: "Attempts per order"
1331
- })
1332
- ]
1333
- }),
1334
- /*#__PURE__*/ _jsx(Typography, {
1335
- variant: "caption",
1336
- color: "text.secondary",
1337
- children: 'Buyers always download the current files — uploading a new ' + 'version re-delivers to everyone.'
1338
- }),
1339
- /*#__PURE__*/ _jsx(MembersVideosField, {
1340
- hostId: hostId,
1341
- productId: product == null ? void 0 : product.$id,
1342
- videos: (_current_gatedVideos = current.gatedVideos) != null ? _current_gatedVideos : [],
1343
- onChange: (gatedVideos)=>update({
1344
- gatedVideos
1345
- })
1346
- })
1347
- ]
1673
+ current.type === 'digital' ? /*#__PURE__*/ _jsx(ProductDigitalFields, {
1674
+ hostId: hostId,
1675
+ productId: product == null ? void 0 : product.$id,
1676
+ digitalFiles: (_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : NO_FILES,
1677
+ downloadLimit: current.downloadLimit,
1678
+ gatedVideos: (_current_gatedVideos = current.gatedVideos) != null ? _current_gatedVideos : NO_VIDEOS,
1679
+ update: update
1348
1680
  }) : null,
1349
1681
  /*#__PURE__*/ _jsx(ProductChipsField, {
1350
1682
  label: "Related products (upsells)",
@@ -1355,74 +1687,23 @@ const sameRecord = (option, value)=>option.$id === value.$id;
1355
1687
  value: pickedRelated,
1356
1688
  onChange: setRelatedProducts
1357
1689
  }),
1358
- /*#__PURE__*/ _jsx(Divider, {
1359
- textAlign: "left",
1360
- children: 'Search engine listing'
1361
- }),
1362
- /*#__PURE__*/ _jsxs(Stack, {
1363
- direction: {
1364
- xs: 'column',
1365
- sm: 'row'
1366
- },
1367
- spacing: 2,
1368
- children: [
1369
- /*#__PURE__*/ _jsx(TextField, {
1370
- label: "SEO title",
1371
- value: (_ref5 = (_current_seo2 = current.seo) == null ? void 0 : _current_seo2.title) != null ? _ref5 : '',
1372
- onChange: (event)=>update({
1373
- seo: _extends({}, current.seo, {
1374
- title: event.target.value
1375
- })
1376
- }),
1377
- // The counts every search listing editor prints (AGL-2910): what a
1378
- // search result shows before it truncates.
1379
- helperText: seoListingFieldCount('title', (_current_seo3 = current.seo) == null ? void 0 : _current_seo3.title),
1380
- error: seoListingFieldTooLong('title', (_current_seo4 = current.seo) == null ? void 0 : _current_seo4.title),
1381
- size: "small",
1382
- fullWidth: true
1383
- }),
1384
- /*#__PURE__*/ _jsx(Button, {
1385
- size: "small",
1386
- onClick: ()=>void pick((media)=>update({
1387
- seo: _extends({}, current.seo, {
1388
- imageUrl: media.url
1389
- })
1390
- })),
1391
- children: ((_current_seo5 = current.seo) == null ? void 0 : _current_seo5.imageUrl) ? 'Change OG image' : 'OG image'
1392
- })
1393
- ]
1394
- }),
1395
- /*#__PURE__*/ _jsx(TextField, {
1396
- label: "SEO description",
1397
- value: (_ref6 = (_current_seo6 = current.seo) == null ? void 0 : _current_seo6.description) != null ? _ref6 : '',
1398
- onChange: (event)=>update({
1399
- seo: _extends({}, current.seo, {
1400
- description: event.target.value
1401
- })
1402
- }),
1403
- helperText: seoListingFieldCount('description', (_current_seo7 = current.seo) == null ? void 0 : _current_seo7.description),
1404
- error: seoListingFieldTooLong('description', (_current_seo8 = current.seo) == null ? void 0 : _current_seo8.description),
1405
- size: "small",
1406
- multiline: true,
1407
- minRows: 2
1690
+ /*#__PURE__*/ _jsx(ProductSeoFields, {
1691
+ title: (_current_seo4 = current.seo) == null ? void 0 : _current_seo4.title,
1692
+ description: (_current_seo5 = current.seo) == null ? void 0 : _current_seo5.description,
1693
+ hasImage: Boolean((_current_seo6 = current.seo) == null ? void 0 : _current_seo6.imageUrl),
1694
+ update: update,
1695
+ pick: pick
1408
1696
  }),
1409
- WidgetSlot ? /*#__PURE__*/ _jsx(WidgetSlot, {
1697
+ WidgetSlot ? /*#__PURE__*/ _jsx(HostedZone, {
1698
+ renderer: WidgetSlot,
1410
1699
  slot: "seoFields",
1411
1700
  hostId: hostId,
1412
1701
  orgId: undefined,
1413
1702
  orgSlug: "",
1414
- subject: {
1415
- kind: 'product',
1416
- id: (_ref7 = product == null ? void 0 : product.$id) != null ? _ref7 : null,
1417
- name: current.name,
1418
- description: String((_current_description2 = current.description) != null ? _current_description2 : '')
1419
- },
1703
+ subject: seoSubject,
1420
1704
  fields: PRODUCT_SEO_LISTING_FIELDS,
1421
- values: {
1422
- title: (_ref8 = (_current_seo9 = current.seo) == null ? void 0 : _current_seo9.title) != null ? _ref8 : '',
1423
- description: (_ref9 = (_current_seo10 = current.seo) == null ? void 0 : _current_seo10.description) != null ? _ref9 : ''
1424
- },
1425
- hasImage: Boolean((_current_seo11 = current.seo) == null ? void 0 : _current_seo11.imageUrl),
1705
+ values: seoValues,
1706
+ hasImage: Boolean((_current_seo7 = current.seo) == null ? void 0 : _current_seo7.imageUrl),
1426
1707
  proposeValues: proposeSeoValues
1427
1708
  }) : null,
1428
1709
  error ? /*#__PURE__*/ _jsx(Alert, {