@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
@@ -24,7 +24,7 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
24
  import { Timestamp } from "@aglyn/shared-util-timestamp";
25
25
  import { Alert, Autocomplete, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
26
26
  import { collection, deleteDoc, doc, setDoc } from "firebase/firestore";
27
- import { memo, useCallback, useMemo, useState } from "react";
27
+ import { memo, useCallback, useMemo, useRef, useState } from "react";
28
28
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
29
29
  import { ceilingedWindow, collectionCeiling, useFirestoreCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
30
30
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
@@ -119,13 +119,571 @@ const sameProduct = (option, value)=>option.$id === value.$id;
119
119
  }))
120
120
  });
121
121
  });
122
+ /**
123
+ * A text field that redraws only when what it shows changes (AGL-3423).
124
+ *
125
+ * Both editors keep their draft in this card, so a keystroke in either one
126
+ * re-renders the card, and drawn whole that is both lists with a match count
127
+ * per collection, their pagers, and every field of the dialog typed into.
128
+ * When a keystroke costs that much, typed input queues and is processed back
129
+ * to back, which is what lets a chip list's pending update climb to #185 (see
130
+ * `CollectionProductsField`). The lists and each part of the dialogs are
131
+ * memoized on what they show, and each field is this, handed a handler and
132
+ * styles that keep their identity, so the keystroke redraws the field it
133
+ * lands in.
134
+ */ const MemoTextField = /*#__PURE__*/ memo(TextField);
135
+ const FIRST_FIELD = {
136
+ mt: 1
137
+ };
138
+ const RULE_FIELD = {
139
+ width: 130
140
+ };
141
+ const RULE_OP = {
142
+ width: 110
143
+ };
144
+ const RULE_VALUE = {
145
+ flex: 1
146
+ };
147
+ const MODE_MENU = [
148
+ /*#__PURE__*/ _jsx(MenuItem, {
149
+ value: "manual",
150
+ children: 'Manual — pick products'
151
+ }, "manual"),
152
+ /*#__PURE__*/ _jsx(MenuItem, {
153
+ value: "smart",
154
+ children: 'Smart — rule based'
155
+ }, "smart")
156
+ ];
157
+ const RULE_FIELD_MENU = RULE_FIELDS.map((field)=>/*#__PURE__*/ _jsx(MenuItem, {
158
+ value: field.value,
159
+ children: field.label
160
+ }, field.value));
161
+ const RULE_OP_MENU = RULE_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
162
+ value: op.value,
163
+ children: op.label
164
+ }, op.value));
165
+ /** The category tree, a page at a time, and the button that adds one. */ const CategoriesSection = /*#__PURE__*/ memo(function CategoriesSection(props) {
166
+ const { categories, visible, onEdit, onDelete } = props;
167
+ return /*#__PURE__*/ _jsxs(_Fragment, {
168
+ children: [
169
+ /*#__PURE__*/ _jsx(Typography, {
170
+ variant: "subtitle2",
171
+ children: 'Categories'
172
+ }),
173
+ categories.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
174
+ variant: "body2",
175
+ color: "text.secondary",
176
+ children: 'Group products into a browsable tree (e.g. Brakes → Pads).'
177
+ }) : visible.map((category)=>/*#__PURE__*/ _jsxs(Stack, {
178
+ direction: "row",
179
+ spacing: 1,
180
+ sx: {
181
+ alignItems: 'center',
182
+ pl: category.depth * 2
183
+ },
184
+ children: [
185
+ /*#__PURE__*/ _jsxs(Typography, {
186
+ variant: "body2",
187
+ sx: {
188
+ flex: 1
189
+ },
190
+ noWrap: true,
191
+ children: [
192
+ category.name,
193
+ /*#__PURE__*/ _jsx(Typography, {
194
+ component: "span",
195
+ variant: "caption",
196
+ color: "text.secondary",
197
+ children: ` /${category.slug}`
198
+ })
199
+ ]
200
+ }),
201
+ /*#__PURE__*/ _jsx(Button, {
202
+ size: "small",
203
+ onClick: ()=>{
204
+ var _category_parentId;
205
+ return onEdit({
206
+ id: category.$id,
207
+ name: category.name,
208
+ parentId: (_category_parentId = category.parentId) != null ? _category_parentId : ''
209
+ });
210
+ },
211
+ children: 'Edit'
212
+ }),
213
+ /*#__PURE__*/ _jsx(Button, {
214
+ size: "small",
215
+ color: "error",
216
+ onClick: onDelete(category),
217
+ children: 'Delete'
218
+ })
219
+ ]
220
+ }, category.$id)),
221
+ categories.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
222
+ page: props.page,
223
+ pageSize: props.pageSize,
224
+ rowCount: visible.length,
225
+ // The categories the card HOLDS — a client slice of rows already
226
+ // read, so the total is known exactly.
227
+ count: categories.length,
228
+ onPageChange: props.onPageChange,
229
+ onPageSizeChange: props.onPageSizeChange
230
+ }),
231
+ props.truncated ? /*#__PURE__*/ _jsx(Alert, {
232
+ severity: "info",
233
+ children: `Showing ${CATEGORY_CEILING} categories, ordered by id. This ` + 'catalog has more — a category whose parent is past that ' + 'boundary is drawn at the top level here rather than under it.'
234
+ }) : null,
235
+ /*#__PURE__*/ _jsx(Button, {
236
+ size: "small",
237
+ sx: {
238
+ alignSelf: 'flex-start'
239
+ },
240
+ onClick: ()=>onEdit({
241
+ id: null,
242
+ name: '',
243
+ parentId: ''
244
+ }),
245
+ children: 'Add category'
246
+ })
247
+ ]
248
+ });
249
+ });
250
+ /** The catalog collections, a page at a time, and the button that adds one. */ const CollectionsSection = /*#__PURE__*/ memo(function CollectionsSection(props) {
251
+ const { collections, visible, products, countPrefix, onEdit, onDelete } = props;
252
+ /**
253
+ * How many products a collection holds — over the CATALOG WINDOW.
254
+ *
255
+ * Rendered with "at least" when the probe found a product past the ceiling,
256
+ * because past that point this is a lower bound and printing it as a total
257
+ * is the count-that-is-a-window-length defect. The number itself is honest
258
+ * either way; only the claim around it changes.
259
+ */ const collectionCount = (row)=>products.filter((product)=>CommerceModel.matchesCollection(product, row, product.$id)).length;
260
+ return /*#__PURE__*/ _jsxs(_Fragment, {
261
+ children: [
262
+ /*#__PURE__*/ _jsx(Typography, {
263
+ variant: "subtitle2",
264
+ children: 'Collections'
265
+ }),
266
+ visible.map((row)=>{
267
+ var _row_mode;
268
+ return /*#__PURE__*/ _jsxs(Stack, {
269
+ direction: "row",
270
+ spacing: 1,
271
+ sx: {
272
+ alignItems: 'center'
273
+ },
274
+ children: [
275
+ /*#__PURE__*/ _jsxs(Typography, {
276
+ variant: "body2",
277
+ sx: {
278
+ flex: 1
279
+ },
280
+ noWrap: true,
281
+ children: [
282
+ row.name,
283
+ /*#__PURE__*/ _jsx(Typography, {
284
+ component: "span",
285
+ variant: "caption",
286
+ color: "text.secondary",
287
+ children: ` · ${(_row_mode = row.mode) != null ? _row_mode : 'manual'} · ${countPrefix}${collectionCount(row)} products`
288
+ })
289
+ ]
290
+ }),
291
+ /*#__PURE__*/ _jsx(Button, {
292
+ size: "small",
293
+ onClick: ()=>{
294
+ var _row_slug, _row_mode, _row_productIds, _row_rules;
295
+ return onEdit({
296
+ id: row.$id,
297
+ name: row.name,
298
+ slug: (_row_slug = row.slug) != null ? _row_slug : '',
299
+ mode: (_row_mode = row.mode) != null ? _row_mode : 'manual',
300
+ productIds: (_row_productIds = row.productIds) != null ? _row_productIds : [],
301
+ rules: (_row_rules = row.rules) != null ? _row_rules : [],
302
+ matchAll: row.matchAll !== false
303
+ });
304
+ },
305
+ children: 'Edit'
306
+ }),
307
+ /*#__PURE__*/ _jsx(Button, {
308
+ size: "small",
309
+ color: "error",
310
+ onClick: onDelete(row),
311
+ children: 'Delete'
312
+ })
313
+ ]
314
+ }, row.$id);
315
+ }),
316
+ collections.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
317
+ page: props.page,
318
+ pageSize: props.pageSize,
319
+ rowCount: visible.length,
320
+ // The CATALOG collections, which is what this list is. Not the
321
+ // window's length: content collections share the subcollection
322
+ // and are classified out before this count is taken.
323
+ count: collections.length,
324
+ onPageChange: props.onPageChange,
325
+ onPageSizeChange: props.onPageSizeChange
326
+ }),
327
+ props.truncated ? /*#__PURE__*/ _jsx(Alert, {
328
+ severity: "info",
329
+ children: `Showing ${COLLECTION_CEILING} collections, ordered by id. This ` + 'site has more — the slug check below only covers the ones ' + 'listed here, so an address may already be taken by one that ' + 'is not.'
330
+ }) : null,
331
+ /*#__PURE__*/ _jsx(Button, {
332
+ size: "small",
333
+ sx: {
334
+ alignSelf: 'flex-start'
335
+ },
336
+ onClick: ()=>onEdit({
337
+ id: null,
338
+ name: '',
339
+ slug: '',
340
+ mode: 'manual',
341
+ productIds: [],
342
+ rules: [],
343
+ matchAll: true
344
+ }),
345
+ children: 'Add collection'
346
+ })
347
+ ]
348
+ });
349
+ });
350
+ /** The category editor: a name and a parent. */ const CategoryDialog = /*#__PURE__*/ memo(function CategoryDialog(props) {
351
+ var _ref, _ref1;
352
+ const { draft, categories, setDraft } = props;
353
+ const draftId = draft == null ? void 0 : draft.id;
354
+ const close = useCallback(()=>setDraft(null), [
355
+ setDraft
356
+ ]);
357
+ const setName = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
358
+ name: event.target.value
359
+ }) : prev), [
360
+ setDraft
361
+ ]);
362
+ const setParent = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
363
+ parentId: event.target.value
364
+ }) : prev), [
365
+ setDraft
366
+ ]);
367
+ const parentMenu = useMemo(()=>[
368
+ /*#__PURE__*/ _jsx(MenuItem, {
369
+ value: "",
370
+ children: 'None (top level)'
371
+ }, ""),
372
+ // The whole set, not `visibleCategories` — a parent picker that
373
+ // offered only the current page could not reparent a category under
374
+ // one two pages away.
375
+ ...categories.filter((category)=>category.$id !== draftId).map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
376
+ value: category.$id,
377
+ children: `${'— '.repeat(category.depth)}${category.name}`
378
+ }, category.$id))
379
+ ], [
380
+ categories,
381
+ draftId
382
+ ]);
383
+ return /*#__PURE__*/ _jsxs(Dialog, {
384
+ open: Boolean(draft),
385
+ onClose: close,
386
+ maxWidth: "xs",
387
+ fullWidth: true,
388
+ children: [
389
+ /*#__PURE__*/ _jsx(DialogTitle, {
390
+ children: (draft == null ? void 0 : draft.id) ? 'Edit category' : 'New category'
391
+ }),
392
+ /*#__PURE__*/ _jsxs(DialogContent, {
393
+ sx: {
394
+ display: 'flex',
395
+ flexDirection: 'column',
396
+ gap: 2
397
+ },
398
+ children: [
399
+ /*#__PURE__*/ _jsx(MemoTextField, {
400
+ label: "Name",
401
+ value: (_ref = draft == null ? void 0 : draft.name) != null ? _ref : '',
402
+ onChange: setName,
403
+ size: "small",
404
+ autoFocus: true,
405
+ sx: FIRST_FIELD
406
+ }),
407
+ /*#__PURE__*/ _jsx(MemoTextField, {
408
+ label: "Parent category",
409
+ value: (_ref1 = draft == null ? void 0 : draft.parentId) != null ? _ref1 : '',
410
+ onChange: setParent,
411
+ size: "small",
412
+ select: true,
413
+ children: parentMenu
414
+ })
415
+ ]
416
+ }),
417
+ /*#__PURE__*/ _jsxs(DialogActions, {
418
+ children: [
419
+ /*#__PURE__*/ _jsx(Button, {
420
+ onClick: close,
421
+ children: 'Cancel'
422
+ }),
423
+ /*#__PURE__*/ _jsx(Button, {
424
+ variant: "contained",
425
+ color: "primary",
426
+ disabled: !(draft == null ? void 0 : draft.name.trim()),
427
+ onClick: props.onSave,
428
+ children: 'Save'
429
+ })
430
+ ]
431
+ })
432
+ ]
433
+ });
434
+ });
435
+ const CollectionRuleRow = /*#__PURE__*/ memo(function CollectionRuleRow(props) {
436
+ const { index, rule, onChange } = props;
437
+ const setField = useCallback((event)=>onChange(index, {
438
+ field: event.target.value
439
+ }), [
440
+ index,
441
+ onChange
442
+ ]);
443
+ const setOp = useCallback((event)=>onChange(index, {
444
+ op: event.target.value
445
+ }), [
446
+ index,
447
+ onChange
448
+ ]);
449
+ const setCategory = useCallback((event)=>onChange(index, {
450
+ value: event.target.value
451
+ }), [
452
+ index,
453
+ onChange
454
+ ]);
455
+ const ruleField = rule.field;
456
+ const setValue = useCallback((event)=>onChange(index, {
457
+ value: ruleField === 'priceUsd' ? Number(event.target.value) : event.target.value
458
+ }), [
459
+ index,
460
+ onChange,
461
+ ruleField
462
+ ]);
463
+ return /*#__PURE__*/ _jsxs(Stack, {
464
+ direction: "row",
465
+ spacing: 1,
466
+ children: [
467
+ /*#__PURE__*/ _jsx(MemoTextField, {
468
+ value: rule.field,
469
+ onChange: setField,
470
+ size: "small",
471
+ select: true,
472
+ sx: RULE_FIELD,
473
+ children: RULE_FIELD_MENU
474
+ }),
475
+ /*#__PURE__*/ _jsx(MemoTextField, {
476
+ value: rule.op,
477
+ onChange: setOp,
478
+ size: "small",
479
+ select: true,
480
+ sx: RULE_OP,
481
+ children: RULE_OP_MENU
482
+ }),
483
+ rule.field === 'categoryId' ? /*#__PURE__*/ _jsx(MemoTextField, {
484
+ value: rule.value,
485
+ onChange: setCategory,
486
+ size: "small",
487
+ select: true,
488
+ sx: RULE_VALUE,
489
+ children: props.categoryMenu
490
+ }) : /*#__PURE__*/ _jsx(MemoTextField, {
491
+ value: rule.value,
492
+ onChange: setValue,
493
+ size: "small",
494
+ sx: RULE_VALUE,
495
+ placeholder: rule.field === 'type' ? 'physical' : 'Value'
496
+ }),
497
+ /*#__PURE__*/ _jsx(Button, {
498
+ size: "small",
499
+ color: "error",
500
+ onClick: ()=>onChange(index, null),
501
+ children: '✕'
502
+ })
503
+ ]
504
+ });
505
+ });
506
+ /** Whether every rule must hold, or any one. */ const CollectionMatchSwitch = /*#__PURE__*/ memo(function CollectionMatchSwitch(props) {
507
+ const { matchAll, setDraft } = props;
508
+ return /*#__PURE__*/ _jsxs(Stack, {
509
+ direction: "row",
510
+ spacing: 1,
511
+ sx: {
512
+ alignItems: 'center'
513
+ },
514
+ children: [
515
+ /*#__PURE__*/ _jsx(Typography, {
516
+ variant: "body2",
517
+ children: 'Match'
518
+ }),
519
+ /*#__PURE__*/ _jsx(Switch, {
520
+ size: "small",
521
+ checked: matchAll,
522
+ onChange: (event)=>setDraft((prev)=>prev ? _extends({}, prev, {
523
+ matchAll: event.target.checked
524
+ }) : prev)
525
+ }),
526
+ /*#__PURE__*/ _jsx(Typography, {
527
+ variant: "body2",
528
+ children: matchAll ? 'all rules' : 'any rule'
529
+ })
530
+ ]
531
+ });
532
+ });
533
+ /** A smart collection's rules, whether all or any must hold, and the button that adds one. */ const CollectionRulesFields = /*#__PURE__*/ memo(function CollectionRulesFields(props) {
534
+ const { rules, matchAll, categories, setDraft, onRule } = props;
535
+ const categoryMenu = useMemo(()=>categories.map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
536
+ value: category.$id,
537
+ children: category.name
538
+ }, category.$id)), [
539
+ categories
540
+ ]);
541
+ return /*#__PURE__*/ _jsxs(_Fragment, {
542
+ children: [
543
+ /*#__PURE__*/ _jsx(CollectionMatchSwitch, {
544
+ matchAll: matchAll,
545
+ setDraft: setDraft
546
+ }),
547
+ rules.map((rule, index)=>/*#__PURE__*/ _jsx(CollectionRuleRow, {
548
+ index: index,
549
+ rule: rule,
550
+ categoryMenu: categoryMenu,
551
+ onChange: onRule
552
+ }, index)),
553
+ /*#__PURE__*/ _jsx(Button, {
554
+ size: "small",
555
+ sx: {
556
+ alignSelf: 'flex-start'
557
+ },
558
+ onClick: ()=>onRule(rules.length, {}),
559
+ children: 'Add rule'
560
+ })
561
+ ]
562
+ });
563
+ });
564
+ /** The products a draft collection holds, the first few by name. */ const CollectionPreview = /*#__PURE__*/ memo(function CollectionPreview(props) {
565
+ const { matches, countPrefix } = props;
566
+ return /*#__PURE__*/ _jsxs(Stack, {
567
+ direction: "row",
568
+ spacing: 0.5,
569
+ sx: {
570
+ flexWrap: 'wrap'
571
+ },
572
+ children: [
573
+ /*#__PURE__*/ _jsx(Typography, {
574
+ variant: "caption",
575
+ color: "text.secondary",
576
+ children: `Matches ${countPrefix}${matches.length} products: `
577
+ }),
578
+ matches.slice(0, 6).map((product)=>/*#__PURE__*/ _jsx(Chip, {
579
+ label: product.name,
580
+ size: "small"
581
+ }, product.$id)),
582
+ matches.length > 6 ? /*#__PURE__*/ _jsx(Typography, {
583
+ variant: "caption",
584
+ color: "text.secondary",
585
+ children: `+${matches.length - 6} more`
586
+ }) : null
587
+ ]
588
+ });
589
+ });
590
+ /** The collection editor: a name, a mode, and the products or rules it holds. */ const CollectionDialog = /*#__PURE__*/ memo(function CollectionDialog(props) {
591
+ var _ref, _ref1, _ref2;
592
+ const { draft, error, setDraft } = props;
593
+ const close = useCallback(()=>setDraft(null), [
594
+ setDraft
595
+ ]);
596
+ const setName = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
597
+ name: event.target.value
598
+ }) : prev), [
599
+ setDraft
600
+ ]);
601
+ const setMode = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
602
+ mode: event.target.value
603
+ }) : prev), [
604
+ setDraft
605
+ ]);
606
+ return /*#__PURE__*/ _jsxs(Dialog, {
607
+ open: Boolean(draft),
608
+ onClose: close,
609
+ maxWidth: "sm",
610
+ fullWidth: true,
611
+ children: [
612
+ /*#__PURE__*/ _jsx(DialogTitle, {
613
+ children: (draft == null ? void 0 : draft.id) ? 'Edit collection' : 'New collection'
614
+ }),
615
+ /*#__PURE__*/ _jsxs(DialogContent, {
616
+ sx: {
617
+ display: 'flex',
618
+ flexDirection: 'column',
619
+ gap: 2
620
+ },
621
+ children: [
622
+ /*#__PURE__*/ _jsx(MemoTextField, {
623
+ label: "Name",
624
+ value: (_ref = draft == null ? void 0 : draft.name) != null ? _ref : '',
625
+ onChange: setName,
626
+ size: "small",
627
+ autoFocus: true,
628
+ sx: FIRST_FIELD,
629
+ error: props.slugTaken,
630
+ helperText: (draft == null ? void 0 : draft.name) ? `/collections/${props.slug}` : undefined
631
+ }),
632
+ /*#__PURE__*/ _jsx(MemoTextField, {
633
+ label: "Mode",
634
+ value: (_ref1 = draft == null ? void 0 : draft.mode) != null ? _ref1 : 'manual',
635
+ onChange: setMode,
636
+ size: "small",
637
+ select: true,
638
+ children: MODE_MENU
639
+ }),
640
+ (draft == null ? void 0 : draft.mode) === 'manual' ? /*#__PURE__*/ _jsx(CollectionProductsField, {
641
+ products: props.products,
642
+ value: props.picked,
643
+ onChange: props.onProducts
644
+ }) : /*#__PURE__*/ _jsx(CollectionRulesFields, {
645
+ rules: (_ref2 = draft == null ? void 0 : draft.rules) != null ? _ref2 : NO_RULES,
646
+ matchAll: (draft == null ? void 0 : draft.matchAll) !== false,
647
+ categories: props.categories,
648
+ setDraft: setDraft,
649
+ onRule: props.onRule
650
+ }),
651
+ error ? /*#__PURE__*/ _jsx(Alert, {
652
+ severity: "warning",
653
+ children: error
654
+ }) : null,
655
+ draft ? /*#__PURE__*/ _jsx(CollectionPreview, {
656
+ matches: props.matches,
657
+ countPrefix: props.countPrefix
658
+ }) : null
659
+ ]
660
+ }),
661
+ /*#__PURE__*/ _jsxs(DialogActions, {
662
+ children: [
663
+ /*#__PURE__*/ _jsx(Button, {
664
+ onClick: close,
665
+ children: 'Cancel'
666
+ }),
667
+ /*#__PURE__*/ _jsx(Button, {
668
+ variant: "contained",
669
+ color: "primary",
670
+ disabled: !(draft == null ? void 0 : draft.name.trim()) || Boolean(error),
671
+ onClick: props.onSave,
672
+ children: 'Save'
673
+ })
674
+ ]
675
+ })
676
+ ]
677
+ });
678
+ });
679
+ /** A collection with no rules yet, the same list on every render. */ const NO_RULES = [];
122
680
  /**
123
681
  * Categories & collections manager (AGL-280): category tree (parentId)
124
682
  * at `hosts/{hostId}/productCategories`, manual + smart collections at
125
683
  * `hosts/{hostId}/collections` with a live matched-product preview from
126
684
  * the same `matchesCollection` matcher the storefront uses.
127
685
  */ export function CatalogOrganizationCard(props) {
128
- var _CommerceModel_validateCollection, _ref, _ref1, _ref2, _ref3, _ref4;
686
+ var _CommerceModel_validateCollection;
129
687
  const { hostId } = props;
130
688
  const firestore = useFirestore();
131
689
  const { enqueueSnackbar } = useSnackbar();
@@ -459,14 +1017,27 @@ const sameProduct = (option, value)=>option.$id === value.$id;
459
1017
  const setCollectionProducts = useCallback((picked)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
460
1018
  productIds: picked.map((item)=>item.$id)
461
1019
  }) : prev), []);
