@aglyn/plugins-email 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 (56) hide show
  1. package/package.json +18 -18
  2. package/src/lib/components/campaign-topic-options-widget.js +2 -2
  3. package/src/lib/components/campaign-topic-options-widget.js.map +1 -1
  4. package/src/lib/components/campaign-topic-select.js +3 -3
  5. package/src/lib/components/campaign-topic-select.js.map +1 -1
  6. package/src/lib/components/consent-group-review.js +2 -2
  7. package/src/lib/components/consent-group-review.js.map +1 -1
  8. package/src/lib/components/dynamic-list-rule-fields.d.ts +20 -4
  9. package/src/lib/components/dynamic-list-rule-fields.js +537 -439
  10. package/src/lib/components/dynamic-list-rule-fields.js.map +1 -1
  11. package/src/lib/components/email-topic-detail.d.ts +1 -1
  12. package/src/lib/components/email-topic-detail.js +5 -5
  13. package/src/lib/components/email-topic-detail.js.map +1 -1
  14. package/src/lib/components/email-topics-card.d.ts +1 -1
  15. package/src/lib/components/email-topics-card.js +6 -5
  16. package/src/lib/components/email-topics-card.js.map +1 -1
  17. package/src/lib/components/emails-console-sections.js +3 -2
  18. package/src/lib/components/emails-console-sections.js.map +1 -1
  19. package/src/lib/components/list-import-drawer.js +2 -2
  20. package/src/lib/components/list-import-drawer.js.map +1 -1
  21. package/src/lib/components/use-org-email-topics.d.ts +4 -4
  22. package/src/lib/components/use-org-email-topics.js +6 -5
  23. package/src/lib/components/use-org-email-topics.js.map +1 -1
  24. package/src/lib/list-import.d.ts +149 -0
  25. package/src/lib/list-import.js +324 -0
  26. package/src/lib/list-import.js.map +1 -0
  27. package/src/lib/model/email-record-routes.d.ts +22 -0
  28. package/src/lib/model/email-record-routes.js +75 -0
  29. package/src/lib/model/email-record-routes.js.map +1 -0
  30. package/src/lib/model/email-topic-catalog.d.ts +36 -0
  31. package/src/lib/model/email-topic-catalog.js +36 -0
  32. package/src/lib/model/email-topic-catalog.js.map +1 -0
  33. package/src/lib/model/index.d.ts +0 -7
  34. package/src/lib/model/index.js +0 -6
  35. package/src/lib/model/index.js.map +1 -1
  36. package/src/lib/model/template-provenance.d.ts +1 -1
  37. package/src/lib/model/template-provenance.js +1 -1
  38. package/src/lib/model/template-provenance.js.map +1 -1
  39. package/src/lib/plugin.js +5 -1
  40. package/src/lib/plugin.js.map +1 -1
  41. package/src/lib/server-console.js +6 -2
  42. package/src/lib/server-console.js.map +1 -1
  43. package/src/lib/server-email-drafts.js +2 -2
  44. package/src/lib/server-email-drafts.js.map +1 -1
  45. package/src/lib/server-list-gate.js +35 -10
  46. package/src/lib/server-list-gate.js.map +1 -1
  47. package/src/lib/server-list-import.js +4 -3
  48. package/src/lib/server-list-import.js.map +1 -1
  49. package/src/lib/server-topic-index.d.ts +52 -0
  50. package/src/lib/server-topic-index.js +92 -0
  51. package/src/lib/server-topic-index.js.map +1 -0
  52. package/src/lib/server.js +11 -11
  53. package/src/lib/server.js.map +1 -1
  54. package/src/lib/unsubscribe-link.d.ts +1 -1
  55. package/src/lib/unsubscribe-link.js +2 -2
  56. package/src/lib/unsubscribe-link.js.map +1 -1
@@ -58,9 +58,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
58
58
  * nobody has opened.
59
59
  */ import { CONTACT_LIFECYCLE_STAGES, CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_SOURCE_LABELS, DYNAMIC_LIST_CUSTOM_OPS, normalizeDynamicListRule } from "@aglyn/aglyn";
60
60
  import { Autocomplete, Box, Button, Divider, MenuItem, Stack, TextField, Typography } from "@mui/material";
61
- import { useEffect, useMemo, useRef, useState } from "react";
62
- import { useHostCampaigns, useOrgMemberOptions } from "@aglyn/tenant-feature-instance";
63
- import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
61
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
62
+ import { useSiteContainerOptions, useOrgMemberOptions } from "@aglyn/tenant-feature-instance";
63
+ import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
64
+ import { useContentStable } from "@aglyn/shared-ui-jsx/hooks/use-content-stable";
64
65
  import { useOrgCompanyOptions } from "../hooks/use-org-company-options.js";
65
66
  import { useOrgContactFields } from "../hooks/use-org-contact-fields.js";
66
67
  import { useOrgContactSegments } from "../hooks/use-org-contact-segments.js";
@@ -585,7 +586,307 @@ export const EMPTY_RULE_DRAFT = {
585
586
  negate: true
586
587
  } : {})));
587
588
  }
