@aglyn/plugins-commerce 1.0.0-beta.218 → 1.0.0-beta.220

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 (148) hide show
  1. package/README.md +6 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -19
  5. package/src/index.js.map +1 -1
  6. package/src/lib/components/cart.js +2 -1
  7. package/src/lib/components/cart.js.map +1 -1
  8. package/src/lib/components/console/catalog-organization-card.component.js +665 -459
  9. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-orders-card.component.js +2 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/member-posts-card.component.js +1 -1
  13. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  14. package/src/lib/components/console/order-detail-dialog.component.js +8 -7
  15. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  16. package/src/lib/components/console/payments-settings-card.component.js +2 -2
  17. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  18. package/src/lib/components/console/product-editor-dialog.component.js +1221 -889
  19. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/product-zones.d.ts +184 -0
  21. package/src/lib/components/console/product-zones.js +54 -0
  22. package/src/lib/components/console/product-zones.js.map +1 -0
  23. package/src/lib/components/console/products-hub-card.component.js +3 -2
  24. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
  26. package/src/lib/components/console/products-hub-zone.component.js +2 -1
  27. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  28. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  29. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  30. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  31. package/src/lib/components/console/store-settings-card.component.js +2 -2
  32. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  33. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
  34. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  35. package/src/lib/components/member-signup.js +2 -2
  36. package/src/lib/components/member-signup.js.map +1 -1
  37. package/src/lib/components/newsletter-signup.js +2 -2
  38. package/src/lib/components/newsletter-signup.js.map +1 -1
  39. package/src/lib/components/product-detail.js +2 -1
  40. package/src/lib/components/product-detail.js.map +1 -1
  41. package/src/lib/components/product-grid.js +1 -1
  42. package/src/lib/components/product-grid.js.map +1 -1
  43. package/src/lib/constants/operator-alerts.js +5 -3
  44. package/src/lib/constants/operator-alerts.js.map +1 -1
  45. package/src/lib/constants/storefront-payments.d.ts +33 -0
  46. package/src/lib/constants/storefront-payments.js +35 -0
  47. package/src/lib/constants/storefront-payments.js.map +1 -0
  48. package/src/lib/declarations.server.d.ts +3 -1
  49. package/src/lib/declarations.server.js +19 -1
  50. package/src/lib/declarations.server.js.map +1 -1
  51. package/src/lib/model/commerce-io.js +1 -1
  52. package/src/lib/model/commerce-io.js.map +1 -1
  53. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  54. package/src/lib/model/commerce-record-lists.js +45 -0
  55. package/src/lib/model/commerce-record-lists.js.map +1 -0
  56. package/src/lib/model/commerce-record-routes.d.ts +45 -0
  57. package/src/lib/model/commerce-record-routes.js +72 -0
  58. package/src/lib/model/commerce-record-routes.js.map +1 -0
  59. package/src/lib/model/commerce-search-sources.d.ts +21 -0
  60. package/src/lib/model/commerce-search-sources.js +42 -0
  61. package/src/lib/model/commerce-search-sources.js.map +1 -0
  62. package/src/lib/model/commerce-topics.d.ts +20 -0
  63. package/src/lib/model/commerce-topics.js +33 -0
  64. package/src/lib/model/commerce-topics.js.map +1 -0
  65. package/src/lib/model/index.d.ts +1 -0
  66. package/src/lib/model/index.js +1 -0
  67. package/src/lib/model/index.js.map +1 -1
  68. package/src/lib/model/product-record.d.ts +32 -0
  69. package/src/lib/model/product-record.js +82 -0
  70. package/src/lib/model/product-record.js.map +1 -0
  71. package/src/lib/model/site-member-purchases.d.ts +48 -0
  72. package/src/lib/model/site-member-purchases.js +50 -0
  73. package/src/lib/model/site-member-purchases.js.map +1 -0
  74. package/src/lib/model/storefront-tax-summary.d.ts +121 -0
  75. package/src/lib/model/storefront-tax-summary.js +148 -0
  76. package/src/lib/model/storefront-tax-summary.js.map +1 -0
  77. package/src/lib/plan-entitlements.d.ts +35 -0
  78. package/src/lib/plan-entitlements.js +54 -0
  79. package/src/lib/plan-entitlements.js.map +1 -0
  80. package/src/lib/plugin.js +21 -0
  81. package/src/lib/plugin.js.map +1 -1
  82. package/src/lib/server/billing-webhook.js +387 -248
  83. package/src/lib/server/billing-webhook.js.map +1 -1
  84. package/src/lib/server/cart-checkout.js +3 -3
  85. package/src/lib/server/cart-checkout.js.map +1 -1
  86. package/src/lib/server/checkout.js +2 -2
  87. package/src/lib/server/checkout.js.map +1 -1
  88. package/src/lib/server/connect.js +5 -5
  89. package/src/lib/server/connect.js.map +1 -1
  90. package/src/lib/server/draft-order.js +2 -2
  91. package/src/lib/server/draft-order.js.map +1 -1
  92. package/src/lib/server/gift-cards.js +7 -3
  93. package/src/lib/server/gift-cards.js.map +1 -1
  94. package/src/lib/server/low-stock.js +3 -1
  95. package/src/lib/server/low-stock.js.map +1 -1
  96. package/src/lib/server/member-post.js +2 -2
  97. package/src/lib/server/member-post.js.map +1 -1
  98. package/src/lib/server/membership-admin-password.js +4 -1
  99. package/src/lib/server/membership-admin-password.js.map +1 -1
  100. package/src/lib/server/membership-content.js +1 -1
  101. package/src/lib/server/membership-content.js.map +1 -1
  102. package/src/lib/server/membership-register.js +13 -2
  103. package/src/lib/server/membership-register.js.map +1 -1
  104. package/src/lib/server/newsletter.js +32 -19
  105. package/src/lib/server/newsletter.js.map +1 -1
  106. package/src/lib/server/pos-order.js +3 -3
  107. package/src/lib/server/pos-order.js.map +1 -1
  108. package/src/lib/server/process-abandoned.js +4 -2
  109. package/src/lib/server/process-abandoned.js.map +1 -1
  110. package/src/lib/server/process-restock.js +7 -5
  111. package/src/lib/server/process-restock.js.map +1 -1
  112. package/src/lib/server/product-record-index.d.ts +19 -0
  113. package/src/lib/server/product-record-index.js +83 -0
  114. package/src/lib/server/product-record-index.js.map +1 -0
  115. package/src/lib/server/reconcile-stock.js +1 -1
  116. package/src/lib/server/reconcile-stock.js.map +1 -1
  117. package/src/lib/server/reserve-stock.js +6 -1
  118. package/src/lib/server/reserve-stock.js.map +1 -1
  119. package/src/lib/server/reserve.js +2 -2
  120. package/src/lib/server/reserve.js.map +1 -1
  121. package/src/lib/server/revenue-source.d.ts +130 -0
  122. package/src/lib/server/revenue-source.js +286 -0
  123. package/src/lib/server/revenue-source.js.map +1 -0
  124. package/src/lib/server/supplier-outbox.js +16 -1
  125. package/src/lib/server/supplier-outbox.js.map +1 -1
  126. package/src/lib/server/supplier-update.js +19 -9
  127. package/src/lib/server/supplier-update.js.map +1 -1
  128. package/src/lib/server/tax-profile.d.ts +8 -5
  129. package/src/lib/server/tax-profile.js +22 -5
  130. package/src/lib/server/tax-profile.js.map +1 -1
  131. package/src/lib/server/tax-return-source.d.ts +125 -0
  132. package/src/lib/server/tax-return-source.js +524 -0
  133. package/src/lib/server/tax-return-source.js.map +1 -0
  134. package/src/lib/server/tax-summary.d.ts +92 -0
  135. package/src/lib/server/tax-summary.js +258 -0
  136. package/src/lib/server/tax-summary.js.map +1 -0
  137. package/src/lib/server.d.ts +1 -0
  138. package/src/lib/server.js +23 -0
  139. package/src/lib/server.js.map +1 -1
  140. package/src/lib/starter-templates.d.ts +19 -0
  141. package/src/lib/starter-templates.js +157 -0
  142. package/src/lib/starter-templates.js.map +1 -0
  143. package/src/lib/tenant-emails.d.ts +33 -0
  144. package/src/lib/tenant-emails.js +551 -0
  145. package/src/lib/tenant-emails.js.map +1 -0
  146. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  147. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  148. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
