@aglyn/plugins-email 1.0.0-beta.143

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 (144) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +58 -0
  4. package/src/index.d.ts +35 -0
  5. package/src/index.js +35 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
  8. package/src/lib/components/campaign-design-create-widget.js +61 -0
  9. package/src/lib/components/campaign-design-create-widget.js.map +1 -0
  10. package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
  11. package/src/lib/components/campaign-sender-editor-widget.js +39 -0
  12. package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
  13. package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
  14. package/src/lib/components/campaign-topic-options-widget.js +48 -0
  15. package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
  16. package/src/lib/components/campaign-topic-select.d.ts +32 -0
  17. package/src/lib/components/campaign-topic-select.js +87 -0
  18. package/src/lib/components/campaign-topic-select.js.map +1 -0
  19. package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
  20. package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
  21. package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
  22. package/src/lib/components/email-blocks.d.ts +111 -0
  23. package/src/lib/components/email-blocks.js +875 -0
  24. package/src/lib/components/email-blocks.js.map +1 -0
  25. package/src/lib/components/email-design-preview.d.ts +62 -0
  26. package/src/lib/components/email-design-preview.js +174 -0
  27. package/src/lib/components/email-design-preview.js.map +1 -0
  28. package/src/lib/components/email-screens-card.d.ts +42 -0
  29. package/src/lib/components/email-screens-card.js +277 -0
  30. package/src/lib/components/email-screens-card.js.map +1 -0
  31. package/src/lib/components/email-template-detail.d.ts +48 -0
  32. package/src/lib/components/email-template-detail.js +681 -0
  33. package/src/lib/components/email-template-detail.js.map +1 -0
  34. package/src/lib/components/email-topic-detail.d.ts +32 -0
  35. package/src/lib/components/email-topic-detail.js +293 -0
  36. package/src/lib/components/email-topic-detail.js.map +1 -0
  37. package/src/lib/components/email-topics-card.d.ts +46 -0
  38. package/src/lib/components/email-topics-card.js +327 -0
  39. package/src/lib/components/email-topics-card.js.map +1 -0
  40. package/src/lib/components/email-zones.d.ts +28 -0
  41. package/src/lib/components/email-zones.js +20 -0
  42. package/src/lib/components/email-zones.js.map +1 -0
  43. package/src/lib/components/emails-console-page.d.ts +32 -0
  44. package/src/lib/components/emails-console-page.js +229 -0
  45. package/src/lib/components/emails-console-page.js.map +1 -0
  46. package/src/lib/components/emails-console-sections.d.ts +36 -0
  47. package/src/lib/components/emails-console-sections.js +108 -0
  48. package/src/lib/components/emails-console-sections.js.map +1 -0
  49. package/src/lib/components/list-detail-card.d.ts +47 -0
  50. package/src/lib/components/list-detail-card.js +273 -0
  51. package/src/lib/components/list-detail-card.js.map +1 -0
  52. package/src/lib/components/list-edit-card.d.ts +11 -0
  53. package/src/lib/components/list-edit-card.js +287 -0
  54. package/src/lib/components/list-edit-card.js.map +1 -0
  55. package/src/lib/components/list-import-drawer.d.ts +22 -0
  56. package/src/lib/components/list-import-drawer.js +662 -0
  57. package/src/lib/components/list-import-drawer.js.map +1 -0
  58. package/src/lib/components/list-members-panel.d.ts +94 -0
  59. package/src/lib/components/list-members-panel.js +686 -0
  60. package/src/lib/components/list-members-panel.js.map +1 -0
  61. package/src/lib/components/lists-card.d.ts +28 -0
  62. package/src/lib/components/lists-card.js +377 -0
  63. package/src/lib/components/lists-card.js.map +1 -0
  64. package/src/lib/components/sending-domain-detail.d.ts +26 -0
  65. package/src/lib/components/sending-domain-detail.js +496 -0
  66. package/src/lib/components/sending-domain-detail.js.map +1 -0
  67. package/src/lib/components/sending-domains-card.d.ts +33 -0
  68. package/src/lib/components/sending-domains-card.js +962 -0
  69. package/src/lib/components/sending-domains-card.js.map +1 -0
  70. package/src/lib/components/sending-sender-drawer.d.ts +94 -0
  71. package/src/lib/components/sending-sender-drawer.js +543 -0
  72. package/src/lib/components/sending-sender-drawer.js.map +1 -0
  73. package/src/lib/components/suppressions-card.d.ts +49 -0
  74. package/src/lib/components/suppressions-card.js +639 -0
  75. package/src/lib/components/suppressions-card.js.map +1 -0
  76. package/src/lib/components/use-org-email-topics.d.ts +79 -0
  77. package/src/lib/components/use-org-email-topics.js +111 -0
  78. package/src/lib/components/use-org-email-topics.js.map +1 -0
  79. package/src/lib/constants/bundle-common.d.ts +18 -0
  80. package/src/lib/constants/bundle-common.js +18 -0
  81. package/src/lib/constants/bundle-common.js.map +1 -0
  82. package/src/lib/hooks/use-org-company-options.d.ts +20 -0
  83. package/src/lib/hooks/use-org-company-options.js +138 -0
  84. package/src/lib/hooks/use-org-company-options.js.map +1 -0
  85. package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
  86. package/src/lib/hooks/use-org-contact-fields.js +91 -0
  87. package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
  88. package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
  89. package/src/lib/hooks/use-org-contact-segments.js +55 -0
  90. package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
  91. package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
  92. package/src/lib/hooks/use-org-crm-views.js +74 -0
  93. package/src/lib/hooks/use-org-crm-views.js.map +1 -0
  94. package/src/lib/hooks/use-org-lists.d.ts +8 -0
  95. package/src/lib/hooks/use-org-lists.js +47 -0
  96. package/src/lib/hooks/use-org-lists.js.map +1 -0
  97. package/src/lib/model/email-design-document.d.ts +52 -0
  98. package/src/lib/model/email-design-document.js +62 -0
  99. package/src/lib/model/email-design-document.js.map +1 -0
  100. package/src/lib/model/index.d.ts +64 -0
  101. package/src/lib/model/index.js +71 -0
  102. package/src/lib/model/index.js.map +1 -0
  103. package/src/lib/model/sending-domain-status.d.ts +99 -0
  104. package/src/lib/model/sending-domain-status.js +196 -0
  105. package/src/lib/model/sending-domain-status.js.map +1 -0
  106. package/src/lib/model/template-provenance.d.ts +113 -0
  107. package/src/lib/model/template-provenance.js +107 -0
  108. package/src/lib/model/template-provenance.js.map +1 -0
  109. package/src/lib/model/template-report.d.ts +158 -0
  110. package/src/lib/model/template-report.js +249 -0
  111. package/src/lib/model/template-report.js.map +1 -0
  112. package/src/lib/plugin.d.ts +27 -0
  113. package/src/lib/plugin.js +163 -0
  114. package/src/lib/plugin.js.map +1 -0
  115. package/src/lib/server-console.d.ts +116 -0
  116. package/src/lib/server-console.js +422 -0
  117. package/src/lib/server-console.js.map +1 -0
  118. package/src/lib/server-email-drafts.d.ts +104 -0
  119. package/src/lib/server-email-drafts.js +381 -0
  120. package/src/lib/server-email-drafts.js.map +1 -0
  121. package/src/lib/server-list-gate.d.ts +183 -0
  122. package/src/lib/server-list-gate.js +365 -0
  123. package/src/lib/server-list-gate.js.map +1 -0
  124. package/src/lib/server-list-import.d.ts +199 -0
  125. package/src/lib/server-list-import.js +632 -0
  126. package/src/lib/server-list-import.js.map +1 -0
  127. package/src/lib/server-suppressions.d.ts +135 -0
  128. package/src/lib/server-suppressions.js +295 -0
  129. package/src/lib/server-suppressions.js.map +1 -0
  130. package/src/lib/server.d.ts +19 -0
  131. package/src/lib/server.js +834 -0
  132. package/src/lib/server.js.map +1 -0
  133. package/src/lib/site.d.ts +26 -0
  134. package/src/lib/site.js +81 -0
  135. package/src/lib/site.js.map +1 -0
  136. package/src/lib/unsubscribe-link.d.ts +311 -0
  137. package/src/lib/unsubscribe-link.js +398 -0
  138. package/src/lib/unsubscribe-link.js.map +1 -0
  139. package/src/lib/utils/create-email-screen.d.ts +59 -0
  140. package/src/lib/utils/create-email-screen.js +59 -0
  141. package/src/lib/utils/create-email-screen.js.map +1 -0
  142. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  143. package/src/lib/utils/generate-preset-id.js +25 -0
  144. package/src/lib/utils/generate-preset-id.js.map +1 -0