1020
+ /*
1021
+ * Recomputed when what decides membership changes — the mode, the picked
1022
+ * products, the rules and whether all must hold — and not on a keystroke in
1023
+ * the name, which no match reads (AGL-3423).
1024
+ */ const draftMode = collectionDraft == null ? void 0 : collectionDraft.mode;
1025
+ const draftRules = collectionDraft == null ? void 0 : collectionDraft.rules;
1026
+ const draftMatchAll = collectionDraft == null ? void 0 : collectionDraft.matchAll;
462
1027
  const previewMatches = useMemo(()=>{
463
- if (!collectionDraft) return [];
464
- const candidate = _extends({}, collectionDraft, {
465
- slug: collectionDraft.slug || CommerceModel.commerceSlug(collectionDraft.name)
466
- });
1028
+ if (!draftMode) return [];
1029
+ const candidate = {
1030
+ mode: draftMode,
1031
+ productIds: collectionProductIds,
1032
+ rules: draftRules,
1033
+ matchAll: draftMatchAll
1034
+ };
467
1035
  return products.filter((product)=>CommerceModel.matchesCollection(product, candidate, product.$id));
468
1036
  }, [
469
- collectionDraft,
1037
+ draftMode,
1038
+ collectionProductIds,
1039
+ draftRules,
1040
+ draftMatchAll,
470
1041
  products
471
1042
  ]);