@@ -23,14 +23,16 @@ import { Timestamp } from "@aglyn/shared-util-timestamp";
23
23
  import { Alert, Autocomplete, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, MenuItem, Stack, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@mui/material";
24
24
  import { collection, doc, getDoc } from "firebase/firestore";
25
25
  import { productCollectionFields } from "./smart-collections.js";
26
- import { useCallback, useMemo, useState } from "react";
26
+ import { memo, useCallback, useMemo, useRef, useState } from "react";
27
27
  import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection, useHostResourceApi, writeGuardedBySeed, useUser } from "@aglyn/tenant-feature-instance";
28
28
  import { writeSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change";
29
29
  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";
35
+ import { PRODUCT_EDITOR_ZONE } from "./product-zones.js";
34
36
  import { MembersVideosField, PaidDownloadAddButton, PaidMediaProtection } from "./paid-media.js";
35
37
  /**
36
38
  * What each picker in this dialog will offer.
@@ -72,6 +74,991 @@ function comboLabel(options) {
72
74
  const values = Object.values(options != null ? options : {});
73
75
  return values.length ? values.join(' / ') : 'Default';
74
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 = [];
81
+ const FILL_ROW = {
82
+ flex: 1
83
+ };
84
+ const categoryLabel = (category)=>category.name;
85
+ const recordLabel = (item)=>{
86
+ var _item_name;
87
+ return (_item_name = item.name) != null ? _item_name : item.$id;
88
+ };
89
+ const sameRecord = (option, value)=>option.$id === value.$id;
90
+ /**
91
+ * One of the editor's chip lists — tags, categories, an option's values,
92
+ * related products — MEMOIZED, and every prop it takes is stable while its
93
+ * own list is (AGL-3423).
94
+ *
95
+ * A multiple Autocomplete hands its input a new chip array as
96
+ * `startAdornment` on every render, and MUI's `InputBase` copies that into
97
+ * its `FormControl` from a passive effect: a state update left pending after
98
+ * every commit the field takes part in. React 19 counts each such commit as a
99
+ * nested update, and keystrokes delivered back to back commit one after
100
+ * another with nothing to clear the count, so the fifty-first throws
101
+ * "Maximum update depth exceeded" (#185). Every field in this editor writes
102
+ * one draft, so every keystroke in any of them re-rendered all four lists;
103
+ * kept out of those renders, a list commits only when it changes.
104
+ */ const ProductChipsField = /*#__PURE__*/ memo(function ProductChipsField(props) {
105
+ const { index = 0, onChange } = props;
106
+ return /*#__PURE__*/ _jsx(Autocomplete, {
107
+ multiple: true,
108
+ freeSolo: props.freeSolo,
109
+ options: props.options,
110
+ value: props.value,
111
+ getOptionLabel: props.getOptionLabel,
112
+ isOptionEqualToValue: props.isOptionEqualToValue,
113
+ onChange: (_event, next)=>onChange(next, index),
114
+ renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
115
+ label: props.label,
116
+ size: "small",
117
+ placeholder: props.placeholder,
118
+ helperText: props.helperText
119
+ })),
120
+ sx: props.fill ? FILL_ROW : undefined
121
+ });
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
+ });
75
1062
  /**
76
1063
  * Products hub editor (AGL-279): the full catalog editor — basics,
77
1064
  * media, tags, options → variants matrix, per-variant pricing/stock, and
@@ -79,8 +1066,8 @@ function comboLabel(options) {
79
1066
  * first variant so the legacy Product block + checkout API (AGL-90) keep
80
1067
  * charging correctly without reading the variants array.
81
1068
  */ export function ProductEditorDialog(props) {
82
- 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;
83
- 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;
84
1071
  const { hostId, product, seedFromCache, seedUnreadable, open, onClose } = props;
85
1072
  /**
86
1073
  * Field-level entitlement gates (AGL-2080). Both of these were fully
@@ -160,7 +1147,50 @@ function comboLabel(options) {
160
1147
  ]);
161
1148
  // Lazy-init per open; parent remounts via `key` on product change.
162
1149
  const current = (_ref = draft != null ? draft : lifted) != null ? _ref : blankProduct();
163
- const update = (patch)=>setDraft(_extends({}, current, patch));
1150
+ /*
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.
1158
+ */ const latest = useRef(current);
1159
+ latest.current = current;
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({
1165
+ tags: tags.map((tag)=>String(tag).trim()).filter(Boolean)
1166
+ }), [
1167
+ update
1168
+ ]);
1169
+ const setCategories = useCallback((picked)=>update({
1170
+ categoryIds: picked.map((category)=>category.$id)
1171
+ }), [
1172
+ update
1173
+ ]);
1174
+ const setRelatedProducts = useCallback((picked)=>update({
1175
+ relatedProductIds: picked.map((item)=>item.$id)
1176
+ }), [
1177
+ update
1178
+ ]);
1179
+ const categoryIds = current.categoryIds;
1180
+ const pickedCategories = useMemo(()=>categories.rows.filter((category)=>(categoryIds != null ? categoryIds : []).includes(category.$id)), [
1181
+ categories.rows,
1182
+ categoryIds
1183
+ ]);
1184
+ const relatedProductIds = current.relatedProductIds;
1185
+ const productId = product == null ? void 0 : product.$id;
1186
+ const relatedOptions = useMemo(()=>relatedProducts.rows.filter((item)=>!item.deletedAt && item.$id !== productId), [
1187
+ relatedProducts.rows,
1188
+ productId
1189
+ ]);
1190
+ const pickedRelated = useMemo(()=>relatedProducts.rows.filter((item)=>(relatedProductIds != null ? relatedProductIds : []).includes(item.$id)), [
1191
+ relatedProducts.rows,
1192
+ relatedProductIds
1193
+ ]);
164
1194
  /** The shell's zone renderer (AGL-2910); `null` outside the console shell. */ const WidgetSlot = useConsoleWidgetSlot();