@@ -0,0 +1,1473 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ /**
20
+ * Every field of `DynamicListRule`, with a control for each.
21
+ *
22
+ * ## Why the whole rule, and why in one place
23
+ *
24
+ * Every dimension the matcher reads has a control here, including the two
25
+ * that are answered by a keyed lookup rather than by the person's own record
26
+ * — engagement, and membership of another audience — and the combinator that
27
+ * decides whether the filters are ANDed, ORed or inverted. Needing to
28
+ * hand-write a stored structure to reach a shipped engine is a missing
29
+ * picker, not a power-user affordance, which is the standard this form is
30
+ * held to rather than a count of fields.
31
+ *
32
+ * One component rather than a form on the create card and a second on the edit
33
+ * page: the two would drift, and the half that drifts is whichever the
34
+ * merchant is not looking at. A rule authored at creation and the same rule
35
+ * reopened for editing are the same questions.
36
+ *
37
+ * ## The draft is text, the value is a rule
38
+ *
39
+ * The controls hold what was TYPED — `vip, ` is a legitimate intermediate
40
+ * state of a tag list, and a component that round-tripped every keystroke
41
+ * through `normalizeDynamicListRule` would delete the comma the moment it was
42
+ * typed. So the draft is strings, `draftToRule` is the one place it becomes a
43
+ * rule, and the parent never sees anything else.
44
+ *
45
+ * ## Money is entered the way a merchant says it
46
+ *
47
+ * `ltvCentsAtLeast` is stored in cents. A field labeled for the stored unit
48
+ * turns "customers who spent over 500" into customers who spent over five
49
+ * dollars, silently, and the audience looks plausible either way. The control
50
+ * takes whole currency units and this file owns the only multiplication.
51
+ *
52
+ * ## It reads the segments, and only when it is on screen
53
+ *
54
+ * The segment picker needs the org's saved segments, through
55
+ * `useOrgContactSegments`. That listen belongs to this component so it opens
56
+ * when a rule is actually being authored — the audiences section's cost is a
57
+ * list of lists, and a segment read on arrival would be a read for a form
58
+ * nobody has opened.
59
+ */ import { CONTACT_LIFECYCLE_STAGES, CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_SOURCE_LABELS, DYNAMIC_LIST_CUSTOM_OPS, normalizeDynamicListRule } from "@aglyn/aglyn";
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";
64
+ import { useOrgCompanyOptions } from "../hooks/use-org-company-options.js";
65
+ import { useOrgContactFields } from "../hooks/use-org-contact-fields.js";
66
+ import { useOrgContactSegments } from "../hooks/use-org-contact-segments.js";
67
+ import { useOrgCrmViews } from "../hooks/use-org-crm-views.js";
68
+ import { useOrgLists } from "../hooks/use-org-lists.js";
69
+ /** How each silo reads on screen. */ const SOURCE_LABELS = {
70
+ contacts: 'Contacts',
71
+ leads: 'Leads',
72
+ siteMembers: 'Site members',
73
+ formSubmissions: 'Form submissions'
74
+ };
75
+ /** The same silos, lower-cased for the middle of a sentence. */ const SOURCE_PHRASES = {
76
+ contacts: 'contacts',
77
+ leads: 'leads',
78
+ siteMembers: 'site members',
79
+ formSubmissions: 'form submissions'
80
+ };
81
+ /** A day, in the same UTC calendar the rule's boundaries are stored against. */ const dayLabel = (ms)=>new Date(ms).toISOString().slice(0, 10);
82
+ /** An id, as its name when one is known and as itself when none is. */ const named = (id, names)=>(names == null ? void 0 : names[id]) || id;
83
+ /**
84
+ * How each custom-field operator reads, as the verb of a clause.
85
+ *
86
+ * `neq` carries its lean in the sentence: the matcher requires a VALUE that
87
+ * differs, so a merchant excluding one plan does not sweep in everyone whose
88
+ * plan was never recorded — and a reader checking a paragraph against their
89
+ * intent cannot be expected to know that from the word "not".
90
+ */ const CUSTOM_OP_LABELS = {
91
+ eq: 'is',
92
+ neq: 'is not',
93
+ contains: 'contains',
94
+ gt: 'is more than',
95
+ lt: 'is less than',
96
+ set: 'is set',
97
+ unset: 'is not set'
98
+ };
99
+ /** The operators that take no value — the clause is about presence. */ const PRESENCE_OPS = new Set([
100
+ 'set',
101
+ 'unset'
102
+ ]);
103
+ /** One custom clause as a sentence fragment: `Plan is enterprise`. */ const describeCustomClause = (clause, names)=>{
104
+ var _clause_value;
105
+ const field = named(clause.key, names == null ? void 0 : names.fields);
106
+ const verb = CUSTOM_OP_LABELS[clause.op];
107
+ if (clause.op === 'set' || clause.op === 'unset') return `${field} ${verb}`;
108
+ const value = String((_clause_value = clause.value) != null ? _clause_value : '');
109
+ return clause.op === 'neq' ? `${field} ${verb} ${value} (a blank does not count)` : `${field} ${verb} ${value}`;
110
+ };
111
+ /** One AND-block, as clauses. The rule's own block and each branch share it. */ function describeDimensions(rule, names) {
112
+ var _rule_custom;
113
+ var _rule_tags, _rule_captureSources, _rule_formNames, _rule_campaignIds, _rule_inListIds, _rule_notInListIds, _rule_ownerUids, _rule_lifecycleStages, _rule_companyIds;
114
+ const clauses = [];
115
+ if ((_rule_tags = rule.tags) == null ? void 0 : _rule_tags.length) {
116
+ clauses.push(`Tagged any of: ${rule.tags.join(', ')}.`);
117
+ }
118
+ if ((_rule_captureSources = rule.captureSources) == null ? void 0 : _rule_captureSources.length) {
119
+ clauses.push(`Captured by: ${rule.captureSources.map((source)=>{
120
+ var _CONTACT_SOURCE_LABELS_source;
121
+ return (_CONTACT_SOURCE_LABELS_source = CONTACT_SOURCE_LABELS[source]) != null ? _CONTACT_SOURCE_LABELS_source : source;
122
+ }).join(', ')}.`);
123
+ }
124
+ if ((_rule_formNames = rule.formNames) == null ? void 0 : _rule_formNames.length) {
125
+ clauses.push(`Submitted any of: ${rule.formNames.join(', ')}.`);
126
+ }
127
+ /*
128
+ * The clause names the SILOS the dimension applies to, like every other
129
+ * silo-specific clause here. A lead and a site member carry no campaign, so
130
+ * the sentence has to say that the filter passes them through rather than
131
+ * leaving a reader to conclude that a rule drawing from leads and naming a
132
+ * campaign selects no leads.
133
+ */ if ((_rule_campaignIds = rule.campaignIds) == null ? void 0 : _rule_campaignIds.length) {
134
+ clauses.push(`Filed under any of these campaigns: ${rule.campaignIds.map((id)=>named(id, names == null ? void 0 : names.campaigns)).join(', ')} — contacts and form submissions only.`);
135
+ }
136
+ if (rule.createdAfterMs !== undefined) {
137
+ clauses.push(`Created on or after ${dayLabel(rule.createdAfterMs)}.`);
138
+ }
139
+ if (rule.createdBeforeMs !== undefined) {
140
+ clauses.push(`Created before ${dayLabel(rule.createdBeforeMs)}.`);
141
+ }
142
+ const behavior = rule.behavior;
143
+ if ((behavior == null ? void 0 : behavior.ordersCountAtLeast) !== undefined) {
144
+ clauses.push(`At least ${behavior.ordersCountAtLeast} orders.`);
145
+ }
146
+ if ((behavior == null ? void 0 : behavior.ltvCentsAtLeast) !== undefined) {
147
+ clauses.push(`Spent at least ${behavior.ltvCentsAtLeast / 100}.`);
148
+ }
149
+ if ((behavior == null ? void 0 : behavior.lastPurchaseWithinDays) !== undefined) {
150
+ clauses.push(`Bought within the last ${behavior.lastPurchaseWithinDays} days.`);
151
+ }
152
+ if ((behavior == null ? void 0 : behavior.noPurchaseForDays) !== undefined) {
153
+ clauses.push(`Has bought nothing for ${behavior.noPurchaseForDays} days.`);
154
+ }
155
+ /*
156
+ * The quiet arms say "or never" out loud.
157
+ *
158
+ * The engine counts a person with no open on record as having not opened,
159
+ * which is the opposite lean from `noPurchaseForDays` two clauses above —
160
+ * and a reader checking a paragraph against their intent cannot be expected
161
+ * to remember which dimension leans which way. So the sentence carries it.
162
+ */ const engagement = rule.engagement;
163
+ if ((engagement == null ? void 0 : engagement.openedWithinDays) !== undefined) {
164
+ clauses.push(`Opened one of your emails in the last ${engagement.openedWithinDays} days.`);
165
+ }
166
+ if ((engagement == null ? void 0 : engagement.clickedWithinDays) !== undefined) {
167
+ clauses.push(`Clicked a link in one of your emails in the last ` + `${engagement.clickedWithinDays} days.`);
168
+ }
169
+ if ((engagement == null ? void 0 : engagement.notOpenedForDays) !== undefined) {
170
+ clauses.push(`Has opened nothing for ${engagement.notOpenedForDays} days, or never ` + `opened anything.`);
171
+ }
172
+ if ((engagement == null ? void 0 : engagement.notClickedForDays) !== undefined) {
173
+ clauses.push(`Has clicked nothing for ${engagement.notClickedForDays} days, or never ` + `clicked anything.`);
174
+ }
175
+ if ((_rule_inListIds = rule.inListIds) == null ? void 0 : _rule_inListIds.length) {
176
+ clauses.push(`Already on: ${rule.inListIds.map((id)=>named(id, names == null ? void 0 : names.lists)).join(', ')}.`);
177
+ }
178
+ if ((_rule_notInListIds = rule.notInListIds) == null ? void 0 : _rule_notInListIds.length) {
179
+ clauses.push(`Not on: ${rule.notInListIds.map((id)=>named(id, names == null ? void 0 : names.lists)).join(', ')}.`);
180
+ }
181
+ /*
182
+ * THE CRM DIMENSIONS (AGL-2603). Each names people, stages and companies
183
+ * the way the pickers show them, never by id: a clause reading `uid-8f2a`
184
+ * is a clause nobody can check against their intent. The custom clauses
185
+ * are one sentence each, because each is its own condition and a reader
186
+ * adding a second one has narrowed the audience by exactly one sentence.
187
+ */ if ((_rule_ownerUids = rule.ownerUids) == null ? void 0 : _rule_ownerUids.length) {
188
+ clauses.push(`Owned by: ${rule.ownerUids.map((uid)=>named(uid, names == null ? void 0 : names.members)).join(', ')}.`);
189
+ }
190
+ if ((_rule_lifecycleStages = rule.lifecycleStages) == null ? void 0 : _rule_lifecycleStages.length) {
191
+ clauses.push(`In stage: ${rule.lifecycleStages.map((stage)=>{
192
+ var _CONTACT_LIFECYCLE_STAGE_LABELS_stage;
193
+ return (_CONTACT_LIFECYCLE_STAGE_LABELS_stage = CONTACT_LIFECYCLE_STAGE_LABELS[stage]) != null ? _CONTACT_LIFECYCLE_STAGE_LABELS_stage : stage;
194
+ }).join(', ')}.`);
195
+ }
196
+ if ((_rule_companyIds = rule.companyIds) == null ? void 0 : _rule_companyIds.length) {
197
+ clauses.push(`At company: ${rule.companyIds.map((id)=>named(id, names == null ? void 0 : names.companies)).join(', ')}.`);
198
+ }
199
+ /*
200
+ * "One of your campaigns", against the engagement arms above that say
201
+ * "one of your emails": those count every message this workspace sent the
202
+ * address, this counts the campaigns the reading site's group sent — the
203
+ * sentence is the only place a reader can tell the two windows apart.
204
+ */ if (rule.engagedWithinDays !== undefined) {
205
+ clauses.push(`Opened or clicked one of your campaigns in the last ` + `${rule.engagedWithinDays} days.`);
206
+ }
207
+ for (const clause of (_rule_custom = rule.custom) != null ? _rule_custom : []){
208
+ clauses.push(`${describeCustomClause(clause, names)}.`);
209
+ }
210
+ return clauses;
211
+ }
212
+ export function describeDynamicListRule(rule, names) {
213
+ var _rule_any;
214
+ const clauses = [];
215
+ clauses.push(rule.sources.length ? `Draws from ${rule.sources.map((source)=>{
216
+ var _SOURCE_PHRASES_source;
217
+ return (_SOURCE_PHRASES_source = SOURCE_PHRASES[source]) != null ? _SOURCE_PHRASES_source : source;
218
+ }).join(', ')}.` : 'Draws from nothing, so it matches nobody.');
219
+ if (rule.segmentId) {
220
+ clauses.push(`Reuses saved segment ${named(rule.segmentId, names == null ? void 0 : names.segments)}.`);
221
+ }
222
+ if (rule.viewId) {
223
+ clauses.push(`Reuses saved view ${named(rule.viewId, names == null ? void 0 : names.views)}.`);
224
+ }
225
+ const own = describeDimensions(rule, names);
226
+ if (rule.negate && own.length) {
227
+ /*
228
+ * A negated block is ONE clause, not a list of them.
229
+ *
230
+ * "Excludes anyone who is tagged vip. Excludes anyone who has spent over
231
+ * 500." would read as two independent exclusions, and the rule is one:
232
+ * it excludes people who are BOTH. Somebody checking a paragraph against
233
+ * their intent would come away with the wrong audience.
234
+ */ clauses.push(`Excludes anyone matching all of: ${own.map((clause)=>clause.replace(/\.$/, '')).join('; ')}.`);
235
+ } else {
236
+ clauses.push(...own);
237
+ }
238
+ /*
239
+ * The OR branches are ONE clause covering all of them, not one clause each.
240
+ *
241
+ * A candidate has to satisfy at least one branch — so "at least one of"
242
+ * printed once per branch would read as a separate requirement per branch,
243
+ * which is the AND this operator exists to escape. The whole disjunction is
244
+ * therefore rendered as a single sentence, with each branch's own filters
245
+ * joined by "and" inside it.
246
+ */ const branches = ((_rule_any = rule.any) != null ? _rule_any : []).map((group)=>{
247
+ const inner = describeDimensions(group, names);
248
+ if (!inner.length) return '';
249
+ const joined = inner.map((clause)=>clause.replace(/\.$/, '').toLowerCase()).join(' and ');
250
+ return group.negate ? `not (${joined})` : joined;
251
+ }).filter(Boolean);
252
+ if (branches.length) {
253
+ clauses.push(`And at least one of these: ${branches.join('; or ')}.`);
254
+ }
255
+ return clauses;
256
+ }
257
+ export const EMPTY_RULE_DRAFT = {
258
+ sources: [
259
+ 'contacts'
260
+ ],
261
+ match: 'all',
262
+ segmentId: '',
263
+ viewId: '',
264
+ tags: '',
265
+ captureSources: [],
266
+ formNames: '',
267
+ campaignIds: [],
268
+ createdAfter: '',
269
+ createdBefore: '',
270
+ ordersCountAtLeast: '',
271
+ ltvAtLeast: '',
272
+ lastPurchaseWithinDays: '',
273
+ noPurchaseForDays: '',
274
+ openedWithinDays: '',
275
+ clickedWithinDays: '',
276
+ notOpenedForDays: '',
277
+ notClickedForDays: '',
278
+ inListIds: [],
279
+ notInListIds: [],
280
+ ownerUids: [],
281
+ lifecycleStages: [],
282
+ companyIds: [],
283
+ custom: [],
284
+ engagedWithinDays: ''
285
+ };
286
+ /** Comma-separated free text → the trimmed, non-empty values. */ export const splitRuleList = (value)=>value.split(',').map((entry)=>entry.trim()).filter(Boolean);
287
+ /**
288
+ * A day stamp for a date input, in UTC.
289
+ *
290
+ * `Date.parse('2026-01-01')` is UTC midnight, so reading the value back
291
+ * through the local calendar would move the boundary by the reader's offset —
292
+ * a rule saved in Los Angeles would reopen naming the previous day, and saving
293
+ * it again would walk it backwards once per edit.
294
+ */ const dayStamp = (ms)=>ms === undefined || !Number.isFinite(ms) ? '' : dayLabel(ms);
295
+ /**
296
+ * A stored rule, as the controls should show it.
297
+ *
298
+ * ⚠️ The three shapes {@link DynamicListRuleMatch} names are the shapes this
299
+ * form AUTHORS. A rule written through the REST API can hold branches the
300
+ * controls have no place for — a branch with its own `negate`, or top-level
301
+ * filters alongside branches — and this reads such a rule back as the union
302
+ * of every filter in it, in `any` mode. It is not a silent rewrite: the
303
+ * sentences above the controls are computed from `draftToRule(draft)`, so
304
+ * what a save would store is on screen before the reader saves it.
305
+ */ export function ruleToDraft(rule) {
306
+ var _rule_any, _rule_sources, _rule_segmentId, _rule_viewId;
307
+ var _rule_any1;
308
+ // Every filter the rule carries, wherever it carries it. The branches are
309
+ // read for their VALUES; how they combined is answered by `match` below.
310
+ const blocks = [
311
+ rule,
312
+ ...(_rule_any = rule.any) != null ? _rule_any : []
313
+ ];
314
+ const first = (read)=>{
315
+ for (const block of blocks){
316
+ const value = read(block);
317
+ if (value !== undefined) return value;
318
+ }
319
+ return undefined;
320
+ };
321
+ const list = (read)=>{
322
+ var _read;
323
+ const found = new Set();
324
+ for (const block of blocks)for (const entry of (_read = read(block)) != null ? _read : [])found.add(entry);
325
+ return [
326
+ ...found
327
+ ];
328
+ };
329
+ const number = (read)=>{
330
+ const value = first(read);
331
+ return value === undefined ? '' : String(value);
332
+ };
333
+ const ltvCents = first((block)=>{
334
+ var _block_behavior;
335
+ return (_block_behavior = block.behavior) == null ? void 0 : _block_behavior.ltvCentsAtLeast;
336
+ });
337
+ return {
338
+ sources: (_rule_sources = rule.sources) != null ? _rule_sources : [],
339
+ match: ((_rule_any1 = rule.any) == null ? void 0 : _rule_any1.length) ? 'any' : rule.negate ? 'none' : 'all',
340
+ segmentId: (_rule_segmentId = rule.segmentId) != null ? _rule_segmentId : '',
341
+ viewId: (_rule_viewId = rule.viewId) != null ? _rule_viewId : '',
342
+ tags: list((block)=>block.tags).join(', '),
343
+ captureSources: list((block)=>block.captureSources),
344
+ formNames: list((block)=>block.formNames).join(', '),
345
+ campaignIds: list((block)=>block.campaignIds),
346
+ createdAfter: dayStamp(first((block)=>block.createdAfterMs)),
347
+ createdBefore: dayStamp(first((block)=>block.createdBeforeMs)),
348
+ ordersCountAtLeast: number((block)=>{
349
+ var _block_behavior;
350
+ return (_block_behavior = block.behavior) == null ? void 0 : _block_behavior.ordersCountAtLeast;
351
+ }),
352
+ ltvAtLeast: ltvCents === undefined ? '' : String(ltvCents / 100),
353
+ lastPurchaseWithinDays: number((block)=>{
354
+ var _block_behavior;
355
+ return (_block_behavior = block.behavior) == null ? void 0 : _block_behavior.lastPurchaseWithinDays;
356
+ }),
357
+ noPurchaseForDays: number((block)=>{
358
+ var _block_behavior;
359
+ return (_block_behavior = block.behavior) == null ? void 0 : _block_behavior.noPurchaseForDays;
360
+ }),
361
+ openedWithinDays: number((block)=>{
362
+ var _block_engagement;
363
+ return (_block_engagement = block.engagement) == null ? void 0 : _block_engagement.openedWithinDays;
364
+ }),
365
+ clickedWithinDays: number((block)=>{
366
+ var _block_engagement;
367
+ return (_block_engagement = block.engagement) == null ? void 0 : _block_engagement.clickedWithinDays;
368
+ }),
369
+ notOpenedForDays: number((block)=>{
370
+ var _block_engagement;
371
+ return (_block_engagement = block.engagement) == null ? void 0 : _block_engagement.notOpenedForDays;
372
+ }),
373
+ notClickedForDays: number((block)=>{
374
+ var _block_engagement;
375
+ return (_block_engagement = block.engagement) == null ? void 0 : _block_engagement.notClickedForDays;
376
+ }),
377
+ inListIds: list((block)=>block.inListIds),
378
+ notInListIds: list((block)=>block.notInListIds),
379
+ ownerUids: list((block)=>block.ownerUids),
380
+ lifecycleStages: list((block)=>block.lifecycleStages),
381
+ companyIds: list((block)=>block.companyIds),
382
+ engagedWithinDays: number((block)=>block.engagedWithinDays),
383
+ // Clauses are objects, so the string-set dedupe above cannot hold them;
384
+ // two blocks carrying the same clause are one condition, keyed on its
385
+ // whole shape.
386
+ custom: (()=>{
387
+ const seen = new Map();
388
+ for (const block of blocks){
389
+ var _block_custom;
390
+ for (const clause of (_block_custom = block.custom) != null ? _block_custom : []){
391
+ seen.set(JSON.stringify(clause), clause);
392
+ }
393
+ }
394
+ return [
395
+ ...seen.values()
396
+ ];
397
+ })()
398
+ };
399
+ }
400
+ /**
401
+ * A typed number, or nothing at all.
402
+ *
403
+ * An empty box is not zero. `Number('')` is `0`, and a rule normalizer that
404
+ * accepts zero would turn every field the merchant left alone into an active
405
+ * filter — `noPurchaseForDays: 0` matches nobody, so an untouched form would
406
+ * quietly produce an empty audience.
407
+ */ const typedNumber = (value)=>{
408
+ const trimmed = value.trim();
409
+ if (!trimmed) return undefined;
410
+ const parsed = Number(trimmed);
411
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
412
+ };
413
+ /**
414
+ * One filter of the draft, as the smallest block it can live in.
415
+ *
416
+ * `any` mode puts each of these in its own OR branch, so this list is what
417
+ * decides how finely "any of these filters" is read: per CONTROL, which is
418
+ * what a reader who just typed into eleven boxes means by "any of them". A
419
+ * coarser split — the whole purchase-history block as one branch — would ask
420
+ * them to satisfy three purchase conditions together to match the branch.
421
+ */ function draftDimensions(draft) {
422
+ const blocks = [];
423
+ const tags = splitRuleList(draft.tags);
424
+ if (tags.length) blocks.push({
425
+ tags
426
+ });
427
+ if (draft.captureSources.length) {
428
+ blocks.push({
429
+ captureSources: draft.captureSources
430
+ });
431
+ }
432
+ const formNames = splitRuleList(draft.formNames);
433
+ if (formNames.length) blocks.push({
434
+ formNames
435
+ });
436
+ if (draft.campaignIds.length) blocks.push({
437
+ campaignIds: draft.campaignIds
438
+ });
439
+ const createdAfterMs = draft.createdAfter ? Date.parse(draft.createdAfter) : undefined;
440
+ if (createdAfterMs !== undefined && Number.isFinite(createdAfterMs)) {
441
+ blocks.push({
442
+ createdAfterMs
443
+ });
444
+ }
445
+ const createdBeforeMs = draft.createdBefore ? Date.parse(draft.createdBefore) : undefined;
446
+ if (createdBeforeMs !== undefined && Number.isFinite(createdBeforeMs)) {
447
+ blocks.push({
448
+ createdBeforeMs
449
+ });
450
+ }
451
+ const ordersCountAtLeast = typedNumber(draft.ordersCountAtLeast);
452
+ if (ordersCountAtLeast !== undefined) {
453
+ blocks.push({
454
+ behavior: {
455
+ ordersCountAtLeast
456
+ }
457
+ });
458
+ }
459
+ // Whole units in, cents out — rounded, because a half-cent threshold is not
460
+ // a quantity any order total can be compared against.
461
+ const ltv = typedNumber(draft.ltvAtLeast);
462
+ if (ltv !== undefined) {
463
+ blocks.push({
464
+ behavior: {
465
+ ltvCentsAtLeast: Math.round(ltv * 100)
466
+ }
467
+ });
468
+ }
469
+ const lastPurchaseWithinDays = typedNumber(draft.lastPurchaseWithinDays);
470
+ if (lastPurchaseWithinDays !== undefined) {
471
+ blocks.push({
472
+ behavior: {
473
+ lastPurchaseWithinDays
474
+ }
475
+ });
476
+ }
477
+ const noPurchaseForDays = typedNumber(draft.noPurchaseForDays);
478
+ if (noPurchaseForDays !== undefined) {
479
+ blocks.push({
480
+ behavior: {
481
+ noPurchaseForDays
482
+ }
483
+ });
484
+ }
485
+ const openedWithinDays = typedNumber(draft.openedWithinDays);
486
+ if (openedWithinDays !== undefined) {
487
+ blocks.push({
488
+ engagement: {
489
+ openedWithinDays
490
+ }
491
+ });
492
+ }
493
+ const clickedWithinDays = typedNumber(draft.clickedWithinDays);
494
+ if (clickedWithinDays !== undefined) {
495
+ blocks.push({
496
+ engagement: {
497
+ clickedWithinDays
498
+ }
499
+ });
500
+ }
501
+ const notOpenedForDays = typedNumber(draft.notOpenedForDays);
502
+ if (notOpenedForDays !== undefined) {
503
+ blocks.push({
504
+ engagement: {
505
+ notOpenedForDays
506
+ }
507
+ });
508
+ }
509
+ const notClickedForDays = typedNumber(draft.notClickedForDays);
510
+ if (notClickedForDays !== undefined) {
511
+ blocks.push({
512
+ engagement: {
513
+ notClickedForDays
514
+ }
515
+ });
516
+ }
517
+ if (draft.inListIds.length) blocks.push({
518
+ inListIds: draft.inListIds
519
+ });
520
+ if (draft.notInListIds.length) {
521
+ blocks.push({
522
+ notInListIds: draft.notInListIds
523
+ });
524
+ }
525
+ if (draft.ownerUids.length) blocks.push({
526
+ ownerUids: draft.ownerUids
527
+ });
528
+ if (draft.lifecycleStages.length) {
529
+ blocks.push({
530
+ lifecycleStages: draft.lifecycleStages
531
+ });
532
+ }
533
+ if (draft.companyIds.length) blocks.push({
534
+ companyIds: draft.companyIds
535
+ });
536
+ const engagedWithinDays = typedNumber(draft.engagedWithinDays);
537
+ if (engagedWithinDays !== undefined) blocks.push({
538
+ engagedWithinDays
539
+ });
540
+ // One block PER CLAUSE, so that "any one of the filters below" reads each
541
+ // condition row as a filter of its own — the same grain the purchase
542
+ // figures get. `mergeDimensions` folds them back into one list for `all`.
543
+ // An unfinished row (no value where one is compared) is dropped by the
544
+ // normalizer at the end, so it is never a condition until it is one.
545
+ for (const clause of draft.custom)blocks.push({
546
+ custom: [
547
+ clause
548
+ ]
549
+ });
550
+ return blocks;
551
+ }
552
+ /** The blocks merged into one AND-block, for `all` and `none`. */ function mergeDimensions(blocks) {
553
+ const merged = {};
554
+ for (const block of blocks){
555
+ for (const [key, value] of Object.entries(block)){
556
+ var _merged_key, _merged_key1;
557
+ // `behavior` and `engagement` arrive one leaf at a time, so they are
558
+ // merged at depth. A shallow assign would keep only the last leaf of
559
+ // each and silently drop the other three. `custom` arrives one clause
560
+ // at a time for the same reason and is concatenated for the same one.
561
+ merged[key] = key === 'behavior' || key === 'engagement' ? _extends({}, (_merged_key = merged[key]) != null ? _merged_key : {}, value) : key === 'custom' ? [
562
+ ...(_merged_key1 = merged[key]) != null ? _merged_key1 : [],
563
+ ...value
564
+ ] : value;
565
+ }
566
+ }
567
+ return merged;
568
+ }
569
+ /** What the controls mean, coerced by the function the matcher reads through. */ export function draftToRule(draft) {
570
+ const blocks = draftDimensions(draft);
571
+ /*
572
+ * Normalized on the way IN by the same function the materializer reads it
573
+ * back through. A rule coerced on the way out but not on the way in is a
574
+ * rule the console can display differently from the way it evaluates.
575
+ */ return normalizeDynamicListRule(_extends({
576
+ sources: draft.sources
577
+ }, draft.segmentId ? {
578
+ segmentId: draft.segmentId
579
+ } : {}, draft.viewId ? {
580
+ viewId: draft.viewId
581
+ } : {}, draft.match === 'any' ? // is no constraint — so the rule is `sources AND (one of these)`.
582
+ {
583
+ any: blocks
584
+ } : _extends({}, mergeDimensions(blocks), draft.match === 'none' && blocks.length ? {
585
+ negate: true
586
+ } : {})));
587
+ }
588
+ export function DynamicListRuleFields(props) {
589
+ var _team_error;
590
+ const { scope, hostId, draft, onChange, listId } = props;
591
+ const segmentDocs = useOrgContactSegments(scope);
592
+ const viewDocs = useOrgCrmViews(scope);
593
+ const listDocs = useOrgLists(scope);
594
+ /*
595
+ * The site's campaigns, read because this form is on screen.
596
+ *
597
+ * `enabled` is off by default on the hook for the surfaces that render a
598
+ * picker beside fields a reader came for; here the picker IS one of the
599
+ * fields the reader came for, and the same reasoning that opens the segment
600
+ * listen opens this one.
601
+ */ const siteCampaigns = useHostCampaigns(hostId, {
602
+ enabled: true
603
+ });
604
+ /*
605
+ * THE CRM PICKERS' OPTIONS (AGL-2603), read because this form is on screen
606
+ * — the same bargain the segments and the campaigns get, one section up.
607
+ *
608
+ * The team comes through the members route rather than the collection, so
609
+ * a collaborator scoped to one site can still assign an owner; the field
610
+ * definitions are one bounded read; the companies are a SEARCH and read
611
+ * nothing until something is typed.
612
+ */ const team = useOrgMemberOptions(scope[1], {
613
+ enabled: true
614
+ });
615
+ const fieldDefinitions = useOrgContactFields(scope);
616
+ const [companySearch, setCompanySearch] = useState('');
617
+ const companies = useOrgCompanyOptions({
618
+ scope,
619
+ search: companySearch,
620
+ selectedIds: draft.companyIds
621
+ });
622
+ /*
623
+ * The saved segment the rule already names, even when the picker's window
624
+ * does not reach it.
625
+ *
626
+ * A `Select` whose value is not among its options renders empty and warns,
627
+ * so a rule pointing at the fifty-first segment would reopen looking as
628
+ * though it named none — and saving from that screen would erase the
629
+ * reference. The id is offered as its own option instead.
630
+ */ const segments = useMemo(()=>{
631
+ const rows = segmentDocs;
632
+ if (!draft.segmentId || rows.some((row)=>row.$id === draft.segmentId)) {
633
+ return rows;
634
+ }
635
+ return [
636
+ {
637
+ $id: draft.segmentId,
638
+ name: draft.segmentId
639
+ },
640
+ ...rows
641
+ ];
642
+ }, [
643
+ segmentDocs,
644
+ draft.segmentId
645
+ ]);
646
+ /*
647
+ * The saved VIEW the rule already names, kept the way the segment is
648
+ * (AGL-2617) — and for one more reason: the picker lists only views that
649
+ * translate whole, so a view edited past that after the rule named it
650
+ * would vanish from its own picker, and saving from that screen would
651
+ * erase the reference. Shown as its id, the sweep's refusal is at least
652
+ * visible.
653
+ */ const crmViews = useMemo(()=>{
654
+ const rows = viewDocs;
655
+ if (!draft.viewId || rows.some((row)=>row.$id === draft.viewId)) {
656
+ return rows;
657
+ }
658
+ return [
659
+ {
660
+ $id: draft.viewId,
661
+ name: draft.viewId
662
+ },
663
+ ...rows
664
+ ];
665
+ }, [
666
+ viewDocs,
667
+ draft.viewId
668
+ ]);
669
+ /** Every audience except the one being edited — see the prop's note. */ const lists = useMemo(()=>listDocs.filter((row)=>row.$id !== listId), [
670
+ listDocs,
671
+ listId
672
+ ]);
673
+ /** Ids the pickers can show, so a clause names an audience rather than a uid. */ const names = useMemo(()=>({
674
+ lists: Object.fromEntries(listDocs.map((row)=>{
675
+ var _row_name;
676
+ return [
677
+ row.$id,
678
+ (_row_name = row.name) != null ? _row_name : row.$id
679
+ ];
680
+ })),
681
+ segments: Object.fromEntries(segments.map((row)=>{
682
+ var _row_name;
683
+ return [
684
+ row.$id,
685
+ (_row_name = row.name) != null ? _row_name : row.$id
686
+ ];
687
+ })),
688
+ views: Object.fromEntries(crmViews.map((row)=>[
689
+ row.$id,
690
+ row.name
691
+ ])),
692
+ campaigns: Object.fromEntries(siteCampaigns.options.map((option)=>[
693
+ option.value,
694
+ option.label
695
+ ])),
696
+ members: Object.fromEntries(team.options.map((option)=>[
697
+ option.uid,
698
+ option.label
699
+ ])),
700
+ companies: companies.names,
701
+ fields: Object.fromEntries(fieldDefinitions.fields.map((field)=>[
702
+ field.key,
703
+ field.label
704
+ ]))
705
+ }), [
706
+ listDocs,
707
+ segments,
708
+ crmViews,
709
+ siteCampaigns.options,
710
+ team.options,
711
+ companies.names,
712
+ fieldDefinitions.fields
713
+ ]);
714
+ /*
715
+ * A stored owner the roster no longer lists, and a stored field key no
716
+ * definition names, are offered as their own option — the segment picker's
717
+ * discipline, for the same reason: a `Select` whose value is not among its
718
+ * options renders empty, and a rule saved from that screen would lose the
719
+ * clause it was opened with. A team member who left still owns the
720
+ * contacts they were assigned until somebody reassigns them, and a retired
721
+ * field still holds every value written under it.
722
+ */ const ownerOptions = useMemo(()=>{
723
+ const known = new Set(team.options.map((option)=>option.uid));
724
+ return [
725
+ ...draft.ownerUids.filter((uid)=>!known.has(uid)).map((uid)=>({
726
+ uid,
727
+ label: uid
728
+ })),
729
+ ...team.options
730
+ ];
731
+ }, [
732
+ team.options,
733
+ draft.ownerUids
734
+ ]);
735
+ const fieldOptions = useMemo(()=>{
736
+ const known = new Set(fieldDefinitions.fields.map((field)=>field.key));
737
+ return [
738
+ ...draft.custom.filter((clause)=>!known.has(clause.key)).map((clause)=>({
739
+ key: clause.key,
740
+ label: clause.key
741
+ })).filter((entry, index, all)=>all.findIndex((other)=>other.key === entry.key) === index),
742
+ ...fieldDefinitions.fields.map(({ key, label })=>({
743
+ key,
744
+ label
745
+ }))
746
+ ];
747
+ }, [
748
+ fieldDefinitions.fields,
749
+ draft.custom
750
+ ]);
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, {
753
+ [key]: value
754
+ }));
755
+ /*
756
+ * THE CUSTOM-FIELD ROWS. A new row names the first definition with an
757
+ * empty value, so it is on screen and NOT yet a filter — `draftDimensions`
758
+ * hands it to the normalizer, which drops a valued operator with no value
759
+ * until the reader types one. Changing the field clears the value, because
760
+ * a value typed for a number field is not a value for the select that
761
+ * replaced it; changing the operator to a presence test removes it, so the
762
+ * stored clause carries no value the sentence does not read.
763
+ */ const addClause = ()=>{
764
+ const first = fieldOptions[0];
765
+ if (!first) return;
766
+ set('custom', [
767
+ ...draft.custom,
768
+ {
769
+ key: first.key,
770
+ op: 'eq',
771
+ value: ''
772
+ }
773
+ ]);
774
+ };
775
+ const updateClause = (index, patch)=>set('custom', draft.custom.map((clause, at)=>{
776
+ if (at !== index) return clause;
777
+ const next = _extends({}, clause, patch);
778
+ if (PRESENCE_OPS.has(next.op)) delete next.value;
779
+ 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 : [];
785
+ const contactsOnly = draft.sources.includes('contacts');
786
+ return /*#__PURE__*/ _jsxs(Stack, {
787
+ spacing: 1.5,
788
+ children: [
789
+ /*#__PURE__*/ _jsxs(Box, {
790
+ sx: {
791
+ border: 1,
792
+ borderColor: 'divider',
793
+ borderRadius: 1,
794
+ p: 1.5
795
+ },
796
+ children: [
797
+ /*#__PURE__*/ _jsx(Typography, {
798
+ variant: "overline",
799
+ color: "text.secondary",
800
+ children: 'This audience'
801
+ }),
802
+ describeDynamicListRule(draftToRule(draft), names).map((clause)=>/*#__PURE__*/ _jsx(Typography, {
803
+ variant: "body2",
804
+ children: clause
805
+ }, clause))
806
+ ]
807
+ }),
808
+ /*#__PURE__*/ _jsxs(Stack, {
809
+ direction: "row",
810
+ spacing: 1,
811
+ useFlexGap: true,
812
+ sx: {
813
+ flexWrap: 'wrap'
814
+ },
815
+ children: [
816
+ /*#__PURE__*/ _jsx(TextField, {
817
+ select: true,
818
+ size: "small",
819
+ label: "People from",
820
+ 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))
834
+ }),
835
+ /*#__PURE__*/ _jsxs(TextField, {
836
+ select: true,
837
+ size: "small",
838
+ label: "Match",
839
+ value: draft.match,
840
+ onChange: (event)=>set('match', event.target.value),
841
+ 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
+ ]
859
+ }),
860
+ /*#__PURE__*/ _jsx(TextField, {
861
+ type: "date",
862
+ size: "small",
863
+ label: "Created after",
864
+ 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
+ }
874
+ }),
875
+ /*#__PURE__*/ _jsx(TextField, {
876
+ type: "date",
877
+ size: "small",
878
+ label: "Created before",
879
+ 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
+ }
889
+ }),
890
+ /*#__PURE__*/ _jsx(TextField, {
891
+ size: "small",
892
+ label: "Submitted form",
893
+ placeholder: "Contact us",
894
+ helperText: "Form submissions only",
895
+ value: draft.formNames,
896
+ onChange: (event)=>set('formNames', event.target.value),
897
+ sx: {
898
+ minWidth: 180
899
+ }
900
+ }),
901
+ /*#__PURE__*/ _jsx(Box, {
902
+ sx: {
903
+ minWidth: 240,
904
+ flexGrow: 1,
905
+ maxWidth: 360
906
+ },
907
+ children: /*#__PURE__*/ _jsx(CampaignPicker, {
908
+ options: siteCampaigns.options,
909
+ value: draft.campaignIds,
910
+ onChange: (next)=>set('campaignIds', next),
911
+ label: "In campaign",
912
+ helperText: "Contacts and form submissions filed under any campaign picked. Being in a campaign is not consent to be emailed.",
913
+ empty: siteCampaigns.ready && !siteCampaigns.options.length,
914
+ emptyText: "This site has no campaigns yet. Create one from Marketing to build an audience from it."
915
+ })
916
+ })
917
+ ]
918
+ }),
919
+ /*#__PURE__*/ _jsx(Divider, {}),
920
+ /*#__PURE__*/ _jsx(Typography, {
921
+ variant: "overline",
922
+ color: "text.secondary",
923
+ children: 'Contacts'
924
+ }),
925
+ contactsOnly ? null : /*#__PURE__*/ _jsx(Typography, {
926
+ variant: "caption",
927
+ color: "text.secondary",
928
+ children: 'These apply only to people drawn from Contacts. The rule does not ' + 'draw from Contacts, so they select nobody on their own — the ' + 'other sources are matched without them.'
929
+ }),
930
+ /*#__PURE__*/ _jsxs(Stack, {
931
+ direction: "row",
932
+ spacing: 1,
933
+ useFlexGap: true,
934
+ sx: {
935
+ flexWrap: 'wrap'
936
+ },
937
+ children: [
938
+ /*#__PURE__*/ _jsxs(TextField, {
939
+ select: true,
940
+ size: "small",
941
+ label: "Saved segment",
942
+ value: draft.segmentId,
943
+ onChange: (event)=>set('segmentId', event.target.value),
944
+ 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
+ ]
961
+ }),
962
+ /*#__PURE__*/ _jsxs(TextField, {
963
+ select: true,
964
+ size: "small",
965
+ label: "Saved view",
966
+ value: draft.viewId,
967
+ onChange: (event)=>set('viewId', event.target.value),
968
+ 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
+ ]
982
+ }),
983
+ /*#__PURE__*/ _jsx(TextField, {
984
+ size: "small",
985
+ label: "Tagged",
986
+ placeholder: "vip, wholesale",
987
+ value: draft.tags,
988
+ onChange: (event)=>set('tags', event.target.value),
989
+ sx: {
990
+ minWidth: 180
991
+ }
992
+ }),
993
+ /*#__PURE__*/ _jsx(TextField, {
994
+ select: true,
995
+ size: "small",
996
+ label: "Captured by",
997
+ value: draft.captureSources,
998
+ onChange: (event)=>set('captureSources', asArray(event.target.value)),
999
+ slotProps: {
1000
+ select: {
1001
+ multiple: true
1002
+ }
1003
+ },
1004
+ 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))
1012
+ })
1013
+ ]
1014
+ }),
1015
+ /*#__PURE__*/ _jsxs(Stack, {
1016
+ direction: "row",
1017
+ spacing: 1,
1018
+ useFlexGap: true,
1019
+ sx: {
1020
+ flexWrap: 'wrap'
1021
+ },
1022
+ children: [
1023
+ /*#__PURE__*/ _jsx(TextField, {
1024
+ select: true,
1025
+ size: "small",
1026
+ label: "Owned by",
1027
+ 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
+ },
1035
+ error: Boolean(team.error),
1036
+ 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))
1044
+ }),
1045
+ /*#__PURE__*/ _jsx(TextField, {
1046
+ select: true,
1047
+ size: "small",
1048
+ label: "Lifecycle stage",
1049
+ 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
+ },
1060
+ 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))
1068
+ }),
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
+ }
1102
+ }),
1103
+ /*#__PURE__*/ _jsx(TextField, {
1104
+ type: "number",
1105
+ size: "small",
1106
+ label: "Engaged with a campaign within (days)",
1107
+ helperText: "Opened or clicked one of your campaigns. Never engaged is left out.",
1108
+ value: draft.engagedWithinDays,
1109
+ onChange: (event)=>set('engagedWithinDays', event.target.value),
1110
+ sx: {
1111
+ minWidth: 280
1112
+ }
1113
+ })
1114
+ ]
1115
+ }),
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
+ }),
1241
+ /*#__PURE__*/ _jsxs(Box, {
1242
+ children: [
1243
+ /*#__PURE__*/ _jsx(Button, {
1244
+ size: "small",
1245
+ variant: "outlined",
1246
+ disabled: !fieldOptions.length,
1247
+ onClick: addClause,
1248
+ children: 'Add a field condition'
1249
+ }),
1250
+ fieldDefinitions.ready && !fieldDefinitions.fields.length ? /*#__PURE__*/ _jsx(Typography, {
1251
+ variant: "caption",
1252
+ color: "text.secondary",
1253
+ sx: {
1254
+ display: 'block',
1255
+ mt: 0.5
1256
+ },
1257
+ children: 'This workspace has no custom contact fields yet. Define them ' + 'under CRM → Fields to filter on them here.'
1258
+ }) : null
1259
+ ]
1260
+ }),
1261
+ /*#__PURE__*/ _jsx(Typography, {
1262
+ variant: "overline",
1263
+ color: "text.secondary",
1264
+ children: 'Purchase history'
1265
+ }),
1266
+ /*#__PURE__*/ _jsxs(Stack, {
1267
+ direction: "row",
1268
+ spacing: 1,
1269
+ useFlexGap: true,
1270
+ sx: {
1271
+ flexWrap: 'wrap'
1272
+ },
1273
+ children: [
1274
+ /*#__PURE__*/ _jsx(TextField, {
1275
+ type: "number",
1276
+ size: "small",
1277
+ label: "Orders at least",
1278
+ value: draft.ordersCountAtLeast,
1279
+ onChange: (event)=>set('ordersCountAtLeast', event.target.value),
1280
+ sx: {
1281
+ minWidth: 160
1282
+ }
1283
+ }),
1284
+ /*#__PURE__*/ _jsx(TextField, {
1285
+ type: "number",
1286
+ size: "small",
1287
+ label: "Spent at least",
1288
+ helperText: "Lifetime, in your store's currency",
1289
+ value: draft.ltvAtLeast,
1290
+ onChange: (event)=>set('ltvAtLeast', event.target.value),
1291
+ sx: {
1292
+ minWidth: 190
1293
+ }
1294
+ }),
1295
+ /*#__PURE__*/ _jsx(TextField, {
1296
+ type: "number",
1297
+ size: "small",
1298
+ label: "Bought within (days)",
1299
+ value: draft.lastPurchaseWithinDays,
1300
+ onChange: (event)=>set('lastPurchaseWithinDays', event.target.value),
1301
+ sx: {
1302
+ minWidth: 190
1303
+ }
1304
+ }),
1305
+ /*#__PURE__*/ _jsx(TextField, {
1306
+ type: "number",
1307
+ size: "small",
1308
+ label: "Nothing bought for (days)",
1309
+ helperText: "Lapsed customers — never bought is not lapsed",
1310
+ value: draft.noPurchaseForDays,
1311
+ onChange: (event)=>set('noPurchaseForDays', event.target.value),
1312
+ sx: {
1313
+ minWidth: 220
1314
+ }
1315
+ })
1316
+ ]
1317
+ }),
1318
+ /*#__PURE__*/ _jsx(Divider, {}),
1319
+ /*#__PURE__*/ _jsx(Typography, {
1320
+ variant: "overline",
1321
+ color: "text.secondary",
1322
+ children: 'Email engagement'
1323
+ }),
1324
+ /*#__PURE__*/ _jsx(Typography, {
1325
+ variant: "caption",
1326
+ color: "text.secondary",
1327
+ children: 'Opens and clicks across every email this workspace has sent the ' + 'person. Clicks are the stronger signal: mail apps that preload ' + 'images record an open the reader never made.'
1328
+ }),
1329
+ /*#__PURE__*/ _jsxs(Stack, {
1330
+ direction: "row",
1331
+ spacing: 1,
1332
+ useFlexGap: true,
1333
+ sx: {
1334
+ flexWrap: 'wrap'
1335
+ },
1336
+ children: [
1337
+ /*#__PURE__*/ _jsx(TextField, {
1338
+ type: "number",
1339
+ size: "small",
1340
+ label: "Opened within (days)",
1341
+ value: draft.openedWithinDays,
1342
+ onChange: (event)=>set('openedWithinDays', event.target.value),
1343
+ sx: {
1344
+ minWidth: 190
1345
+ }
1346
+ }),
1347
+ /*#__PURE__*/ _jsx(TextField, {
1348
+ type: "number",
1349
+ size: "small",
1350
+ label: "Clicked within (days)",
1351
+ value: draft.clickedWithinDays,
1352
+ onChange: (event)=>set('clickedWithinDays', event.target.value),
1353
+ sx: {
1354
+ minWidth: 190
1355
+ }
1356
+ }),
1357
+ /*#__PURE__*/ _jsx(TextField, {
1358
+ type: "number",
1359
+ size: "small",
1360
+ label: "Nothing opened for (days)",
1361
+ helperText: "Never opened counts",
1362
+ value: draft.notOpenedForDays,
1363
+ onChange: (event)=>set('notOpenedForDays', event.target.value),
1364
+ sx: {
1365
+ minWidth: 210
1366
+ }
1367
+ }),
1368
+ /*#__PURE__*/ _jsx(TextField, {
1369
+ type: "number",
1370
+ size: "small",
1371
+ label: "Nothing clicked for (days)",
1372
+ helperText: "Never clicked counts",
1373
+ value: draft.notClickedForDays,
1374
+ onChange: (event)=>set('notClickedForDays', event.target.value),
1375
+ sx: {
1376
+ minWidth: 210
1377
+ }
1378
+ })
1379
+ ]
1380
+ }),
1381
+ /*#__PURE__*/ _jsx(Divider, {}),
1382
+ /*#__PURE__*/ _jsx(Typography, {
1383
+ variant: "overline",
1384
+ color: "text.secondary",
1385
+ children: 'Other audiences'
1386
+ }),
1387
+ /*#__PURE__*/ _jsxs(Stack, {
1388
+ direction: "row",
1389
+ spacing: 1,
1390
+ useFlexGap: true,
1391
+ sx: {
1392
+ flexWrap: 'wrap'
1393
+ },
1394
+ children: [
1395
+ /*#__PURE__*/ _jsx(TextField, {
1396
+ select: true,
1397
+ size: "small",
1398
+ label: "Already on",
1399
+ value: draft.inListIds,
1400
+ onChange: (event)=>set('inListIds', asArray(event.target.value)),
1401
+ slotProps: {
1402
+ select: {
1403
+ multiple: true
1404
+ }
1405
+ },
1406
+ 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
+ })
1417
+ }),
1418
+ /*#__PURE__*/ _jsx(TextField, {
1419
+ select: true,
1420
+ size: "small",
1421
+ label: "Not on",
1422
+ value: draft.notInListIds,
1423
+ onChange: (event)=>set('notInListIds', asArray(event.target.value)),
1424
+ slotProps: {
1425
+ select: {
1426
+ multiple: true
1427
+ }
1428
+ },
1429
+ 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
+ })
1440
+ })
1441
+ ]
1442
+ })
1443
+ ]
1444
+ });
1445
+ }
1446
+ DynamicListRuleFields.displayName = 'DynamicListRuleFields';
1447
+ /**
1448
+ * A draft seeded from a rule that arrives after the form does.
1449
+ *
1450
+ * The edit page mounts before its list document lands, so the controls are
1451
+ * built from an empty rule and filled in once. Seeding ONCE per subject is the
1452
+ * whole point: the document is a live listen, and re-seeding on every snapshot
1453
+ * would overwrite whatever the reader had typed each time anything on the list
1454
+ * changed — including the snapshot the reader's own save produces.
1455
+ */ export function useRuleDraft(rule, resetKey) {
1456
+ const [draft, setDraft] = useState(EMPTY_RULE_DRAFT);
1457
+ const seededFor = useRef('');
1458
+ useEffect(()=>{
1459
+ if (!rule || seededFor.current === resetKey) return;
1460
+ seededFor.current = resetKey;
1461
+ setDraft(ruleToDraft(rule));
1462
+ }, [
1463
+ rule,
1464
+ resetKey
1465
+ ]);
1466
+ return [
1467
+ draft,
1468
+ setDraft
1469
+ ];
1470
+ }
1471
+ export default DynamicListRuleFields;
1472
+
1473
+ //# sourceMappingURL=dynamic-list-rule-fields.js.map