588
- export function DynamicListRuleFields(props) {
589
+ const companyLabel = (option)=>option.label;
590
+ const sameCompany = (option, chosen)=>option.id === chosen.id;
591
+ // The query already narrowed the hits; a second, client-side filter on the
592
+ // label would hide a match whose stored name differs from its search key.
593
+ const keepHits = (options)=>options;
594
+ const COMPANY_FIELD_SX = {
595
+ minWidth: 260,
596
+ flexGrow: 1,
597
+ maxWidth: 420
598
+ };
599
+ /**
600
+ * The rule's companies, as chips beside a search — MEMOIZED, and every prop it
601
+ * takes is stable while the chosen companies and the search are (AGL-3423).
602
+ *
603
+ * A multiple Autocomplete hands its input a new chip array as
604
+ * `startAdornment` on every render, and MUI's `InputBase` copies that into
605
+ * its `FormControl` from a passive effect: a state update left pending after
606
+ * every commit the field takes part in. React 19 counts each such commit as a
607
+ * nested update, and keystrokes delivered back to back commit one after
608
+ * another with nothing to clear the count, so the fifty-first throws
609
+ * "Maximum update depth exceeded" (#185). Every other field of the rule
610
+ * writes the same draft, so each keystroke in them re-rendered this one;
611
+ * kept out of those renders, it commits only when it changes.
612
+ */ const RuleCompaniesField = /*#__PURE__*/ memo(function RuleCompaniesField(props) {
613
+ const { onSearch, onChange } = props;
614
+ return /*#__PURE__*/ _jsx(Autocomplete, {
615
+ multiple: true,
616
+ size: "small",
617
+ options: props.hits,
618
+ value: props.value,
619
+ getOptionLabel: companyLabel,
620
+ isOptionEqualToValue: sameCompany,
621
+ filterOptions: keepHits,
622
+ inputValue: props.search,
623
+ onInputChange: (_event, value, reason)=>{
624
+ if (reason !== 'reset') onSearch(value);
625
+ },
626
+ onChange: (_event, value)=>onChange(value.map((option)=>option.id)),
627
+ loading: props.searching,
628
+ noOptionsText: props.search ? 'No company by that name' : 'Type to search companies',
629
+ renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
630
+ label: "At company",
631
+ helperText: "Any of these companies"
632
+ })),
633
+ sx: COMPANY_FIELD_SX
634
+ });
635
+ });
636
+ /** A text field that redraws only when what it shows changes (AGL-3423). */ const MemoTextField = /*#__PURE__*/ memo(TextField);
637
+ const MemoContainerPicker = /*#__PURE__*/ memo(ContainerPicker);
638
+ /**
639
+ * One `{ minWidth }` object per width, so a memoized field's `sx` is the same
640
+ * object on every render.
641
+ */ const MIN_WIDTHS = new Map();
642
+ const atLeast = (minWidth)=>{
643
+ let sx = MIN_WIDTHS.get(minWidth);
644
+ if (!sx) {
645
+ sx = {
646
+ minWidth
647
+ };
648
+ MIN_WIDTHS.set(minWidth, sx);
649
+ }
650
+ return sx;
651
+ };
652
+ const WRAP = {
653
+ flexWrap: 'wrap'
654
+ };
655
+ const WRAP_TOP = {
656
+ flexWrap: 'wrap',
657
+ alignItems: 'flex-start'
658
+ };
659
+ const SUMMARY_BOX = {
660
+ border: 1,
661
+ borderColor: 'divider',
662
+ borderRadius: 1,
663
+ p: 1.5
664
+ };
665
+ const CAMPAIGN_BOX = {
666
+ minWidth: 240,
667
+ flexGrow: 1,
668
+ maxWidth: 360
669
+ };
670
+ const CAPTION_BLOCK = {
671
+ display: 'block',
672
+ mt: 0.5
673
+ };
674
+ const MULTIPLE = {
675
+ select: {
676
+ multiple: true
677
+ }
678
+ };
679
+ const SHRUNK_LABEL = {
680
+ inputLabel: {
681
+ shrink: true
682
+ }
683
+ };
684
+ const STAGES_SELECT = {
685
+ select: {
686
+ multiple: true,
687
+ renderValue: (selected)=>selected.map((stage)=>{
688
+ var _CONTACT_LIFECYCLE_STAGE_LABELS_stage;
689
+ return (_CONTACT_LIFECYCLE_STAGE_LABELS_stage = CONTACT_LIFECYCLE_STAGE_LABELS[stage]) != null ? _CONTACT_LIFECYCLE_STAGE_LABELS_stage : stage;
690
+ }).join(', ')
691
+ }
692
+ };
693
+ /*
694
+ * The fixed menus, built once. A select's options are its children, and a
695
+ * child list built during render is a new prop on every render.
696
+ */ const SOURCE_MENU = Object.entries(SOURCE_LABELS).map(([value, label])=>/*#__PURE__*/ _jsx(MenuItem, {
697
+ value: value,
698
+ children: label
699
+ }, value));
700
+ const MATCH_MENU = [
701
+ /*#__PURE__*/ _jsx(MenuItem, {
702
+ value: "all",
703
+ children: 'All of the filters below'
704
+ }, "all"),
705
+ /*#__PURE__*/ _jsx(MenuItem, {
706
+ value: "any",
707
+ children: 'Any one of the filters below'
708
+ }, "any"),
709
+ /*#__PURE__*/ _jsx(MenuItem, {
710
+ value: "none",
711
+ children: 'Nobody matching all of them'
712
+ }, "none")
713
+ ];
714
+ const CAPTURE_MENU = Object.entries(CONTACT_SOURCE_LABELS).map(([value, label])=>/*#__PURE__*/ _jsx(MenuItem, {
715
+ value: value,
716
+ children: label
717
+ }, value));
718
+ const STAGE_MENU = CONTACT_LIFECYCLE_STAGES.map((stage)=>/*#__PURE__*/ _jsx(MenuItem, {
719
+ value: stage,
720
+ children: CONTACT_LIFECYCLE_STAGE_LABELS[stage]
721
+ }, stage));
722
+ const OP_MENU = DYNAMIC_LIST_CUSTOM_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
723
+ value: op,
724
+ children: CUSTOM_OP_LABELS[op]
725
+ }, op));
726
+ const CHECKED_MENU = [
727
+ /*#__PURE__*/ _jsx(MenuItem, {
728
+ value: "true",
729
+ children: 'Checked'
730
+ }, "true"),
731
+ /*#__PURE__*/ _jsx(MenuItem, {
732
+ value: "false",
733
+ children: 'Not checked'
734
+ }, "false")
735
+ ];
736
+ /** A multi-select hands back a string when only one option is chosen. */ const asArray = (value)=>typeof value === 'string' ? [
737
+ value
738
+ ] : value != null ? value : [];
739
+ /** Saved segments, views and audiences as menu items: a name, else the id. */ const menuOf = (rows)=>rows.map((row)=>{
740
+ var _row_name;
741
+ return /*#__PURE__*/ _jsx(MenuItem, {
742
+ value: row.$id,
743
+ children: (_row_name = row.name) != null ? _row_name : row.$id
744
+ }, row.$id);
745
+ });
746
+ /** The first choice of a saved segment or view: none. No stored id is empty. */ const NONE_ITEM = /*#__PURE__*/ _jsx(MenuItem, {
747
+ value: "",
748
+ children: 'None'
749
+ }, "");
750
+ /**
751
+ * One field condition: the field, how it compares, and the value — typed by
752
+ * the field's definition, so a number field hands the matcher a number and a
753
+ * choice field offers its own options, and a presence test asks for no value
754
+ * at all.
755
+ *
756
+ * Memoized, with handlers that write through the row's index, so a keystroke
757
+ * in one condition's value redraws that value and nothing else (AGL-3423).
758
+ */ const CustomClauseRow = /*#__PURE__*/ memo(function CustomClauseRow(props) {
759
+ var _clause_value, _definition_options;
760
+ const { index, clause, definition, fieldMenu, onUpdate, onRemove } = props;
761
+ const valueText = String((_clause_value = clause.value) != null ? _clause_value : '');
762
+ const setKey = useCallback((event)=>onUpdate(index, {
763
+ key: event.target.value,
764
+ value: ''
765
+ }), [
766
+ index,
767
+ onUpdate
768
+ ]);
769
+ const setOp = useCallback((event)=>onUpdate(index, {
770
+ op: event.target.value
771
+ }), [
772
+ index,
773
+ onUpdate
774
+ ]);
775
+ const setValue = useCallback((event)=>onUpdate(index, {
776
+ value: event.target.value
777
+ }), [
778
+ index,
779
+ onUpdate
780
+ ]);
781
+ const setChecked = useCallback((event)=>onUpdate(index, {
782
+ value: event.target.value === 'true'
783
+ }), [
784
+ index,
785
+ onUpdate
786
+ ]);
787
+ const setNumber = useCallback((event)=>{
788
+ // A number field stores a NUMBER, so the matcher compares 10 with 9
789
+ // and not "10" with "9"; half-typed text stays text until it parses,
790
+ // rather than becoming NaN.
791
+ const text = event.target.value;
792
+ const parsed = Number(text);
793
+ onUpdate(index, {
794
+ value: text.trim() !== '' && Number.isFinite(parsed) ? parsed : text
795
+ });
796
+ }, [
797
+ index,
798
+ onUpdate
799
+ ]);
800
+ const remove = useCallback(()=>onRemove(index), [
801
+ index,
802
+ onRemove
803
+ ]);
804
+ return /*#__PURE__*/ _jsxs(Stack, {
805
+ direction: "row",
806
+ spacing: 1,
807
+ useFlexGap: true,
808
+ sx: WRAP_TOP,
809
+ children: [
810
+ /*#__PURE__*/ _jsx(MemoTextField, {
811
+ select: true,
812
+ size: "small",
813
+ label: "Field",
814
+ value: clause.key,
815
+ onChange: setKey,
816
+ sx: atLeast(180),
817
+ children: fieldMenu
818
+ }),
819
+ /*#__PURE__*/ _jsx(MemoTextField, {
820
+ select: true,
821
+ size: "small",
822
+ label: "Condition",
823
+ value: clause.op,
824
+ onChange: setOp,
825
+ sx: atLeast(160),
826
+ children: OP_MENU
827
+ }),
828
+ PRESENCE_OPS.has(clause.op) ? null : (definition == null ? void 0 : definition.type) === 'select' ? /*#__PURE__*/ _jsx(MemoTextField, {
829
+ select: true,
830
+ size: "small",
831
+ label: "Value",
832
+ value: valueText,
833
+ onChange: setValue,
834
+ sx: atLeast(180),
835
+ children: ((_definition_options = definition.options) != null ? _definition_options : []).map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
836
+ value: option,
837
+ children: option
838
+ }, option))
839
+ }) : (definition == null ? void 0 : definition.type) === 'checkbox' ? /*#__PURE__*/ _jsx(MemoTextField, {
840
+ select: true,
841
+ size: "small",
842
+ label: "Value",
843
+ value: clause.value === true ? 'true' : clause.value === false ? 'false' : '',
844
+ onChange: setChecked,
845
+ sx: atLeast(140),
846
+ children: CHECKED_MENU
847
+ }) : (definition == null ? void 0 : definition.type) === 'number' ? /*#__PURE__*/ _jsx(MemoTextField, {
848
+ type: "number",
849
+ size: "small",
850
+ label: "Value",
851
+ value: valueText,
852
+ onChange: setNumber,
853
+ sx: atLeast(140)
854
+ }) : /*#__PURE__*/ _jsx(MemoTextField, {
855
+ type: (definition == null ? void 0 : definition.type) === 'date' ? 'date' : 'text',
856
+ size: "small",
857
+ label: "Value",
858
+ value: valueText,
859
+ onChange: setValue,
860
+ slotProps: (definition == null ? void 0 : definition.type) === 'date' ? SHRUNK_LABEL : undefined,
861
+ sx: atLeast(180)
862
+ }),
863
+ /*#__PURE__*/ _jsx(Button, {
864
+ size: "small",
865
+ onClick: remove,
866
+ children: 'Remove'
867
+ })
868
+ ]
869
+ });
870
+ });
871
+ /**
872
+ * The rule's controls.
873
+ *
874
+ * ## A keystroke redraws the field it lands in (AGL-3423)
875
+ *
876
+ * Every control writes the same draft, and the draft is the parent's state,
877
+ * so each keystroke renders this component again. Drawn inline, that redrew
878
+ * every control on the form — 27 inputs and some thousand components for one
879
+ * letter — and a keystroke that costs that much lets typed input queue, which
880
+ * is what turns a chip field's per-commit update into React #185. So every
881
+ * control is memoized and handed only props that keep their identity while
882
+ * what it shows is unchanged: handlers that write through the latest draft,
883
+ * menus built once or from their own data, and `sx` objects that are
884
+ * constants. The sentences above the controls are the one part that is meant
885
+ * to change with every keystroke.
886
+ *
887
+ * The component is memoized as a whole as well, so a keystroke in the page's
888
+ * own fields — the list's name — does not draw the rule at all.
889
+ */ export const DynamicListRuleFields = /*#__PURE__*/ memo(function DynamicListRuleFields(props) {
589
890
  var _team_error;
590
891
  const { scope, hostId, draft, onChange, listId } = props;
591
892
  const segmentDocs = useOrgContactSegments(scope);
@@ -598,7 +899,7 @@ export function DynamicListRuleFields(props) {
598
899
  * picker beside fields a reader came for; here the picker IS one of the
599
900
  * fields the reader came for, and the same reasoning that opens the segment
600
901
  * listen opens this one.
601
- */ const siteCampaigns = useHostCampaigns(hostId, {
902
+ */ const siteCampaigns = useSiteContainerOptions('campaign', hostId, {
602
903
  enabled: true
603
904
  });
604
905
  /*
@@ -670,6 +971,12 @@ export function DynamicListRuleFields(props) {
670
971
  listDocs,
671
972
  listId
672
973
  ]);
974
+ const memberNames = useMemo(()=>Object.fromEntries(team.options.map((option)=>[
975
+ option.uid,
976
+ option.label
977
+ ])), [
978
+ team.options
979
+ ]);
673
980
  /** Ids the pickers can show, so a clause names an audience rather than a uid. */ const names = useMemo(()=>({
674
981
  lists: Object.fromEntries(listDocs.map((row)=>{
675
982
  var _row_name;
@@ -693,10 +1000,7 @@ export function DynamicListRuleFields(props) {
693
1000
  option.value,
694
1001
  option.label
695
1002
  ])),
696
- members: Object.fromEntries(team.options.map((option)=>[
697
- option.uid,
698
- option.label
699
- ])),
1003
+ members: memberNames,
700
1004
  companies: companies.names,
701
1005
  fields: Object.fromEntries(fieldDefinitions.fields.map((field)=>[
702
1006
  field.key,
@@ -707,7 +1011,7 @@ export function DynamicListRuleFields(props) {
707
1011
  segments,
708
1012
  crmViews,
709
1013
  siteCampaigns.options,
710
- team.options,
1014
+ memberNames,
711
1015
  companies.names,
712
1016
  fieldDefinitions.fields
713
1017
  ]);
@@ -732,7 +1036,11 @@ export function DynamicListRuleFields(props) {
732
1036
  team.options,
733
1037
  draft.ownerUids
734
1038
  ]);
735
- const fieldOptions = useMemo(()=>{
1039
+ /*
1040
+ * Held by content: the options are read from the clauses' KEYS, and a value
1041
+ * typed into a clause is a new clause list with the same keys — which would
1042
+ * otherwise redraw every row's field picker on each keystroke (AGL-3423).
1043
+ */ const fieldOptions = useContentStable(useMemo(()=>{
736
1044
  const known = new Set(fieldDefinitions.fields.map((field)=>field.key));
737
1045
  return [
738
1046
  ...draft.custom.filter((clause)=>!known.has(clause.key)).map((clause)=>({
@@ -747,11 +1055,101 @@ export function DynamicListRuleFields(props) {
747
1055
  }, [
748
1056
  fieldDefinitions.fields,
749
1057
  draft.custom
1058
+ ]));
1059
+ /* The menus that come from data, rebuilt only when their data changes. */ const segmentMenu = useMemo(()=>[
1060
+ NONE_ITEM,
1061
+ ...menuOf(segments)
1062
+ ], [
1063
+ segments
750
1064
  ]);
751
- /** The definition a clause's key names, when the org still has one. */ const definitionFor = (key)=>fieldDefinitions.fields.find((field)=>field.key === key);
752
- const set = (key, value)=>onChange(_extends({}, draft, {
1065
+ const viewMenu = useMemo(()=>[
1066
+ NONE_ITEM,
1067
+ ...menuOf(crmViews)
1068
+ ], [
1069
+ crmViews
1070
+ ]);
1071
+ const listMenu = useMemo(()=>menuOf(lists), [
1072
+ lists
1073
+ ]);
1074
+ const ownerMenu = useMemo(()=>ownerOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1075
+ value: option.uid,
1076
+ children: option.label
1077
+ }, option.uid)), [
1078
+ ownerOptions
1079
+ ]);
1080
+ const ownerSelect = useMemo(()=>({
1081
+ select: {
1082
+ multiple: true,
1083
+ renderValue: (selected)=>selected.map((uid)=>named(uid, memberNames)).join(', ')
1084
+ }
1085
+ }), [
1086
+ memberNames
1087
+ ]);
1088
+ const fieldMenu = useMemo(()=>fieldOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1089
+ value: option.key,
1090
+ children: option.label
1091
+ }, option.key)), [
1092
+ fieldOptions
1093
+ ]);
1094
+ /*
1095
+ * Every control writes `{ ...draft, [key]: value }` from the last rendered
1096
+ * draft, through handlers that keep their identity from render to render —
1097
+ * a handler rebuilt per render is a changed prop, and a changed prop redraws
1098
+ * the field it is on.
1099
+ */ const latest = useRef({
1100
+ draft,
1101
+ onChange
1102
+ });
1103
+ latest.current = {
1104
+ draft,
1105
+ onChange
1106
+ };
1107
+ const set = useCallback((key, value)=>latest.current.onChange(_extends({}, latest.current.draft, {
753
1108
  [key]: value
754
- }));
1109
+ })), []);
1110
+ const on = useMemo(()=>{
1111
+ const typed = (key)=>(event)=>set(key, event.target.value);
1112
+ return {
1113
+ sources: (event)=>set('sources', asArray(event.target.value)),
1114
+ match: (event)=>set('match', event.target.value),
1115
+ createdAfter: typed('createdAfter'),
1116
+ createdBefore: typed('createdBefore'),
1117
+ formNames: typed('formNames'),
1118
+ campaignIds: (next)=>set('campaignIds', next),
1119
+ segmentId: typed('segmentId'),
1120
+ viewId: typed('viewId'),
1121
+ tags: typed('tags'),
1122
+ captureSources: (event)=>set('captureSources', asArray(event.target.value)),
1123
+ ownerUids: (event)=>set('ownerUids', asArray(event.target.value)),
1124
+ lifecycleStages: (event)=>set('lifecycleStages', asArray(event.target.value)),
1125
+ companyIds: (companyIds)=>set('companyIds', companyIds),
1126
+ engagedWithinDays: typed('engagedWithinDays'),
1127
+ ordersCountAtLeast: typed('ordersCountAtLeast'),
1128
+ ltvAtLeast: typed('ltvAtLeast'),
1129
+ lastPurchaseWithinDays: typed('lastPurchaseWithinDays'),
1130
+ noPurchaseForDays: typed('noPurchaseForDays'),
1131
+ openedWithinDays: typed('openedWithinDays'),
1132
+ clickedWithinDays: typed('clickedWithinDays'),
1133
+ notOpenedForDays: typed('notOpenedForDays'),
1134
+ notClickedForDays: typed('notClickedForDays'),
1135
+ inListIds: (event)=>set('inListIds', asArray(event.target.value)),
1136
+ notInListIds: (event)=>set('notInListIds', asArray(event.target.value))
1137
+ };
1138
+ }, [
1139
+ set
1140
+ ]);
1141
+ const companyIds = draft.companyIds;
1142
+ const companyNames = companies.names;
1143
+ const chosenCompanies = useMemo(()=>companyIds.map((id)=>{
1144
+ var _companyNames_id;
1145
+ return {
1146
+ id,
1147
+ label: (_companyNames_id = companyNames[id]) != null ? _companyNames_id : id
1148
+ };
1149
+ }), [
1150
+ companyIds,
1151
+ companyNames
1152
+ ]);
755
1153
  /*
756
1154
  * THE CUSTOM-FIELD ROWS. A new row names the first definition with an
757
1155
  * empty value, so it is on screen and NOT yet a filter — `draftDimensions`
@@ -760,39 +1158,39 @@ export function DynamicListRuleFields(props) {
760
1158
  * a value typed for a number field is not a value for the select that
761
1159
  * replaced it; changing the operator to a presence test removes it, so the
762
1160
  * stored clause carries no value the sentence does not read.
763
- */ const addClause = ()=>{
1161
+ */ const addClause = useCallback(()=>{
764
1162
  const first = fieldOptions[0];
765
1163
  if (!first) return;
766
1164
  set('custom', [
767
- ...draft.custom,
1165
+ ...latest.current.draft.custom,
768
1166
  {
769
1167
  key: first.key,
770
1168
  op: 'eq',
771
1169
  value: ''
772
1170
  }
773
1171
  ]);
774
- };
775
- const updateClause = (index, patch)=>set('custom', draft.custom.map((clause, at)=>{
1172
+ }, [
1173
+ fieldOptions,
1174
+ set
1175
+ ]);
1176
+ const updateClause = useCallback((index, patch)=>set('custom', latest.current.draft.custom.map((clause, at)=>{
776
1177
  if (at !== index) return clause;
777
1178
  const next = _extends({}, clause, patch);
778
1179
  if (PRESENCE_OPS.has(next.op)) delete next.value;
779
1180
  return next;
780
- }));
781
- const removeClause = (index)=>set('custom', draft.custom.filter((_, at)=>at !== index));
782
- /** A multi-select hands back a string when only one option is chosen. */ const asArray = (value)=>typeof value === 'string' ? [
783
- value
784
- ] : value != null ? value : [];
1181
+ })), [
1182
+ set
1183
+ ]);
1184
+ const removeClause = useCallback((index)=>set('custom', latest.current.draft.custom.filter((_, at)=>at !== index)), [
1185
+ set
1186
+ ]);
1187
+ /** The definition a clause's key names, when the org still has one. */ const definitionFor = (key)=>fieldDefinitions.fields.find((field)=>field.key === key);
785
1188
  const contactsOnly = draft.sources.includes('contacts');
786
1189
  return /*#__PURE__*/ _jsxs(Stack, {
787
1190
  spacing: 1.5,
788
1191
  children: [
789
1192
  /*#__PURE__*/ _jsxs(Box, {
790
- sx: {
791
- border: 1,
792
- borderColor: 'divider',
793
- borderRadius: 1,
794
- p: 1.5
795
- },
1193
+ sx: SUMMARY_BOX,
796
1194
  children: [
797
1195
  /*#__PURE__*/ _jsx(Typography, {
798
1196
  variant: "overline",
@@ -809,105 +1207,62 @@ export function DynamicListRuleFields(props) {
809
1207
  direction: "row",
810
1208
  spacing: 1,
811
1209
  useFlexGap: true,
812
- sx: {
813
- flexWrap: 'wrap'
814
- },
1210
+ sx: WRAP,
815
1211
  children: [
816
- /*#__PURE__*/ _jsx(TextField, {
1212
+ /*#__PURE__*/ _jsx(MemoTextField, {
817
1213
  select: true,
818
1214
  size: "small",
819
1215
  label: "People from",
820
1216
  value: draft.sources,
821
- onChange: (event)=>set('sources', asArray(event.target.value)),
822
- slotProps: {
823
- select: {
824
- multiple: true
825
- }
826
- },
827
- sx: {
828
- minWidth: 220
829
- },
830
- children: Object.entries(SOURCE_LABELS).map(([value, label])=>/*#__PURE__*/ _jsx(MenuItem, {
831
- value: value,
832
- children: label
833
- }, value))
1217
+ onChange: on.sources,
1218
+ slotProps: MULTIPLE,
1219
+ sx: atLeast(220),
1220
+ children: SOURCE_MENU
834
1221
  }),
835
- /*#__PURE__*/ _jsxs(TextField, {
1222
+ /*#__PURE__*/ _jsx(MemoTextField, {
836
1223
  select: true,
837
1224
  size: "small",
838
1225
  label: "Match",
839
1226
  value: draft.match,
840
- onChange: (event)=>set('match', event.target.value),
1227
+ onChange: on.match,
841
1228
  helperText: "A saved segment always applies",
842
- sx: {
843
- minWidth: 210
844
- },
845
- children: [
846
- /*#__PURE__*/ _jsx(MenuItem, {
847
- value: "all",
848
- children: 'All of the filters below'
849
- }),
850
- /*#__PURE__*/ _jsx(MenuItem, {
851
- value: "any",
852
- children: 'Any one of the filters below'
853
- }),
854
- /*#__PURE__*/ _jsx(MenuItem, {
855
- value: "none",
856
- children: 'Nobody matching all of them'
857
- })
858
- ]
1229
+ sx: atLeast(210),
1230
+ children: MATCH_MENU
859
1231
  }),
860
- /*#__PURE__*/ _jsx(TextField, {
1232
+ /*#__PURE__*/ _jsx(MemoTextField, {
861
1233
  type: "date",
862
1234
  size: "small",
863
1235
  label: "Created after",
864
1236
  value: draft.createdAfter,
865
- onChange: (event)=>set('createdAfter', event.target.value),
866
- slotProps: {
867
- inputLabel: {
868
- shrink: true
869
- }
870
- },
871
- sx: {
872
- minWidth: 170
873
- }
1237
+ onChange: on.createdAfter,
1238
+ slotProps: SHRUNK_LABEL,
1239
+ sx: atLeast(170)
874
1240
  }),
875
- /*#__PURE__*/ _jsx(TextField, {
1241
+ /*#__PURE__*/ _jsx(MemoTextField, {
876
1242
  type: "date",
877
1243
  size: "small",
878
1244
  label: "Created before",
879
1245
  value: draft.createdBefore,
880
- onChange: (event)=>set('createdBefore', event.target.value),
881
- slotProps: {
882
- inputLabel: {
883
- shrink: true
884
- }
885
- },
886
- sx: {
887
- minWidth: 170
888
- }
1246
+ onChange: on.createdBefore,
1247
+ slotProps: SHRUNK_LABEL,
1248
+ sx: atLeast(170)
889
1249
  }),
890
- /*#__PURE__*/ _jsx(TextField, {
1250
+ /*#__PURE__*/ _jsx(MemoTextField, {
891
1251
  size: "small",
892
1252
  label: "Submitted form",
893
1253
  placeholder: "Contact us",
894
1254
  helperText: "Form submissions only",
895
1255
  value: draft.formNames,
896
- onChange: (event)=>set('formNames', event.target.value),
897
- sx: {
898
- minWidth: 180
899
- }
1256
+ onChange: on.formNames,
1257
+ sx: atLeast(180)
900
1258
  }),
901
1259
  /*#__PURE__*/ _jsx(Box, {
902
- sx: {
903
- minWidth: 240,
904
- flexGrow: 1,
905
- maxWidth: 360
906
- },
907
- children: /*#__PURE__*/ _jsx(CampaignPicker, {
1260
+ sx: CAMPAIGN_BOX,
1261
+ children: /*#__PURE__*/ _jsx(MemoContainerPicker, {
1262
+ kind: "campaign",
908
1263
  options: siteCampaigns.options,
909
1264
  value: draft.campaignIds,
910
- onChange: (next)=>set('campaignIds', next),
1265
+ onChange: on.campaignIds,
911
1266
  label: "In campaign",
912
1267
  helperText: "Contacts and form submissions filed under any campaign picked. Being in a campaign is not consent to be emailed.",
913
1268
  empty: siteCampaigns.ready && !siteCampaigns.options.length,
@@ -931,84 +1286,46 @@ export function DynamicListRuleFields(props) {
931
1286
  direction: "row",
932
1287
  spacing: 1,
933
1288
  useFlexGap: true,
934
- sx: {
935
- flexWrap: 'wrap'
936
- },
1289
+ sx: WRAP,
937
1290
  children: [
938
- /*#__PURE__*/ _jsxs(TextField, {
1291
+ /*#__PURE__*/ _jsx(MemoTextField, {
939
1292
  select: true,
940
1293
  size: "small",
941
1294
  label: "Saved segment",
942
1295
  value: draft.segmentId,
943
- onChange: (event)=>set('segmentId', event.target.value),
1296
+ onChange: on.segmentId,
944
1297
  helperText: "Reuses that segment's tags and sources",
945
- sx: {
946
- minWidth: 200
947
- },
948
- children: [
949
- /*#__PURE__*/ _jsx(MenuItem, {
950
- value: "",
951
- children: 'None'
952
- }),
953
- segments.map((segment)=>{
954
- var _segment_name;
955
- return /*#__PURE__*/ _jsx(MenuItem, {
956
- value: segment.$id,
957
- children: (_segment_name = segment.name) != null ? _segment_name : segment.$id
958
- }, segment.$id);
959
- })
960
- ]
1298
+ sx: atLeast(200),
1299
+ children: segmentMenu
961
1300
  }),
962
- /*#__PURE__*/ _jsxs(TextField, {
1301
+ /*#__PURE__*/ _jsx(MemoTextField, {
963
1302
  select: true,
964
1303
  size: "small",
965
1304
  label: "Saved view",
966
1305
  value: draft.viewId,
967
- onChange: (event)=>set('viewId', event.target.value),
1306
+ onChange: on.viewId,
968
1307
  helperText: "Reuses that Contacts view's filters",
969
- sx: {
970
- minWidth: 200
971
- },
972
- children: [
973
- /*#__PURE__*/ _jsx(MenuItem, {
974
- value: "",
975
- children: 'None'
976
- }),
977
- crmViews.map((view)=>/*#__PURE__*/ _jsx(MenuItem, {
978
- value: view.$id,
979
- children: view.name
980
- }, view.$id))
981
- ]
1308
+ sx: atLeast(200),
1309
+ children: viewMenu
982
1310
  }),
983
- /*#__PURE__*/ _jsx(TextField, {
1311
+ /*#__PURE__*/ _jsx(MemoTextField, {
984
1312
  size: "small",
985
1313
  label: "Tagged",
986
1314
  placeholder: "vip, wholesale",
987
1315
  value: draft.tags,
988
- onChange: (event)=>set('tags', event.target.value),
989
- sx: {
990
- minWidth: 180
991
- }
1316
+ onChange: on.tags,
1317
+ sx: atLeast(180)
992
1318
  }),
993
- /*#__PURE__*/ _jsx(TextField, {
1319
+ /*#__PURE__*/ _jsx(MemoTextField, {
994
1320
  select: true,
995
1321
  size: "small",
996
1322
  label: "Captured by",
997
1323
  value: draft.captureSources,
998
- onChange: (event)=>set('captureSources', asArray(event.target.value)),
999
- slotProps: {
1000
- select: {
1001
- multiple: true
1002
- }
1003
- },
1324
+ onChange: on.captureSources,
1325
+ slotProps: MULTIPLE,
1004
1326
  helperText: "How the contact reached you",
1005
- sx: {
1006
- minWidth: 200
1007
- },
1008
- children: Object.entries(CONTACT_SOURCE_LABELS).map(([value, label])=>/*#__PURE__*/ _jsx(MenuItem, {
1009
- value: value,
1010
- children: label
1011
- }, value))
1327
+ sx: atLeast(200),
1328
+ children: CAPTURE_MENU
1012
1329
  })
1013
1330
  ]
1014
1331
  }),
@@ -1016,228 +1333,58 @@ export function DynamicListRuleFields(props) {
1016
1333
  direction: "row",
1017
1334
  spacing: 1,
1018
1335
  useFlexGap: true,
1019
- sx: {
1020
- flexWrap: 'wrap'
1021
- },
1336
+ sx: WRAP,
1022
1337
  children: [
1023
- /*#__PURE__*/ _jsx(TextField, {
1338
+ /*#__PURE__*/ _jsx(MemoTextField, {
1024
1339
  select: true,
1025
1340
  size: "small",
1026
1341
  label: "Owned by",
1027
1342
  value: draft.ownerUids,
1028
- onChange: (event)=>set('ownerUids', asArray(event.target.value)),
1029
- slotProps: {
1030
- select: {
1031
- multiple: true,
1032
- renderValue: (selected)=>selected.map((uid)=>named(uid, names.members)).join(', ')
1033
- }
1034
- },
1343
+ onChange: on.ownerUids,
1344
+ slotProps: ownerSelect,
1035
1345
  error: Boolean(team.error),
1036
1346
  helperText: (_team_error = team.error) != null ? _team_error : 'Any of these team members. Unowned contacts are left out.',
1037
- sx: {
1038
- minWidth: 220
1039
- },
1040
- children: ownerOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1041
- value: option.uid,
1042
- children: option.label
1043
- }, option.uid))
1347
+ sx: atLeast(220),
1348
+ children: ownerMenu
1044
1349
  }),
1045
- /*#__PURE__*/ _jsx(TextField, {
1350
+ /*#__PURE__*/ _jsx(MemoTextField, {
1046
1351
  select: true,
1047
1352
  size: "small",
1048
1353
  label: "Lifecycle stage",
1049
1354
  value: draft.lifecycleStages,
1050
- onChange: (event)=>set('lifecycleStages', asArray(event.target.value)),
1051
- slotProps: {
1052
- select: {
1053
- multiple: true,
1054
- renderValue: (selected)=>selected.map((stage)=>{
1055
- var _CONTACT_LIFECYCLE_STAGE_LABELS_stage;
1056
- return (_CONTACT_LIFECYCLE_STAGE_LABELS_stage = CONTACT_LIFECYCLE_STAGE_LABELS[stage]) != null ? _CONTACT_LIFECYCLE_STAGE_LABELS_stage : stage;
1057
- }).join(', ')
1058
- }
1059
- },
1355
+ onChange: on.lifecycleStages,
1356
+ slotProps: STAGES_SELECT,
1060
1357
  helperText: "Any of these stages",
1061
- sx: {
1062
- minWidth: 200
1063
- },
1064
- children: CONTACT_LIFECYCLE_STAGES.map((stage)=>/*#__PURE__*/ _jsx(MenuItem, {
1065
- value: stage,
1066
- children: CONTACT_LIFECYCLE_STAGE_LABELS[stage]
1067
- }, stage))
1358
+ sx: atLeast(200),
1359
+ children: STAGE_MENU
1068
1360
  }),
1069
- /*#__PURE__*/ _jsx(Autocomplete, {
1070
- multiple: true,
1071
- size: "small",
1072
- options: companies.hits,
1073
- value: draft.companyIds.map((id)=>{
1074
- var _companies_names_id;
1075
- return {
1076
- id,
1077
- label: (_companies_names_id = companies.names[id]) != null ? _companies_names_id : id
1078
- };
1079
- }),
1080
- getOptionLabel: (option)=>option.label,
1081
- isOptionEqualToValue: (option, chosen)=>option.id === chosen.id,
1082
- // The query already narrowed the hits; a second, client-side
1083
- // filter on the label would hide a match whose stored name differs
1084
- // from its search key.
1085
- filterOptions: (options)=>options,
1086
- inputValue: companySearch,
1087
- onInputChange: (_event, value, reason)=>{
1088
- if (reason !== 'reset') setCompanySearch(value);
1089
- },
1090
- onChange: (_event, value)=>set('companyIds', value.map((option)=>option.id)),
1091
- loading: companies.searching,
1092
- noOptionsText: companySearch ? 'No company by that name' : 'Type to search companies',
1093
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
1094
- label: "At company",
1095
- helperText: "Any of these companies"
1096
- })),
1097
- sx: {
1098
- minWidth: 260,
1099
- flexGrow: 1,
1100
- maxWidth: 420
1101
- }
1361
+ /*#__PURE__*/ _jsx(RuleCompaniesField, {
1362
+ hits: companies.hits,
1363
+ value: chosenCompanies,
1364
+ search: companySearch,
1365
+ searching: companies.searching,
1366
+ onSearch: setCompanySearch,
1367
+ onChange: on.companyIds
1102
1368
  }),
1103
- /*#__PURE__*/ _jsx(TextField, {
1369
+ /*#__PURE__*/ _jsx(MemoTextField, {
1104
1370
  type: "number",
1105
1371
  size: "small",
1106
1372
  label: "Engaged with a campaign within (days)",
1107
1373
  helperText: "Opened or clicked one of your campaigns. Never engaged is left out.",
1108
1374
  value: draft.engagedWithinDays,
1109
- onChange: (event)=>set('engagedWithinDays', event.target.value),
1110
- sx: {
1111
- minWidth: 280
1112
- }
1375
+ onChange: on.engagedWithinDays,
1376
+ sx: atLeast(280)
1113
1377
  })
1114
1378
  ]
1115
1379
  }),
1116
- draft.custom.map((clause, index)=>{
1117
- var _clause_value, _definition_options;
1118
- const definition = definitionFor(clause.key);
1119
- const valueText = String((_clause_value = clause.value) != null ? _clause_value : '');
1120
- return /*#__PURE__*/ _jsxs(Stack, {
1121
- direction: "row",
1122
- spacing: 1,
1123
- useFlexGap: true,
1124
- sx: {
1125
- flexWrap: 'wrap',
1126
- alignItems: 'flex-start'
1127
- },
1128
- children: [
1129
- /*#__PURE__*/ _jsx(TextField, {
1130
- select: true,
1131
- size: "small",
1132
- label: "Field",
1133
- value: clause.key,
1134
- onChange: (event)=>updateClause(index, {
1135
- key: event.target.value,
1136
- value: ''
1137
- }),
1138
- sx: {
1139
- minWidth: 180
1140
- },
1141
- children: fieldOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1142
- value: option.key,
1143
- children: option.label
1144
- }, option.key))
1145
- }),
1146
- /*#__PURE__*/ _jsx(TextField, {
1147
- select: true,
1148
- size: "small",
1149
- label: "Condition",
1150
- value: clause.op,
1151
- onChange: (event)=>updateClause(index, {
1152
- op: event.target.value
1153
- }),
1154
- sx: {
1155
- minWidth: 160
1156
- },
1157
- children: DYNAMIC_LIST_CUSTOM_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
1158
- value: op,
1159
- children: CUSTOM_OP_LABELS[op]
1160
- }, op))
1161
- }),
1162
- PRESENCE_OPS.has(clause.op) ? null : (definition == null ? void 0 : definition.type) === 'select' ? /*#__PURE__*/ _jsx(TextField, {
1163
- select: true,
1164
- size: "small",
1165
- label: "Value",
1166
- value: valueText,
1167
- onChange: (event)=>updateClause(index, {
1168
- value: event.target.value
1169
- }),
1170
- sx: {
1171
- minWidth: 180
1172
- },
1173
- children: ((_definition_options = definition.options) != null ? _definition_options : []).map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1174
- value: option,
1175
- children: option
1176
- }, option))
1177
- }) : (definition == null ? void 0 : definition.type) === 'checkbox' ? /*#__PURE__*/ _jsxs(TextField, {
1178
- select: true,
1179
- size: "small",
1180
- label: "Value",
1181
- value: clause.value === true ? 'true' : clause.value === false ? 'false' : '',
1182
- onChange: (event)=>updateClause(index, {
1183
- value: event.target.value === 'true'
1184
- }),
1185
- sx: {
1186
- minWidth: 140
1187
- },
1188
- children: [
1189
- /*#__PURE__*/ _jsx(MenuItem, {
1190
- value: "true",
1191
- children: 'Checked'
1192
- }),
1193
- /*#__PURE__*/ _jsx(MenuItem, {
1194
- value: "false",
1195
- children: 'Not checked'
1196
- })
1197
- ]
1198
- }) : (definition == null ? void 0 : definition.type) === 'number' ? /*#__PURE__*/ _jsx(TextField, {
1199
- type: "number",
1200
- size: "small",
1201
- label: "Value",
1202
- value: valueText,
1203
- onChange: (event)=>{
1204
- // A number field stores a NUMBER, so the matcher compares
1205
- // 10 with 9 and not "10" with "9"; half-typed text stays
1206
- // text until it parses, rather than becoming NaN.
1207
- const text = event.target.value;
1208
- const parsed = Number(text);
1209
- updateClause(index, {
1210
- value: text.trim() !== '' && Number.isFinite(parsed) ? parsed : text
1211
- });
1212
- },
1213
- sx: {
1214
- minWidth: 140
1215
- }
1216
- }) : /*#__PURE__*/ _jsx(TextField, {
1217
- type: (definition == null ? void 0 : definition.type) === 'date' ? 'date' : 'text',
1218
- size: "small",
1219
- label: "Value",
1220
- value: valueText,
1221
- onChange: (event)=>updateClause(index, {
1222
- value: event.target.value
1223
- }),
1224
- slotProps: (definition == null ? void 0 : definition.type) === 'date' ? {
1225
- inputLabel: {
1226
- shrink: true
1227
- }
1228
- } : undefined,
1229
- sx: {
1230
- minWidth: 180
1231
- }
1232
- }),
1233
- /*#__PURE__*/ _jsx(Button, {
1234
- size: "small",
1235
- onClick: ()=>removeClause(index),
1236
- children: 'Remove'
1237
- })
1238
- ]
1239
- }, index);
1240
- }),
1380
+ draft.custom.map((clause, index)=>/*#__PURE__*/ _jsx(CustomClauseRow, {
1381
+ index: index,
1382
+ clause: clause,
1383
+ definition: definitionFor(clause.key),
1384
+ fieldMenu: fieldMenu,
1385
+ onUpdate: updateClause,
1386
+ onRemove: removeClause
1387
+ }, index)),
1241
1388
  /*#__PURE__*/ _jsxs(Box, {
1242
1389
  children: [
1243
1390
  /*#__PURE__*/ _jsx(Button, {
@@ -1250,10 +1397,7 @@ export function DynamicListRuleFields(props) {
1250
1397
  fieldDefinitions.ready && !fieldDefinitions.fields.length ? /*#__PURE__*/ _jsx(Typography, {
1251
1398
  variant: "caption",
1252
1399
  color: "text.secondary",
1253
- sx: {
1254
- display: 'block',
1255
- mt: 0.5
1256
- },
1400
+ sx: CAPTION_BLOCK,
1257
1401
  children: 'This workspace has no custom contact fields yet. Define them ' + 'under CRM → Fields to filter on them here.'
1258
1402
  }) : null
1259
1403
  ]
@@ -1267,51 +1411,41 @@ export function DynamicListRuleFields(props) {
1267
1411
  direction: "row",
1268
1412
  spacing: 1,
1269
1413
  useFlexGap: true,
1270
- sx: {
1271
- flexWrap: 'wrap'
1272
- },
1414
+ sx: WRAP,
1273
1415
  children: [
1274
- /*#__PURE__*/ _jsx(TextField, {
1416
+ /*#__PURE__*/ _jsx(MemoTextField, {
1275
1417
  type: "number",
1276
1418
  size: "small",
1277
1419
  label: "Orders at least",
1278
1420
  value: draft.ordersCountAtLeast,
1279
- onChange: (event)=>set('ordersCountAtLeast', event.target.value),
1280
- sx: {
1281
- minWidth: 160
1282
- }
1421
+ onChange: on.ordersCountAtLeast,
1422
+ sx: atLeast(160)
1283
1423
  }),
1284
- /*#__PURE__*/ _jsx(TextField, {
1424
+ /*#__PURE__*/ _jsx(MemoTextField, {
1285
1425
  type: "number",
1286
1426
  size: "small",
1287
1427
  label: "Spent at least",
1288
1428
  helperText: "Lifetime, in your store's currency",
1289
1429
  value: draft.ltvAtLeast,
1290
- onChange: (event)=>set('ltvAtLeast', event.target.value),
1291
- sx: {
1292
- minWidth: 190
1293
- }
1430
+ onChange: on.ltvAtLeast,
1431
+ sx: atLeast(190)
1294
1432
  }),
1295
- /*#__PURE__*/ _jsx(TextField, {
1433
+ /*#__PURE__*/ _jsx(MemoTextField, {
1296
1434
  type: "number",
1297
1435
  size: "small",
1298
1436
  label: "Bought within (days)",
1299
1437
  value: draft.lastPurchaseWithinDays,
1300
- onChange: (event)=>set('lastPurchaseWithinDays', event.target.value),
1301
- sx: {
1302
- minWidth: 190
1303
- }
1438
+ onChange: on.lastPurchaseWithinDays,
1439
+ sx: atLeast(190)
1304
1440
  }),
1305
- /*#__PURE__*/ _jsx(TextField, {
1441
+ /*#__PURE__*/ _jsx(MemoTextField, {
1306
1442
  type: "number",
1307
1443
  size: "small",
1308
1444
  label: "Nothing bought for (days)",
1309
1445
  helperText: "Lapsed customers — never bought is not lapsed",
1310
1446
  value: draft.noPurchaseForDays,
1311
- onChange: (event)=>set('noPurchaseForDays', event.target.value),
1312
- sx: {
1313
- minWidth: 220
1314
- }
1447
+ onChange: on.noPurchaseForDays,
1448
+ sx: atLeast(220)
1315
1449
  })
1316
1450
  ]
1317
1451
  }),
@@ -1330,51 +1464,41 @@ export function DynamicListRuleFields(props) {
1330
1464
  direction: "row",
1331
1465
  spacing: 1,
1332
1466
  useFlexGap: true,
1333
- sx: {
1334
- flexWrap: 'wrap'
1335
- },
1467
+ sx: WRAP,
1336
1468
  children: [
1337
- /*#__PURE__*/ _jsx(TextField, {
1469
+ /*#__PURE__*/ _jsx(MemoTextField, {
1338
1470
  type: "number",
1339
1471
  size: "small",
1340
1472
  label: "Opened within (days)",
1341
1473
  value: draft.openedWithinDays,
1342
- onChange: (event)=>set('openedWithinDays', event.target.value),
1343
- sx: {
1344
- minWidth: 190
1345
- }
1474
+ onChange: on.openedWithinDays,
1475
+ sx: atLeast(190)
1346
1476
  }),
1347
- /*#__PURE__*/ _jsx(TextField, {
1477
+ /*#__PURE__*/ _jsx(MemoTextField, {
1348
1478
  type: "number",
1349
1479
  size: "small",
1350
1480
  label: "Clicked within (days)",
1351
1481
  value: draft.clickedWithinDays,
1352
- onChange: (event)=>set('clickedWithinDays', event.target.value),
1353
- sx: {
1354
- minWidth: 190
1355
- }
1482
+ onChange: on.clickedWithinDays,
1483
+ sx: atLeast(190)
1356
1484
  }),
1357
- /*#__PURE__*/ _jsx(TextField, {
1485
+ /*#__PURE__*/ _jsx(MemoTextField, {
1358
1486
  type: "number",
1359
1487
  size: "small",
1360
1488
  label: "Nothing opened for (days)",
1361
1489
  helperText: "Never opened counts",
1362
1490
  value: draft.notOpenedForDays,
1363
- onChange: (event)=>set('notOpenedForDays', event.target.value),
1364
- sx: {
1365
- minWidth: 210
1366
- }
1491
+ onChange: on.notOpenedForDays,
1492
+ sx: atLeast(210)
1367
1493
  }),
1368
- /*#__PURE__*/ _jsx(TextField, {
1494
+ /*#__PURE__*/ _jsx(MemoTextField, {
1369
1495
  type: "number",
1370
1496
  size: "small",
1371
1497
  label: "Nothing clicked for (days)",
1372
1498
  helperText: "Never clicked counts",
1373
1499
  value: draft.notClickedForDays,
1374
- onChange: (event)=>set('notClickedForDays', event.target.value),
1375
- sx: {
1376
- minWidth: 210
1377
- }
1500
+ onChange: on.notClickedForDays,
1501
+ sx: atLeast(210)
1378
1502
  })
1379
1503
  ]
1380
1504
  }),
@@ -1388,61 +1512,35 @@ export function DynamicListRuleFields(props) {
1388
1512
  direction: "row",
1389
1513
  spacing: 1,
1390
1514
  useFlexGap: true,
1391
- sx: {
1392
- flexWrap: 'wrap'
1393
- },
1515
+ sx: WRAP,
1394
1516
  children: [
1395
- /*#__PURE__*/ _jsx(TextField, {
1517
+ /*#__PURE__*/ _jsx(MemoTextField, {
1396
1518
  select: true,
1397
1519
  size: "small",
1398
1520
  label: "Already on",
1399
1521
  value: draft.inListIds,
1400
- onChange: (event)=>set('inListIds', asArray(event.target.value)),
1401
- slotProps: {
1402
- select: {
1403
- multiple: true
1404
- }
1405
- },
1522
+ onChange: on.inListIds,
1523
+ slotProps: MULTIPLE,
1406
1524
  helperText: "Members of every audience picked",
1407
- sx: {
1408
- minWidth: 220
1409
- },
1410
- children: lists.map((row)=>{
1411
- var _row_name;
1412
- return /*#__PURE__*/ _jsx(MenuItem, {
1413
- value: row.$id,
1414
- children: (_row_name = row.name) != null ? _row_name : row.$id
1415
- }, row.$id);
1416
- })
1525
+ sx: atLeast(220),
1526
+ children: listMenu
1417
1527
  }),
1418
- /*#__PURE__*/ _jsx(TextField, {
1528
+ /*#__PURE__*/ _jsx(MemoTextField, {
1419
1529
  select: true,
1420
1530
  size: "small",
1421
1531
  label: "Not on",
1422
1532
  value: draft.notInListIds,
1423
- onChange: (event)=>set('notInListIds', asArray(event.target.value)),
1424
- slotProps: {
1425
- select: {
1426
- multiple: true
1427
- }
1428
- },
1533
+ onChange: on.notInListIds,
1534
+ slotProps: MULTIPLE,
1429
1535
  helperText: "Excludes members of any audience picked",
1430
- sx: {
1431
- minWidth: 220
1432
- },
1433
- children: lists.map((row)=>{
1434
- var _row_name;
1435
- return /*#__PURE__*/ _jsx(MenuItem, {
1436
- value: row.$id,
1437
- children: (_row_name = row.name) != null ? _row_name : row.$id
1438
- }, row.$id);
1439
- })
1536
+ sx: atLeast(220),
1537
+ children: listMenu
1440
1538
  })
1441
1539
  ]
1442
1540
  })
1443
1541
  ]
1444
1542
  });
1445
- }
1543
+ });
1446
1544
  DynamicListRuleFields.displayName = 'DynamicListRuleFields';
1447
1545
  /**
1448
1546
  * A draft seeded from a rule that arrives after the form does.