165
1195
  /**
166
1196
  * A listing zone widget's proposal, staged into the draft like typing.
@@ -207,7 +1237,10 @@ function comboLabel(options) {
207
1237
  }), [
208
1238
  categories.rows
209
1239
  ]);
210
- 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({
211
1244
  id: (_ref1 = product == null ? void 0 : product.$id) != null ? _ref1 : null,
212
1245
  name: current.name,
213
1246
  type: current.type,
@@ -221,7 +1254,17 @@ function comboLabel(options) {
221
1254
  mediaUrls: (_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : [],
222
1255
  seoTitle: (_ref2 = (_current_seo = current.seo) == null ? void 0 : _current_seo.title) != null ? _ref2 : '',
223
1256
  seoDescription: (_ref3 = (_current_seo1 = current.seo) == null ? void 0 : _current_seo1.description) != null ? _ref3 : ''
224
- };
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
+ });
225
1268
  const error = current.name ? CommerceModel.validateProduct(current) : null;
226
1269
  /**
227
1270
  * Stock tracking is inert on a subscription-only product (AGL-1744) —
@@ -244,85 +1287,106 @@ function comboLabel(options) {
244
1287
  * count this just cleared. Keys are DELETED rather than set undefined; the
245
1288
  * save is a client-direct `setDoc`, which rejects undefined values.
246
1289
  */ const handleClearStockTracking = ()=>{
247
- update({
248
- variants: current.variants.map((variant)=>{
249
- const { inventoryByLocation: _perLocation } = variant, rest = _object_without_properties_loose(variant, [
250
- "inventoryByLocation"
251
- ]);
252
- return _extends({}, rest, {
253
- inventory: null
254
- });
255
- })
256
- });
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
+ }));
257
1300
  };
