@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- 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
|