472
1043
  /**
@@ -660,14 +1231,6 @@ const sameProduct = (option, value)=>option.$id === value.$id;
660
1231
  enqueueSnackbar,
661
1232
  restampMembership
662
1233
  ]);
663
- /**
664
- * How many products a collection holds — over the CATALOG WINDOW.
665
- *
666
- * Rendered with "at least" when the probe found a product past the ceiling,
667
- * because past that point this is a lower bound and printing it as a total
668
- * is the count-that-is-a-window-length defect. The number itself is honest
669
- * either way; only the claim around it changes.
670
- */ const collectionCount = (row)=>products.filter((product)=>CommerceModel.matchesCollection(product, row, product.$id)).length;
671
1234
  /*
672
1235
  * "at least", or nothing at all.
673
1236
  *
@@ -680,11 +1243,18 @@ const sameProduct = (option, value)=>option.$id === value.$id;
680
1243
  * because a catalog of exactly the ceiling is complete: nothing is missing,
681
1244
  * and a comparison would put "at least" on a number that is exact.
682
1245
  */ const countPrefix = productsTruncated ? 'at least ' : '';
683
- const updateRule = (index, patch)=>{
684
- var _collectionDraft_rules;
685
- if (!collectionDraft) return;
1246
+ /*
1247
+ * A rule's change, written over the collection draft as last rendered, from
1248
+ * a handler that keeps its identity so a rule row redraws only when its own
1249
+ * rule changes (AGL-3423).
1250
+ */ const latestCollectionDraft = useRef(collectionDraft);
1251
+ latestCollectionDraft.current = collectionDraft;
1252
+ const updateRule = useCallback((index, patch)=>{
1253
+ var _draft_rules;
1254
+ const draft = latestCollectionDraft.current;
1255
+ if (!draft) return;
686
1256
  const rules = [
687
- ...(_collectionDraft_rules = collectionDraft.rules) != null ? _collectionDraft_rules : []
1257
+ ...(_draft_rules = draft.rules) != null ? _draft_rules : []
688
1258
  ];
689
1259
  if (patch === null) rules.splice(index, 1);
690
1260
  else rules[index] = _extends({
@@ -692,10 +1262,10 @@ const sameProduct = (option, value)=>option.$id === value.$id;
692
1262
  op: 'eq',
693
1263
  value: ''
694
1264
  }, rules[index], patch);
695
- setCollectionDraft(_extends({}, collectionDraft, {
1265
+ setCollectionDraft(_extends({}, draft, {
696
1266
  rules
697
1267
  }));
698
- };
1268
+ }, []);
699
1269
  return /*#__PURE__*/ _jsxs(CardDisplay, {
700
1270
  header: 'Categories & collections',
701
1271
  help: pluginDocsHelp('catalog', {
@@ -707,447 +1277,57 @@ const sameProduct = (option, value)=>option.$id === value.$id;
707
1277
  /*#__PURE__*/ _jsxs(Stack, {
708
1278
  spacing: 1,
709
1279
  children: [
710
- /*#__PURE__*/ _jsx(Typography, {
711
- variant: "subtitle2",
712
- children: 'Categories'
713
- }),
714
- categories.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
715
- variant: "body2",
716
- color: "text.secondary",
717
- children: 'Group products into a browsable tree (e.g. Brakes → Pads).'
718
- }) : visibleCategories.map((category)=>/*#__PURE__*/ _jsxs(Stack, {
719
- direction: "row",
720
- spacing: 1,
721
- sx: {
722
- alignItems: 'center',
723
- pl: category.depth * 2
724
- },
725
- children: [
726
- /*#__PURE__*/ _jsxs(Typography, {
727
- variant: "body2",
728
- sx: {
729
- flex: 1
730
- },
731
- noWrap: true,
732
- children: [
733
- category.name,
734
- /*#__PURE__*/ _jsx(Typography, {
735
- component: "span",
736
- variant: "caption",
737
- color: "text.secondary",
738
- children: ` /${category.slug}`
739
- })
740
- ]
741
- }),
742
- /*#__PURE__*/ _jsx(Button, {
743
- size: "small",
744
- onClick: ()=>{
745
- var _category_parentId;
746
- return setCategoryDraft({
747
- id: category.$id,
748
- name: category.name,
749
- parentId: (_category_parentId = category.parentId) != null ? _category_parentId : ''
750
- });
751
- },
752
- children: 'Edit'
753
- }),
754
- /*#__PURE__*/ _jsx(Button, {
755
- size: "small",
756
- color: "error",
757
- onClick: handleCategoryDelete(category),
758
- children: 'Delete'
759
- })
760
- ]
761
- }, category.$id)),
762
- categories.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
1280
+ /*#__PURE__*/ _jsx(CategoriesSection, {
1281
+ categories: categories,
1282
+ visible: visibleCategories,
763
1283
  page: categoryPage,
764
1284
  pageSize: categoryPageSize,
765
- rowCount: visibleCategories.length,
766
- // The categories the card HOLDS — a client slice of rows already
767
- // read, so the total is known exactly.
768
- count: categories.length,
1285
+ truncated: categoriesTruncated,
769
1286
  onPageChange: setCategoryPage,
770
- onPageSizeChange: setCategoryPageSize
771
- }),
772
- categoriesTruncated ? /*#__PURE__*/ _jsx(Alert, {
773
- severity: "info",
774
- children: `Showing ${CATEGORY_CEILING} categories, ordered by id. This ` + 'catalog has more — a category whose parent is past that ' + 'boundary is drawn at the top level here rather than under it.'
775
- }) : null,
776
- /*#__PURE__*/ _jsx(Button, {
777
- size: "small",
778
- sx: {
779
- alignSelf: 'flex-start'
780
- },
781
- onClick: ()=>setCategoryDraft({
782
- id: null,
783
- name: '',
784
- parentId: ''
785
- }),
786
- children: 'Add category'
1287
+ onPageSizeChange: setCategoryPageSize,
1288
+ onEdit: setCategoryDraft,
1289
+ onDelete: handleCategoryDelete
787
1290
  }),
788
1291
  /*#__PURE__*/ _jsx(Divider, {
789
1292
  sx: {
790
1293
  my: 1
791
1294
  }
792
1295
  }),
793
- /*#__PURE__*/ _jsx(Typography, {
794
- variant: "subtitle2",
795
- children: 'Collections'
796
- }),
797
- visibleCollections.map((row)=>{
798
- var _row_mode;
799
- return /*#__PURE__*/ _jsxs(Stack, {
800
- direction: "row",
801
- spacing: 1,
802
- sx: {
803
- alignItems: 'center'
804
- },
805
- children: [
806
- /*#__PURE__*/ _jsxs(Typography, {
807
- variant: "body2",
808
- sx: {
809
- flex: 1
810
- },
811
- noWrap: true,
812
- children: [
813
- row.name,
814
- /*#__PURE__*/ _jsx(Typography, {
815
- component: "span",
816
- variant: "caption",
817
- color: "text.secondary",
818
- children: ` · ${(_row_mode = row.mode) != null ? _row_mode : 'manual'} · ${countPrefix}${collectionCount(row)} products`
819
- })
820
- ]
821
- }),
822
- /*#__PURE__*/ _jsx(Button, {
823
- size: "small",
824
- onClick: ()=>{
825
- var _row_slug, _row_mode, _row_productIds, _row_rules;
826
- return setCollectionDraft({
827
- id: row.$id,
828
- name: row.name,
829
- slug: (_row_slug = row.slug) != null ? _row_slug : '',
830
- mode: (_row_mode = row.mode) != null ? _row_mode : 'manual',
831
- productIds: (_row_productIds = row.productIds) != null ? _row_productIds : [],
832
- rules: (_row_rules = row.rules) != null ? _row_rules : [],
833
- matchAll: row.matchAll !== false
834
- });
835
- },
836
- children: 'Edit'
837
- }),
838
- /*#__PURE__*/ _jsx(Button, {
839
- size: "small",
840
- color: "error",
841
- onClick: handleCollectionDelete(row),
842
- children: 'Delete'
843
- })
844
- ]
845
- }, row.$id);
846
- }),
847
- commerceCollections.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
1296
+ /*#__PURE__*/ _jsx(CollectionsSection, {
1297
+ collections: commerceCollections,
1298
+ visible: visibleCollections,
1299
+ products: products,
1300
+ countPrefix: countPrefix,
848
1301
  page: collectionPage,
849
1302
  pageSize: collectionPageSize,
850
- rowCount: visibleCollections.length,
851
- // The CATALOG collections, which is what this list is. Not the
852
- // window's length: content collections share the subcollection
853
- // and are classified out before this count is taken.
854
- count: commerceCollections.length,
1303
+ truncated: collectionsTruncated,
855
1304
  onPageChange: setCollectionPage,
856
- onPageSizeChange: setCollectionPageSize
857
- }),
858
- collectionsTruncated ? /*#__PURE__*/ _jsx(Alert, {
859
- severity: "info",
860
- children: `Showing ${COLLECTION_CEILING} collections, ordered by id. This ` + 'site has more — the slug check below only covers the ones ' + 'listed here, so an address may already be taken by one that ' + 'is not.'
861
- }) : null,
862
- /*#__PURE__*/ _jsx(Button, {
863
- size: "small",
864
- sx: {
865
- alignSelf: 'flex-start'
866
- },
867
- onClick: ()=>setCollectionDraft({
868
- id: null,
869
- name: '',
870
- slug: '',
871
- mode: 'manual',
872
- productIds: [],
873
- rules: [],
874
- matchAll: true
875
- }),
876
- children: 'Add collection'
1305
+ onPageSizeChange: setCollectionPageSize,
1306
+ onEdit: setCollectionDraft,
1307
+ onDelete: handleCollectionDelete
877
1308
  })
878
1309
  ]
879
1310
  }),
880
- /*#__PURE__*/ _jsxs(Dialog, {
881
- open: Boolean(categoryDraft),
882
- onClose: ()=>setCategoryDraft(null),
883
- maxWidth: "xs",
884
- fullWidth: true,
885
- children: [
886
- /*#__PURE__*/ _jsx(DialogTitle, {
887
- children: (categoryDraft == null ? void 0 : categoryDraft.id) ? 'Edit category' : 'New category'
888
- }),
889
- /*#__PURE__*/ _jsxs(DialogContent, {
890
- sx: {
891
- display: 'flex',
892
- flexDirection: 'column',
893
- gap: 2
894
- },
895
- children: [
896
- /*#__PURE__*/ _jsx(TextField, {
897
- label: "Name",
898
- value: (_ref = categoryDraft == null ? void 0 : categoryDraft.name) != null ? _ref : '',
899
- onChange: (event)=>setCategoryDraft((prev)=>prev ? _extends({}, prev, {
900
- name: event.target.value
901
- }) : prev),
902
- size: "small",
903
- autoFocus: true,
904
- sx: {
905
- mt: 1
906
- }
907
- }),
908
- /*#__PURE__*/ _jsxs(TextField, {
909
- label: "Parent category",
910
- value: (_ref1 = categoryDraft == null ? void 0 : categoryDraft.parentId) != null ? _ref1 : '',
911
- onChange: (event)=>setCategoryDraft((prev)=>prev ? _extends({}, prev, {
912
- parentId: event.target.value
913
- }) : prev),
914
- size: "small",
915
- select: true,
916
- children: [
917
- /*#__PURE__*/ _jsx(MenuItem, {
918
- value: "",
919
- children: 'None (top level)'
920
- }),
921
- categories.filter((category)=>category.$id !== (categoryDraft == null ? void 0 : categoryDraft.id)).map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
922
- value: category.$id,
923
- children: `${'— '.repeat(category.depth)}${category.name}`
924
- }, category.$id))
925
- ]
926
- })
927
- ]
928
- }),
929
- /*#__PURE__*/ _jsxs(DialogActions, {
930
- children: [
931
- /*#__PURE__*/ _jsx(Button, {
932
- onClick: ()=>setCategoryDraft(null),
933
- children: 'Cancel'
934
- }),
935
- /*#__PURE__*/ _jsx(Button, {
936
- variant: "contained",
937
- color: "primary",
938
- disabled: !(categoryDraft == null ? void 0 : categoryDraft.name.trim()),
939
- onClick: handleCategorySave,
940
- children: 'Save'
941
- })
942
- ]
943
- })
944
- ]
1311
+ /*#__PURE__*/ _jsx(CategoryDialog, {
1312
+ draft: categoryDraft,
1313
+ categories: categories,
1314
+ setDraft: setCategoryDraft,
1315
+ onSave: handleCategorySave
945
1316
  }),
946
- /*#__PURE__*/ _jsxs(Dialog, {
947
- open: Boolean(collectionDraft),
948
- onClose: ()=>setCollectionDraft(null),
949
- maxWidth: "sm",
950
- fullWidth: true,
951
- children: [
952
- /*#__PURE__*/ _jsx(DialogTitle, {
953
- children: (collectionDraft == null ? void 0 : collectionDraft.id) ? 'Edit collection' : 'New collection'
954
- }),
955
- /*#__PURE__*/ _jsxs(DialogContent, {
956
- sx: {
957
- display: 'flex',
958
- flexDirection: 'column',
959
- gap: 2
960
- },
961
- children: [
962
- /*#__PURE__*/ _jsx(TextField, {
963
- label: "Name",
964
- value: (_ref2 = collectionDraft == null ? void 0 : collectionDraft.name) != null ? _ref2 : '',
965
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
966
- name: event.target.value
967
- }) : prev),
968
- size: "small",
969
- autoFocus: true,
970
- sx: {
971
- mt: 1
972
- },
973
- error: slugTaken,
974
- helperText: (collectionDraft == null ? void 0 : collectionDraft.name) ? `/collections/${draftSlug}` : undefined
975
- }),
976
- /*#__PURE__*/ _jsxs(TextField, {
977
- label: "Mode",
978
- value: (_ref3 = collectionDraft == null ? void 0 : collectionDraft.mode) != null ? _ref3 : 'manual',
979
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
980
- mode: event.target.value
981
- }) : prev),
982
- size: "small",
983
- select: true,
984
- children: [
985
- /*#__PURE__*/ _jsx(MenuItem, {
986
- value: "manual",
987
- children: 'Manual — pick products'
988
- }),
989
- /*#__PURE__*/ _jsx(MenuItem, {
990
- value: "smart",
991
- children: 'Smart — rule based'
992
- })
993
- ]
994
- }),
995
- (collectionDraft == null ? void 0 : collectionDraft.mode) === 'manual' ? /*#__PURE__*/ _jsx(CollectionProductsField, {
996
- products: products,
997
- value: collectionProducts,
998
- onChange: setCollectionProducts
999
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
1000
- children: [
1001
- /*#__PURE__*/ _jsxs(Stack, {
1002
- direction: "row",
1003
- spacing: 1,
1004
- sx: {
1005
- alignItems: 'center'
1006
- },
1007
- children: [
1008
- /*#__PURE__*/ _jsx(Typography, {
1009
- variant: "body2",
1010
- children: 'Match'
1011
- }),
1012
- /*#__PURE__*/ _jsx(Switch, {
1013
- size: "small",
1014
- checked: (collectionDraft == null ? void 0 : collectionDraft.matchAll) !== false,
1015
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
1016
- matchAll: event.target.checked
1017
- }) : prev)
1018
- }),
1019
- /*#__PURE__*/ _jsx(Typography, {
1020
- variant: "body2",
1021
- children: (collectionDraft == null ? void 0 : collectionDraft.matchAll) !== false ? 'all rules' : 'any rule'
1022
- })
1023
- ]
1024
- }),
1025
- ((_ref4 = collectionDraft == null ? void 0 : collectionDraft.rules) != null ? _ref4 : []).map((rule, index)=>/*#__PURE__*/ _jsxs(Stack, {
1026
- direction: "row",
1027
- spacing: 1,
1028
- children: [
1029
- /*#__PURE__*/ _jsx(TextField, {
1030
- value: rule.field,
1031
- onChange: (event)=>updateRule(index, {
1032
- field: event.target.value
1033
- }),
1034
- size: "small",
1035
- select: true,
1036
- sx: {
1037
- width: 130
1038
- },
1039
- children: RULE_FIELDS.map((field)=>/*#__PURE__*/ _jsx(MenuItem, {
1040
- value: field.value,
1041
- children: field.label
1042
- }, field.value))
1043
- }),
1044
- /*#__PURE__*/ _jsx(TextField, {
1045
- value: rule.op,
1046
- onChange: (event)=>updateRule(index, {
1047
- op: event.target.value
1048
- }),
1049
- size: "small",
1050
- select: true,
1051
- sx: {
1052
- width: 110
1053
- },
1054
- children: RULE_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
1055
- value: op.value,
1056
- children: op.label
1057
- }, op.value))
1058
- }),
1059
- rule.field === 'categoryId' ? /*#__PURE__*/ _jsx(TextField, {
1060
- value: rule.value,
1061
- onChange: (event)=>updateRule(index, {
1062
- value: event.target.value
1063
- }),
1064
- size: "small",
1065
- select: true,
1066
- sx: {
1067
- flex: 1
1068
- },
1069
- children: categories.map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
1070
- value: category.$id,
1071
- children: category.name
1072
- }, category.$id))
1073
- }) : /*#__PURE__*/ _jsx(TextField, {
1074
- value: rule.value,
1075
- onChange: (event)=>updateRule(index, {
1076
- value: rule.field === 'priceUsd' ? Number(event.target.value) : event.target.value
1077
- }),
1078
- size: "small",
1079
- sx: {
1080
- flex: 1
1081
- },
1082
- placeholder: rule.field === 'type' ? 'physical' : 'Value'
1083
- }),
1084
- /*#__PURE__*/ _jsx(Button, {
1085
- size: "small",
1086
- color: "error",
1087
- onClick: ()=>updateRule(index, null),
1088
- children: '✕'
1089
- })
1090
- ]
1091
- }, index)),
1092
- /*#__PURE__*/ _jsx(Button, {
1093
- size: "small",
1094
- sx: {
1095
- alignSelf: 'flex-start'
1096
- },
1097
- onClick: ()=>{
1098
- var _ref;
1099
- var _collectionDraft_rules;
1100
- return updateRule((_ref = collectionDraft == null ? void 0 : (_collectionDraft_rules = collectionDraft.rules) == null ? void 0 : _collectionDraft_rules.length) != null ? _ref : 0, {});
1101
- },
1102
- children: 'Add rule'
1103
- })
1104
- ]
1105
- }),
1106
- collectionError ? /*#__PURE__*/ _jsx(Alert, {
1107
- severity: "warning",
1108
- children: collectionError
1109
- }) : null,
1110
- collectionDraft ? /*#__PURE__*/ _jsxs(Stack, {
1111
- direction: "row",
1112
- spacing: 0.5,
1113
- sx: {
1114
- flexWrap: 'wrap'
1115
- },
1116
- children: [
1117
- /*#__PURE__*/ _jsx(Typography, {
1118
- variant: "caption",
1119
- color: "text.secondary",
1120
- children: `Matches ${countPrefix}${previewMatches.length} products: `
1121
- }),
1122
- previewMatches.slice(0, 6).map((product)=>/*#__PURE__*/ _jsx(Chip, {
1123
- label: product.name,
1124
- size: "small"
1125
- }, product.$id)),
1126
- previewMatches.length > 6 ? /*#__PURE__*/ _jsx(Typography, {
1127
- variant: "caption",
1128
- color: "text.secondary",
1129
- children: `+${previewMatches.length - 6} more`
1130
- }) : null
1131
- ]
1132
- }) : null
1133
- ]
1134
- }),
1135
- /*#__PURE__*/ _jsxs(DialogActions, {
1136
- children: [
1137
- /*#__PURE__*/ _jsx(Button, {
1138
- onClick: ()=>setCollectionDraft(null),
1139
- children: 'Cancel'
1140
- }),
1141
- /*#__PURE__*/ _jsx(Button, {
1142
- variant: "contained",
1143
- color: "primary",
1144
- disabled: !(collectionDraft == null ? void 0 : collectionDraft.name.trim()) || Boolean(collectionError),
1145
- onClick: handleCollectionSave,
1146
- children: 'Save'
1147
- })
1148
- ]
1149
- })
1150
- ]
1317
+ /*#__PURE__*/ _jsx(CollectionDialog, {
1318
+ draft: collectionDraft,
1319
+ slug: draftSlug,
1320
+ slugTaken: slugTaken,
1321
+ error: collectionError,
1322
+ products: products,
1323
+ picked: collectionProducts,
1324
+ categories: categories,
1325
+ matches: previewMatches,
1326
+ countPrefix: countPrefix,
1327
+ setDraft: setCollectionDraft,
1328
+ onProducts: setCollectionProducts,
1329
+ onRule: updateRule,
1330
+ onSave: handleCollectionSave
1151
1331
  })
1152
1332
  ]
1153
1333
  });