258
- const handleName = (name)=>update(_extends({
1301
+ const handleName = useCallback((event)=>{
1302
+ const name = event.target.value;
1303
+ update(_extends({
259
1304
  name
260
1305
  }, !product && !slugTouched ? {
261
1306
  slug: CommerceModel.commerceSlug(name)
262
1307
  } : {}));
263
- const handleOptionsChange = useCallback((index, patch)=>{
264
- var _current_options;
265
- var _current_options1;
266
- // A RENAME moves each variant's selection to the new name (AGL-3066).
267
- // Rebuilding the matrix matches variants by name and value, so under a
268
- // new name it finds none and replaces every variant, its id, price, SKU
269
- // and stock with them. Only a change of values rebuilds.
270
- if (patch && ((_current_options1 = current.options) == null ? void 0 : _current_options1[index]) && Object.keys(patch).length === 1 && typeof patch.name === 'string') {
271
- update(CommerceModel.renameProductOptions(current, current.options.map((option, at)=>at === index ? patch.name : option.name)));
272
- return;
273
- }
274
- const options = [
275
- ...(_current_options = current.options) != null ? _current_options : []
276
- ];
277
- if (patch === null) options.splice(index, 1);
278
- else options[index] = _extends({
279
- name: '',
280
- values: []
281
- }, options[index], patch);
282
- // Regenerate the matrix, carrying data over by option-combo key so
283
- // edits to prices/SKUs survive option tweaks.
284
- const previous = new Map(current.variants.map((variant)=>[
285
- comboKey(variant.options),
286
- variant
287
- ]));
288
- const fallback = current.variants[0];
289
- const variants = CommerceModel.expandVariantMatrix(options).map((combo, comboIndex)=>{
290
- var _ref, _ref1;
291
- const existing = previous.get(comboKey(combo));
292
- return existing != null ? existing : {
293
- id: `v${Date.now().toString(36)}${comboIndex}`,
294
- options: combo,
295
- priceUsd: (_ref = fallback == null ? void 0 : fallback.priceUsd) != null ? _ref : 0,
296
- inventory: (_ref1 = fallback == null ? void 0 : fallback.inventory) != null ? _ref1 : null
297
- };
298
- });
299
- update({
300
- options,
301
- variants
302
- });
303
1308
  }, [
304
- current
1309
+ product,
1310
+ slugTouched,
1311
+ update
305
1312
  ]);
306
- const handleVariantField = (index, field, raw)=>{
307
- const variants = [
308
- ...current.variants
309
- ];
310
- const numeric = [
311
- 'priceUsd',
312
- 'compareAtPriceUsd',
313
- 'weightGrams'
314
- ];
315
- const value = field === 'inventory' ? raw.trim() === '' ? null : Math.max(0, Math.round(Number(raw))) : numeric.includes(field) ? raw.trim() === '' ? undefined : Number(raw) : raw;
316
- variants[index] = _extends({}, variants[index], {
317
- [field]: value
318
- });
319
- if (value === undefined && (field === 'compareAtPriceUsd' || field === 'weightGrams')) {
320
- delete variants[index][field];
321
- }
1313
+ const handleSlug = useCallback((event)=>{
1314
+ setSlugTouched(true);
322
1315
  update({
323
- variants
1316
+ slug: CommerceModel.commerceSlug(event.target.value)
324
1317
  });
325
- };
1318
+ }, [
1319
+ update
1320
+ ]);
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, {
1364
+ values: values.map((value)=>String(value).trim()).filter(Boolean)
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
+ ]);
326
1390
  const handleSave = useCallback(async ()=>{
327
1391
  var _ref, _ref1, _ref2;
328
1392
  var _current_mediaUrls;
@@ -509,163 +1573,37 @@ function comboLabel(options) {
509
1573
  gap: 2
510
1574
  },
511
1575
  children: [
512
- /*#__PURE__*/ _jsxs(Stack, {
513
- direction: {
514
- xs: 'column',
515
- sm: 'row'
516
- },
517
- spacing: 2,
518
- sx: {
519
- mt: 1
520
- },
521
- children: [
522
- /*#__PURE__*/ _jsx(TextField, {
523
- label: "Name",
524
- value: current.name,
525
- onChange: (event)=>handleName(event.target.value),
526
- size: "small",
527
- autoFocus: true,
528
- fullWidth: true
529
- }),
530
- /*#__PURE__*/ _jsx(TextField, {
531
- label: "Slug",
532
- value: current.slug,
533
- onChange: (event)=>{
534
- setSlugTouched(true);
535
- update({
536
- slug: CommerceModel.commerceSlug(event.target.value)
537
- });
538
- },
539
- size: "small",
540
- fullWidth: true,
541
- helperText: current.slug ? `/products/${current.slug}` : undefined
542
- })
543
- ]
544
- }),
545
- /*#__PURE__*/ _jsxs(Stack, {
546
- direction: "row",
547
- spacing: 2,
548
- children: [
549
- /*#__PURE__*/ _jsxs(TextField, {
550
- label: "Type",
551
- value: current.type,
552
- onChange: (event)=>update({
553
- type: event.target.value
554
- }),
555
- size: "small",
556
- select: true,
557
- sx: {
558
- minWidth: 140
559
- },
560
- children: [
561
- /*#__PURE__*/ _jsx(MenuItem, {
562
- value: "physical",
563
- children: 'Physical'
564
- }),
565
- /*#__PURE__*/ _jsx(MenuItem, {
566
- value: "digital",
567
- children: 'Digital'
568
- }),
569
- /*#__PURE__*/ _jsx(MenuItem, {
570
- value: "service",
571
- children: 'Service'
572
- })
573
- ]
574
- }),
575
- /*#__PURE__*/ _jsxs(TextField, {
576
- label: "Status",
577
- value: current.status,
578
- onChange: (event)=>update({
579
- status: event.target.value
580
- }),
581
- size: "small",
582
- select: true,
583
- sx: {
584
- minWidth: 140
585
- },
586
- children: [
587
- /*#__PURE__*/ _jsx(MenuItem, {
588
- value: "draft",
589
- children: 'Draft'
590
- }),
591
- /*#__PURE__*/ _jsx(MenuItem, {
592
- value: "active",
593
- children: 'Active'
594
- }),
595
- /*#__PURE__*/ _jsx(MenuItem, {
596
- value: "archived",
597
- children: 'Archived'
598
- })
599
- ]
600
- }),
601
- suppliers.rows.length > 0 ? /*#__PURE__*/ _jsxs(TextField, {
602
- label: "Supplier",
603
- value: (_current_supplierId = current.supplierId) != null ? _current_supplierId : '',
604
- onChange: (event)=>update({
605
- supplierId: event.target.value || undefined
606
- }),
607
- size: "small",
608
- select: true,
609
- sx: {
610
- minWidth: 160
611
- },
612
- helperText: "Routes paid orders",
613
- children: [
614
- /*#__PURE__*/ _jsx(MenuItem, {
615
- value: "",
616
- children: 'None (self-fulfilled)'
617
- }),
618
- suppliers.rows.map((supplier)=>/*#__PURE__*/ _jsx(MenuItem, {
619
- value: supplier.$id,
620
- children: supplier.name
621
- }, supplier.$id))
622
- ]
623
- }) : null
624
- ]
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
625
1587
  }),
626
- /*#__PURE__*/ _jsx(TextField, {
627
- label: "Description",
628
- value: (_current_description1 = current.description) != null ? _current_description1 : '',
629
- onChange: (event)=>update({
630
- description: event.target.value
631
- }),
632
- size: "small",
633
- multiline: true,
634
- minRows: 2
635
- }),
636
- /*#__PURE__*/ _jsx(Autocomplete, {
637
- multiple: true,
1588
+ /*#__PURE__*/ _jsx(ProductChipsField, {
638
1589
  freeSolo: true,
639
- options: [],
640
- value: (_current_tags1 = current.tags) != null ? _current_tags1 : [],
641
- onChange: (_event, tags)=>update({
642
- tags: tags.map((tag)=>String(tag).trim()).filter(Boolean)
643
- }),
644
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
645
- label: "Tags",
646
- size: "small",
647
- placeholder: "Type and press Enter"
648
- }))
1590
+ label: "Tags",
1591
+ placeholder: "Type and press Enter",
1592
+ options: NO_ITEMS,
1593
+ value: (_current_tags1 = current.tags) != null ? _current_tags1 : NO_ITEMS,
1594
+ onChange: setTags
649
1595
  }),
650
- categories.rows.length > 0 ? /*#__PURE__*/ _jsx(Autocomplete, {
651
- multiple: true,
1596
+ categories.rows.length > 0 ? /*#__PURE__*/ _jsx(ProductChipsField, {
1597
+ label: "Categories",
652
1598
  options: categories.rows,
653
- getOptionLabel: (category)=>category.name,
654
- isOptionEqualToValue: (option, value)=>option.$id === value.$id,
655
- value: categories.rows.filter((category)=>{
656
- var _current_categoryIds;
657
- return ((_current_categoryIds = current.categoryIds) != null ? _current_categoryIds : []).includes(category.$id);
658
- }),
659
- onChange: (_event, picked)=>update({
660
- categoryIds: picked.map((category)=>category.$id)
661
- }),
662
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
663
- label: "Categories",
664
- size: "small"
665
- }))
1599
+ getOptionLabel: categoryLabel,
1600
+ isOptionEqualToValue: sameRecord,
1601
+ value: pickedCategories,
1602
+ onChange: setCategories
666
1603
  }) : null,
667
- WidgetSlot ? /*#__PURE__*/ _jsx(WidgetSlot, {
668
- slot: "productEditor",
1604
+ WidgetSlot ? /*#__PURE__*/ _jsx(HostedZone, {
1605
+ renderer: WidgetSlot,
1606
+ slot: PRODUCT_EDITOR_ZONE.id,
669
1607
  hostId: hostId,
670
1608
  orgId: undefined,
671
1609
  product: zoneProduct,
@@ -676,269 +1614,24 @@ function comboLabel(options) {
676
1614
  textAlign: "left",
677
1615
  children: 'Media'
678
1616
  }),
679
- /*#__PURE__*/ _jsxs(Box, {
680
- sx: {
681
- display: 'flex',
682
- gap: 1,
683
- flexWrap: 'wrap'
684
- },
685
- children: [
686
- ((_current_mediaUrls1 = current.mediaUrls) != null ? _current_mediaUrls1 : []).map((url, index)=>/*#__PURE__*/ _jsxs(Box, {
687
- sx: {
688
- position: 'relative'
689
- },
690
- children: [
691
- /*#__PURE__*/ _jsx(Box, {
692
- component: "img",
693
- src: url,
694
- alt: "",
695
- sx: {
696
- width: 72,
697
- height: 72,
698
- objectFit: 'cover',
699
- borderRadius: 1,
700
- border: 1,
701
- borderColor: 'divider'
702
- }
703
- }),
704
- /*#__PURE__*/ _jsx(IconButton, {
705
- size: "small",
706
- "aria-label": "Remove image",
707
- onClick: ()=>{
708
- var _current_mediaUrls;
709
- return update({
710
- mediaUrls: ((_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : []).filter((_item, itemIndex)=>itemIndex !== index)
711
- });
712
- },
713
- sx: {
714
- position: 'absolute',
715
- top: -8,
716
- right: -8,
717
- bgcolor: 'background.paper',
718
- border: 1,
719
- borderColor: 'divider',
720
- p: 0.25
721
- },
722
- children: '✕'
723
- })
724
- ]
725
- }, `${url}-${index}`)),
726
- /*#__PURE__*/ _jsx(Button, {
727
- size: "small",
728
- onClick: ()=>void pick((media)=>{
729
- var _current_mediaUrls;
730
- return update({
731
- mediaUrls: [
732
- ...(_current_mediaUrls = current.mediaUrls) != null ? _current_mediaUrls : [],
733
- media.url
734
- ]
735
- });
736
- }),
737
- children: 'Add image'
738
- })
739
- ]
1617
+ /*#__PURE__*/ _jsx(ProductMediaFields, {
1618
+ mediaUrls: (_current_mediaUrls1 = current.mediaUrls) != null ? _current_mediaUrls1 : NO_ITEMS,
1619
+ update: update,
1620
+ pick: pick
740
1621
  }),
741
1622
  /*#__PURE__*/ _jsx(Divider, {
742
1623
  textAlign: "left",
743
1624
  children: 'Options & variants'
744
1625
  }),
745
- ((_current_options1 = current.options) != null ? _current_options1 : []).map((option, index)=>/*#__PURE__*/ _jsxs(Stack, {
746
- direction: "row",
747
- spacing: 1,
748
- sx: {
749
- alignItems: 'flex-start'
750
- },
751
- children: [
752
- /*#__PURE__*/ _jsx(TextField, {
753
- label: "Option",
754
- value: option.name,
755
- onChange: (event)=>handleOptionsChange(index, {
756
- name: event.target.value
757
- }),
758
- size: "small",
759
- sx: {
760
- width: 160
761
- },
762
- placeholder: "Size"
763
- }),
764
- /*#__PURE__*/ _jsx(Autocomplete, {
765
- multiple: true,
766
- freeSolo: true,
767
- options: [],
768
- value: option.values,
769
- onChange: (_event, values)=>handleOptionsChange(index, {
770
- values: values.map((value)=>String(value).trim()).filter(Boolean)
771
- }),
772
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
773
- label: "Values",
774
- size: "small",
775
- placeholder: "S, M, L…"
776
- })),
777
- sx: {
778
- flex: 1
779
- }
780
- }),
781
- /*#__PURE__*/ _jsx(Button, {
782
- size: "small",
783
- color: "error",
784
- onClick: ()=>handleOptionsChange(index, null),
785
- sx: {
786
- mt: 0.5
787
- },
788
- children: 'Remove'
789
- })
790
- ]
791
- }, index)),
792
- ((_ref4 = (_current_options2 = current.options) == null ? void 0 : _current_options2.length) != null ? _ref4 : 0) < CommerceModel.COMMERCE_MAX_OPTIONS ? /*#__PURE__*/ _jsx(Button, {
793
- size: "small",
794
- sx: {
795
- alignSelf: 'flex-start'
796
- },
797
- onClick: ()=>{
798
- var _ref;
799
- var _current_options;
800
- return handleOptionsChange((_ref = (_current_options = current.options) == null ? void 0 : _current_options.length) != null ? _ref : 0, {
801
- name: '',
802
- values: []
803
- });
804
- },
805
- children: 'Add option'
806
- }) : null,
807
- /*#__PURE__*/ _jsx(Box, {
808
- children: /*#__PURE__*/ _jsxs(ScrollTable, {
809
- size: "small",
810
- children: [
811
- /*#__PURE__*/ _jsx(TableHead, {
812
- children: /*#__PURE__*/ _jsxs(TableRow, {
813
- children: [
814
- /*#__PURE__*/ _jsx(TableCell, {
815
- children: 'Variant'
816
- }),
817
- /*#__PURE__*/ _jsx(TableCell, {
818
- children: 'Price ($)'
819
- }),
820
- /*#__PURE__*/ _jsx(TableCell, {
821
- children: 'Compare-at'
822
- }),
823
- /*#__PURE__*/ _jsx(TableCell, {
824
- children: 'SKU'
825
- }),
826
- /*#__PURE__*/ _jsx(TableCell, {
827
- children: 'Barcode'
828
- }),
829
- /*#__PURE__*/ _jsx(TableCell, {
830
- children: 'Stock'
831
- }),
832
- /*#__PURE__*/ _jsx(TableCell, {
833
- children: 'Weight (g)'
834
- })
835
- ]
836
- })
837
- }),
838
- /*#__PURE__*/ _jsx(TableBody, {
839
- children: current.variants.map((variant, index)=>{
840
- var _variant_priceUsd, _variant_compareAtPriceUsd, _variant_sku, _variant_barcode, _variant_inventory, _variant_weightGrams;
841
- return /*#__PURE__*/ _jsxs(TableRow, {
842
- children: [
843
- /*#__PURE__*/ _jsx(TableCell, {
844
- sx: {
845
- whiteSpace: 'nowrap'
846
- },
847
- children: comboLabel(variant.options)
848
- }),
849
- /*#__PURE__*/ _jsx(TableCell, {
850
- children: /*#__PURE__*/ _jsx(TextField, {
851
- value: (_variant_priceUsd = variant.priceUsd) != null ? _variant_priceUsd : '',
852
- onChange: (event)=>handleVariantField(index, 'priceUsd', event.target.value),
853
- size: "small",
854
- sx: {
855
- width: 88
856
- },
857
- // An empty price is marked (AGL-2916): a proposed
858
- // product arrives with none, and Save waits for one.
859
- error: !CommerceModel.variantHasPrice(variant),
860
- placeholder: "Set",
861
- slotProps: {
862
- htmlInput: {
863
- inputMode: 'decimal'
864
- }
865
- }
866
- })
867
- }),
868
- /*#__PURE__*/ _jsx(TableCell, {
869
- children: /*#__PURE__*/ _jsx(TextField, {
870
- value: (_variant_compareAtPriceUsd = variant.compareAtPriceUsd) != null ? _variant_compareAtPriceUsd : '',
871
- onChange: (event)=>handleVariantField(index, 'compareAtPriceUsd', event.target.value),
872
- size: "small",
873
- sx: {
874
- width: 88
875
- },
876
- slotProps: {
877
- htmlInput: {
878
- inputMode: 'decimal'
879
- }
880
- }
881
- })
882
- }),
883
- /*#__PURE__*/ _jsx(TableCell, {
884
- children: /*#__PURE__*/ _jsx(TextField, {
885
- value: (_variant_sku = variant.sku) != null ? _variant_sku : '',
886
- onChange: (event)=>handleVariantField(index, 'sku', event.target.value),
887
- size: "small",
888
- sx: {
889
- width: 110
890
- }
891
- })
892
- }),
893
- /*#__PURE__*/ _jsx(TableCell, {
894
- children: /*#__PURE__*/ _jsx(TextField, {
895
- value: (_variant_barcode = variant.barcode) != null ? _variant_barcode : '',
896
- onChange: (event)=>handleVariantField(index, 'barcode', event.target.value),
897
- size: "small",
898
- sx: {
899
- width: 110
900
- }
901
- })
902
- }),
903
- /*#__PURE__*/ _jsx(TableCell, {
904
- children: /*#__PURE__*/ _jsx(TextField, {
905
- value: (_variant_inventory = variant.inventory) != null ? _variant_inventory : '',
906
- placeholder: stockApplies ? '—' : 'n/a',
907
- disabled: !stockApplies,
908
- onChange: (event)=>handleVariantField(index, 'inventory', event.target.value),
909
- size: "small",
910
- sx: {
911
- width: 72
912
- },
913
- slotProps: {
914
- htmlInput: {
915
- inputMode: 'numeric',
916
- 'aria-label': `Stock — ${comboLabel(variant.options)}`
917
- }
918
- }
919
- })
920
- }),
921
- /*#__PURE__*/ _jsx(TableCell, {
922
- children: /*#__PURE__*/ _jsx(TextField, {
923
- value: (_variant_weightGrams = variant.weightGrams) != null ? _variant_weightGrams : '',
924
- onChange: (event)=>handleVariantField(index, 'weightGrams', event.target.value),
925
- size: "small",
926
- sx: {
927
- width: 88
928
- },
929
- slotProps: {
930
- htmlInput: {
931
- inputMode: 'numeric'
932
- }
933
- }
934
- })
935
- })
936
- ]
937
- }, variant.id);
938
- })
939
- })
940
- ]
941
- })
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
942
1635
  }),
943
1636
  /*#__PURE__*/ _jsx(Typography, {
944
1637
  variant: "caption",
@@ -959,419 +1652,58 @@ function comboLabel(options) {
959
1652
  upgradeHref: subsLocked ? subs.upgradeHref : gifts.upgradeHref,
960
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.'
961
1654
  }) : null,
962
- /*#__PURE__*/ _jsxs(Stack, {
963
- direction: "row",
964
- spacing: 2,
965
- children: [
966
- /*#__PURE__*/ _jsxs(TextField, {
967
- label: "When out of stock",
968
- value: (_current_oversellPolicy = current.oversellPolicy) != null ? _current_oversellPolicy : 'deny',
969
- onChange: (event)=>update({
970
- oversellPolicy: event.target.value
971
- }),
972
- size: "small",
973
- select: true,
974
- sx: {
975
- minWidth: 200
976
- },
977
- children: [
978
- /*#__PURE__*/ _jsx(MenuItem, {
979
- value: "deny",
980
- children: 'Stop selling (sold out)'
981
- }),
982
- /*#__PURE__*/ _jsx(MenuItem, {
983
- value: "backorder",
984
- children: 'Keep selling (backorder)'
985
- })
986
- ]
987
- }),
988
- /*#__PURE__*/ _jsxs(TextField, {
989
- label: "Kind",
990
- value: current.giftCard ? 'gift' : 'standard',
991
- onChange: (event)=>update({
992
- giftCard: event.target.value === 'gift'
993
- }),
994
- size: "small",
995
- select: true,
996
- sx: {
997
- minWidth: 130
998
- },
999
- helperText: giftsLocked ? `Gift cards are on ${(_gifts_planLabel2 = gifts.planLabel) != null ? _gifts_planLabel2 : 'a higher plan'}` : 'Gift cards issue a code',
1000
- children: [
1001
- /*#__PURE__*/ _jsx(MenuItem, {
1002
- value: "standard",
1003
- children: 'Standard'
1004
- }),
1005
- /*#__PURE__*/ _jsx(MenuItem, {
1006
- value: "gift",
1007
- disabled: giftsLocked,
1008
- children: giftsLocked ? `Gift card — ${(_gifts_planLabel3 = gifts.planLabel) != null ? _gifts_planLabel3 : 'upgrade'} plan` : 'Gift card'
1009
- })
1010
- ]
1011
- }),
1012
- /*#__PURE__*/ _jsxs(TextField, {
1013
- label: "Tax",
1014
- value: current.taxExempt ? 'exempt' : 'taxable',
1015
- onChange: (event)=>update({
1016
- taxExempt: event.target.value === 'exempt'
1017
- }),
1018
- size: "small",
1019
- select: true,
1020
- sx: {
1021
- minWidth: 120
1022
- },
1023
- children: [
1024
- /*#__PURE__*/ _jsx(MenuItem, {
1025
- value: "taxable",
1026
- children: 'Taxable'
1027
- }),
1028
- /*#__PURE__*/ _jsx(MenuItem, {
1029
- value: "exempt",
1030
- children: 'Tax exempt'
1031
- })
1032
- ]
1033
- }),
1034
- /*#__PURE__*/ _jsx(TextField, {
1035
- label: "Low-stock alert at",
1036
- value: (_current_lowStockThreshold = current.lowStockThreshold) != null ? _current_lowStockThreshold : '',
1037
- placeholder: "Off",
1038
- disabled: !stockApplies,
1039
- onChange: (event)=>{
1040
- const raw = event.target.value.trim();
1041
- update({
1042
- lowStockThreshold: raw === '' ? undefined : Math.max(0, Math.round(Number(raw)))
1043
- });
1044
- },
1045
- size: "small",
1046
- sx: {
1047
- width: 140
1048
- },
1049
- slotProps: {
1050
- htmlInput: {
1051
- inputMode: 'numeric'
1052
- }
1053
- },
1054
- helperText: "Notifies managers"
1055
- })
1056
- ]
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
1057
1664
  }),
1058
- /*#__PURE__*/ _jsxs(Stack, {
1059
- direction: "row",
1060
- spacing: 2,
1061
- children: [
1062
- /*#__PURE__*/ _jsxs(TextField, {
1063
- label: "Billing",
1064
- value: current.subscription ? current.subscriptionOptional ? 'both' : current.subscription.interval : 'once',
1065
- onChange: (event)=>{
1066
- const value = event.target.value;
1067
- // "Both" (AGL-545): the PDP offers one-time OR subscribe at
1068
- // the same price; the interval field beside picks the cadence.
1069
- // Cleared keys are deleted (not set undefined) so the
1070
- // client-direct setDoc on save never sees undefined values.
1071
- const next = _extends({}, current);
1072
- if (value === 'once') {
1073
- delete next.subscription;
1074
- } else {
1075
- var _current_subscription, _ref;
1076
- var _current_subscription1;
1077
- next.subscription = _extends({}, (_current_subscription = current.subscription) != null ? _current_subscription : {}, {
1078
- interval: value === 'both' ? (_ref = (_current_subscription1 = current.subscription) == null ? void 0 : _current_subscription1.interval) != null ? _ref : 'month' : value
1079
- });
1080
- }
1081
- if (value === 'both') next.subscriptionOptional = true;
1082
- else delete next.subscriptionOptional;
1083
- // setDraft directly: update() spreads the patch over
1084
- // `current`, which would resurrect the deleted keys.
1085
- setDraft(next);
1086
- },
1087
- size: "small",
1088
- select: true,
1089
- sx: {
1090
- minWidth: 180
1091
- },
1092
- helperText: subsLocked ? `Subscriptions are on ${(_subs_planLabel2 = subs.planLabel) != null ? _subs_planLabel2 : 'a higher plan'}` : 'Subscriptions bill until canceled',
1093
- children: [
1094
- /*#__PURE__*/ _jsx(MenuItem, {
1095
- value: "once",
1096
- children: 'One-time purchase'
1097
- }),
1098
- /*#__PURE__*/ _jsx(MenuItem, {
1099
- value: "month",
1100
- disabled: subsLocked,
1101
- children: 'Monthly subscription'
1102
- }),
1103
- /*#__PURE__*/ _jsx(MenuItem, {
1104
- value: "year",
1105
- disabled: subsLocked,
1106
- children: 'Yearly subscription'
1107
- }),
1108
- /*#__PURE__*/ _jsx(MenuItem, {
1109
- value: "both",
1110
- disabled: subsLocked,
1111
- children: 'Both — buyer chooses'
1112
- })
1113
- ]
1114
- }),
1115
- current.subscription && current.subscriptionOptional ? /*#__PURE__*/ _jsxs(TextField, {
1116
- label: "Interval",
1117
- value: current.subscription.interval,
1118
- onChange: (event)=>update({
1119
- subscription: _extends({}, current.subscription, {
1120
- interval: event.target.value
1121
- })
1122
- }),
1123
- size: "small",
1124
- select: true,
1125
- sx: {
1126
- minWidth: 120
1127
- },
1128
- children: [
1129
- /*#__PURE__*/ _jsx(MenuItem, {
1130
- value: "month",
1131
- children: 'Monthly'
1132
- }),
1133
- /*#__PURE__*/ _jsx(MenuItem, {
1134
- value: "year",
1135
- children: 'Yearly'
1136
- })
1137
- ]
1138
- }) : null,
1139
- current.subscription ? /*#__PURE__*/ _jsx(TextField, {
1140
- label: "Free trial (days)",
1141
- placeholder: "None",
1142
- value: (_current_subscription_trialDays = current.subscription.trialDays) != null ? _current_subscription_trialDays : '',
1143
- onChange: (event)=>{
1144
- const raw = event.target.value.trim();
1145
- update({
1146
- subscription: _extends({}, current.subscription, raw === '' ? {
1147
- trialDays: undefined
1148
- } : {
1149
- trialDays: Math.max(1, Math.round(Number(raw)))
1150
- })
1151
- });
1152
- },
1153
- size: "small",
1154
- sx: {
1155
- width: 140
1156
- },
1157
- slotProps: {
1158
- htmlInput: {
1159
- inputMode: 'numeric'
1160
- }
1161
- }
1162
- }) : null
1163
- ]
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
1164
1672
  }),
1165
- current.type === 'digital' ? /*#__PURE__*/ _jsxs(_Fragment, {
1166
- children: [
1167
- /*#__PURE__*/ _jsx(Divider, {
1168
- textAlign: "left",
1169
- children: 'Digital delivery'
1170
- }),
1171
- ((_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []).map((file, index)=>{
1172
- var _file_version;
1173
- return /*#__PURE__*/ _jsxs(Stack, {
1174
- direction: "row",
1175
- spacing: 1,
1176
- sx: {
1177
- alignItems: 'center'
1178
- },
1179
- children: [
1180
- /*#__PURE__*/ _jsxs(Typography, {
1181
- variant: "body2",
1182
- sx: {
1183
- flex: 1
1184
- },
1185
- noWrap: true,
1186
- children: [
1187
- file.fileName,
1188
- file.version ? ` · v${file.version}` : ''
1189
- ]
1190
- }),
1191
- /*#__PURE__*/ _jsx(PaidMediaProtection, {
1192
- url: file.url,
1193
- hostId: hostId,
1194
- productId: product == null ? void 0 : product.$id,
1195
- kind: "download"
1196
- }),
1197
- /*#__PURE__*/ _jsx(TextField, {
1198
- label: "Version",
1199
- value: (_file_version = file.version) != null ? _file_version : '',
1200
- onChange: (event)=>{
1201
- var _current_digitalFiles;
1202
- const digitalFiles = [
1203
- ...(_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []
1204
- ];
1205
- digitalFiles[index] = _extends({}, file, {
1206
- version: event.target.value.slice(0, 20)
1207
- });
1208
- update({
1209
- digitalFiles
1210
- });
1211
- },
1212
- size: "small",
1213
- sx: {
1214
- width: 100
1215
- }
1216
- }),
1217
- /*#__PURE__*/ _jsx(Button, {
1218
- size: "small",
1219
- color: "error",
1220
- onClick: ()=>{
1221
- var _current_digitalFiles;
1222
- return update({
1223
- digitalFiles: ((_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : []).filter((_item, itemIndex)=>itemIndex !== index)
1224
- });
1225
- },
1226
- children: '✕'
1227
- })
1228
- ]
1229
- }, index);
1230
- }),
1231
- /*#__PURE__*/ _jsxs(Stack, {
1232
- direction: "row",
1233
- spacing: 2,
1234
- children: [
1235
- /*#__PURE__*/ _jsx(PaidDownloadAddButton, {
1236
- hostId: hostId,
1237
- productId: product == null ? void 0 : product.$id,
1238
- onAdd: (file)=>{
1239
- var _current_digitalFiles;
1240
- return update({
1241
- digitalFiles: [
1242
- ...(_current_digitalFiles = current.digitalFiles) != null ? _current_digitalFiles : [],
1243
- file
1244
- ]
1245
- });
1246
- }
1247
- }),
1248
- /*#__PURE__*/ _jsx(TextField, {
1249
- label: "Download limit",
1250
- placeholder: "Unlimited",
1251
- value: (_current_downloadLimit = current.downloadLimit) != null ? _current_downloadLimit : '',
1252
- onChange: (event)=>{
1253
- const raw = event.target.value.trim();
1254
- update({
1255
- downloadLimit: raw === '' ? undefined : Math.max(1, Math.round(Number(raw)))
1256
- });
1257
- },
1258
- size: "small",
1259
- sx: {
1260
- width: 140
1261
- },
1262
- slotProps: {
1263
- htmlInput: {
1264
- inputMode: 'numeric'
1265
- }
1266
- },
1267
- helperText: "Attempts per order"
1268
- })
1269
- ]
1270
- }),
1271
- /*#__PURE__*/ _jsx(Typography, {
1272
- variant: "caption",
1273
- color: "text.secondary",
1274
- children: 'Buyers always download the current files — uploading a new ' + 'version re-delivers to everyone.'
1275
- }),
1276
- /*#__PURE__*/ _jsx(MembersVideosField, {
1277
- hostId: hostId,
1278
- productId: product == null ? void 0 : product.$id,
1279
- videos: (_current_gatedVideos = current.gatedVideos) != null ? _current_gatedVideos : [],
1280
- onChange: (gatedVideos)=>update({
1281
- gatedVideos
1282
- })
1283
- })
1284
- ]
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
1285
1680
  }) : null,
1286
- /*#__PURE__*/ _jsx(Autocomplete, {
1287
- multiple: true,
1288
- options: relatedProducts.rows.filter((item)=>!item.deletedAt && item.$id !== (product == null ? void 0 : product.$id)),
1289
- getOptionLabel: (item)=>{
1290
- var _item_name;
1291
- return (_item_name = item.name) != null ? _item_name : item.$id;
1292
- },
1293
- isOptionEqualToValue: (option, value)=>option.$id === value.$id,
1294
- value: relatedProducts.rows.filter((item)=>{
1295
- var _current_relatedProductIds;
1296
- return ((_current_relatedProductIds = current.relatedProductIds) != null ? _current_relatedProductIds : []).includes(item.$id);
1297
- }),
1298
- onChange: (_event, picked)=>update({
1299
- relatedProductIds: picked.map((item)=>item.$id)
1300
- }),
1301
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
1302
- label: "Related products (upsells)",
1303
- size: "small",
1304
- helperText: "Shown by the Related products block; blank falls back to frequently-bought-together"
1305
- }))
1306
- }),
1307
- /*#__PURE__*/ _jsx(Divider, {
1308
- textAlign: "left",
1309
- children: 'Search engine listing'
1681
+ /*#__PURE__*/ _jsx(ProductChipsField, {
1682
+ label: "Related products (upsells)",
1683
+ helperText: "Shown by the Related products block; blank falls back to frequently-bought-together",
1684
+ options: relatedOptions,
1685
+ getOptionLabel: recordLabel,
1686
+ isOptionEqualToValue: sameRecord,
1687
+ value: pickedRelated,
1688
+ onChange: setRelatedProducts
1310
1689
  }),
1311
- /*#__PURE__*/ _jsxs(Stack, {
1312
- direction: {
1313
- xs: 'column',
1314
- sm: 'row'
1315
- },
1316
- spacing: 2,
1317
- children: [
1318
- /*#__PURE__*/ _jsx(TextField, {
1319
- label: "SEO title",
1320
- value: (_ref5 = (_current_seo2 = current.seo) == null ? void 0 : _current_seo2.title) != null ? _ref5 : '',
1321
- onChange: (event)=>update({
1322
- seo: _extends({}, current.seo, {
1323
- title: event.target.value
1324
- })
1325
- }),
1326
- // The counts every search listing editor prints (AGL-2910): what a
1327
- // search result shows before it truncates.
1328
- helperText: seoListingFieldCount('title', (_current_seo3 = current.seo) == null ? void 0 : _current_seo3.title),
1329
- error: seoListingFieldTooLong('title', (_current_seo4 = current.seo) == null ? void 0 : _current_seo4.title),
1330
- size: "small",
1331
- fullWidth: true
1332
- }),
1333
- /*#__PURE__*/ _jsx(Button, {
1334
- size: "small",
1335
- onClick: ()=>void pick((media)=>update({
1336
- seo: _extends({}, current.seo, {
1337
- imageUrl: media.url
1338
- })
1339
- })),
1340
- children: ((_current_seo5 = current.seo) == null ? void 0 : _current_seo5.imageUrl) ? 'Change OG image' : 'OG image'
1341
- })
1342
- ]
1343
- }),
1344
- /*#__PURE__*/ _jsx(TextField, {
1345
- label: "SEO description",
1346
- value: (_ref6 = (_current_seo6 = current.seo) == null ? void 0 : _current_seo6.description) != null ? _ref6 : '',
1347
- onChange: (event)=>update({
1348
- seo: _extends({}, current.seo, {
1349
- description: event.target.value
1350
- })
1351
- }),
1352
- helperText: seoListingFieldCount('description', (_current_seo7 = current.seo) == null ? void 0 : _current_seo7.description),
1353
- error: seoListingFieldTooLong('description', (_current_seo8 = current.seo) == null ? void 0 : _current_seo8.description),
1354
- size: "small",
1355
- multiline: true,
1356
- 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
1357
1696
  }),
1358
- WidgetSlot ? /*#__PURE__*/ _jsx(WidgetSlot, {
1697
+ WidgetSlot ? /*#__PURE__*/ _jsx(HostedZone, {
1698
+ renderer: WidgetSlot,
1359
1699
  slot: "seoFields",
1360
1700
  hostId: hostId,
1361
1701
  orgId: undefined,
1362
1702
  orgSlug: "",
1363
- subject: {
1364
- kind: 'product',
1365
- id: (_ref7 = product == null ? void 0 : product.$id) != null ? _ref7 : null,
1366
- name: current.name,
1367
- description: String((_current_description2 = current.description) != null ? _current_description2 : '')
1368
- },
1703
+ subject: seoSubject,
1369
1704
  fields: PRODUCT_SEO_LISTING_FIELDS,
1370
- values: {
1371
- title: (_ref8 = (_current_seo9 = current.seo) == null ? void 0 : _current_seo9.title) != null ? _ref8 : '',
1372
- description: (_ref9 = (_current_seo10 = current.seo) == null ? void 0 : _current_seo10.description) != null ? _ref9 : ''
1373
- },
1374
- 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),
1375
1707
  proposeValues: proposeSeoValues
1376
1708
  }) : null,
1377
1709
  error ? /*#__PURE__*/ _jsx(Alert, {