@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,686 @@
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, Fragment as _Fragment } from "react/jsx-runtime";
19
+ /**
20
+ * Who is on one list, and the three things you can do about it.
21
+ *
22
+ * The audience card could create a list and delete a list. It could not show
23
+ * you a single name on one. This panel is the rest of it: read the membership,
24
+ * add somebody, take somebody off. The list's own settings — its name, and the
25
+ * rule behind a dynamic one — belong to the edit route, because they are
26
+ * facts about the LIST rather than about who is on it.
27
+ *
28
+ * ## It mounts only on a list's own page
29
+ *
30
+ * The audience detail route renders this for the ONE list being read. A
31
+ * membership listener per row would open one per list on the audiences table,
32
+ * on a surface where most visits are to read the list of lists — the
33
+ * read-on-mount shape `emails-console-read-cost.spec.tsx` meters. Nothing here
34
+ * reads anything until a list is opened, and the add form reads nothing until
35
+ * an address is typed.
36
+ *
37
+ * ## Whether somebody may be ADDED is the server's answer, always
38
+ *
39
+ * Every sentence about an address being typed in is `email/list-members-preview`
40
+ * answering from that person's actual record and both suppression lists. This
41
+ * component decides none of it. A screen that decided for itself would be a
42
+ * second copy of the rule on the one surface whose job is to tell the operator
43
+ * the truth about what is about to happen — including, and especially, that
44
+ * the product knows nothing about this person's permission.
45
+ *
46
+ * ## The membership table reads consent through the SEND-TIME reader
47
+ *
48
+ * The `Consent` column answers the same question `performCampaignSend` asks of
49
+ * the same document, so it asks it with the same function —
50
+ * {@link readMarketingBasis}, against the consent group this console is being
51
+ * viewed as. A column that picked the fields out of the row itself is a second
52
+ * reader of a stored shape, and the two only agree until the shape moves.
53
+ *
54
+ * ## Removing is not suppressing, and the screen says so
55
+ *
56
+ * Taking somebody off a list is the operator changing their mind about an
57
+ * audience. It is not the person asking to stop being mailed, it does not stop
58
+ * another list reaching them, and it writes nothing to the suppression list.
59
+ * Those are different acts with different records, and a console that let them
60
+ * blur is a console where "remove" is quietly relied on as an unsubscribe.
61
+ */ import { readMarketingBasis } from "@aglyn/aglyn";
62
+ import { mdiAccountRemoveOutline } from "@aglyn/shared-data-mdi";
63
+ import { MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
64
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
65
+ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
66
+ import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
67
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
68
+ import { Alert, Box, Button, Checkbox, Chip, FormControlLabel, Stack, TextField, Typography } from "@mui/material";
69
+ import { collection, deleteDoc, doc, documentId, limit, orderBy, query } from "firebase/firestore";
70
+ import { useCallback, useEffect, useState } from "react";
71
+ import { useFirestore, usePagedCollection, useUser } from "@aglyn/tenant-feature-instance";
72
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
73
+ /**
74
+ * How the basis reads on screen, for the group this console is viewed as.
75
+ *
76
+ * ## Read through `readMarketingBasis`, never off the row
77
+ *
78
+ * This picked `marketingConsent`, `marketingConsentBasis` and
79
+ * `marketingConsentAtMs` off the top of the membership document, which is
80
+ * where `enrollListMember` used to put them. They now live in an entry per
81
+ * site under `marketingConsentByHost`, because a basis runs to the brand it
82
+ * was given to rather than to the account that holds the row. Nothing here
83
+ * moved with them, so every top-level read answered `undefined` and every
84
+ * member of every list — including people carrying a perfectly good attested
85
+ * basis — was reported as having no basis on record, on the one screen an
86
+ * operator consults before mailing them. `performCampaignSend` reads the same
87
+ * documents through the function below and mailed them, correctly, at the same
88
+ * time.
89
+ *
90
+ * The fix is not a second field name. It is that the send path's reader is the
91
+ * only reader: a surface that parses the stored shape itself agrees with the
92
+ * sender only until the shape moves, and it moves without anybody thinking to
93
+ * look here.
94
+ *
95
+ * ## Five states, because the model carries five
96
+ *
97
+ * An attestation and an opt-in are not the same fact — one is a person's own
98
+ * decision, the other a claim an account made on their behalf — and a table
99
+ * rendering both as a tick is the conflation `list-members.ts` stores the
100
+ * basis to prevent. The three that are not a grant are not one state either.
101
+ * Absence is not refusal, and neither of them is "opted in, to somebody else
102
+ * in this account": that third one is a person the sender deliberately leaves
103
+ * alone, and an operator who cannot see why reads it as the product having
104
+ * lost their subscriber.
105
+ */ function consentLabel(member, group) {
106
+ const record = readMarketingBasis(member, group);
107
+ const when = record.basisAtMs ? ` · ${new Date(record.basisAtMs).toLocaleDateString()}` : '';
108
+ if (record.basis === 'declined') {
109
+ return {
110
+ label: `Opted out${when}`,
111
+ color: 'error'
112
+ };
113
+ }
114
+ if (record.basis === 'granted') {
115
+ /*
116
+ * `assertedBy` rather than the stored basis field, because there are two
117
+ * ways a record says an operator asserted it — a membership's
118
+ * `marketingConsentBasis`, and the provenance object a backfill writes —
119
+ * and the reader has already resolved both into this one answer.
120
+ */ return record.assertedBy === 'operator' ? {
121
+ label: `Attested by your team${when}`,
122
+ color: 'warning'
123
+ } : {
124
+ label: `Opted in${when}`,
125
+ color: 'default'
126
+ };
127
+ }
128
+ /*
129
+ * A grant this site may not use. Reported rather than folded into "no
130
+ * basis" because the two are different situations with different answers:
131
+ * nobody has ever asked this person, versus somebody asked them on behalf
132
+ * of another brand in this account and this brand may not borrow it.
133
+ */ if (record.otherGrant === 'other-host') {
134
+ return {
135
+ label: 'Opted in to another site',
136
+ color: 'warning'
137
+ };
138
+ }
139
+ if (record.otherGrant === 'unscoped') {
140
+ return {
141
+ label: 'Opted in — no site recorded',
142
+ color: 'warning'
143
+ };
144
+ }
145
+ return {
146
+ label: 'No basis on record',
147
+ color: 'default'
148
+ };
149
+ }
150
+ /** Free text — a paste, a typed address, commas or newlines — to addresses. */ export function splitAddresses(value) {
151
+ return value.split(/[\s,;]+/).map((entry)=>entry.trim())// A pasted mail-client line is `Name <a@b.co>`; keep the address part.
152
+ .map((entry)=>entry.replace(/^.*<|>.*$/g, '')).filter(Boolean);
153
+ }
154
+ export function ListMembersPanel(props) {
155
+ const { hostId, consentGroup, scope, listId, listName, onMembershipChanged, findRule, ruleSummary } = props;
156
+ const firestore = useFirestore();
157
+ const { data: user } = useUser();
158
+ const { enqueueSnackbar } = useSnackbar();
159
+ const { confirm } = useConfirmationContext();
160
+ /*
161
+ * Ordered by DOCUMENT ID and paged, not capped and re-sorted here.
162
+ *
163
+ * `__name__` rather than `addedAt`, which is the column a reader would
164
+ * assume: `orderBy` FILTERS as well as sorts, so a document missing the
165
+ * field is not in the result at all — and `addedAt` is stamped only when a
166
+ * membership is CREATED, so every row written under the two legacy ids
167
+ * `list-members.ts` still adopts predates it. Ordering on it would drop the
168
+ * oldest members from their own list, silently, which is the failure that
169
+ * turned an audience into a random sample (AGL-2501). The id is the one key
170
+ * every row has.
171
+ */ const { rows: members, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, scope[0], scope[1], 'lists', listId, 'members'), orderBy(documentId()), limit(pageLimit)), [
172
+ firestore,
173
+ scope[0],
174
+ scope[1],
175
+ listId
176
+ ], {
177
+ idField: '$id'
178
+ });
179
+ const [addInput, setAddInput] = useState('');
180
+ const [addName, setAddName] = useState('');
181
+ const [preview, setPreview] = useState(null);
182
+ const [attested, setAttested] = useState(false);
183
+ const [results, setResults] = useState(null);
184
+ const [busy, setBusy] = useState(false);
185
+ /*
186
+ * The addresses a SEARCH found, when the operator ran one.
187
+ *
188
+ * A second way to fill the same candidate set, not a second way to add. Once
189
+ * this holds addresses, everything below it — the consent readout, the
190
+ * attestation, the Add button — is the code the typed path already used, so
191
+ * there is exactly one place in this component where somebody is put on a
192
+ * list and exactly one gate in front of it.
193
+ */ const [found, setFound] = useState(null);
194
+ const addresses = found ? found.emails : splitAddresses(addInput);
195
+ /*
196
+ * Any edit to the box invalidates the answer.
197
+ *
198
+ * A preview belongs to the exact addresses it was run for. Leaving a stale
199
+ * one on screen would show a count, and an attestation checkbox, for a set
200
+ * the operator has since changed — so the number they stand behind would
201
+ * not be the number that acts. Typing also abandons a search: the two are
202
+ * alternative ways of naming a candidate set, and holding both at once
203
+ * would leave the Add button acting on whichever the code happened to
204
+ * prefer.
205
+ */ useEffect(()=>{
206
+ setPreview(null);
207
+ setAttested(false);
208
+ setResults(null);
209
+ setFound(null);
210
+ }, [
211
+ addInput
212
+ ]);
213
+ const post = useCallback(async (route, body)=>{
214
+ const response = await authorizedFetch(user, `/api/email/${route}`, {
215
+ method: 'POST',
216
+ headers: {
217
+ 'Content-Type': 'application/json'
218
+ },
219
+ body: JSON.stringify(_extends({
220
+ hostId,
221
+ listId
222
+ }, body))
223
+ });
224
+ const payload = await response.json().catch(()=>({}));
225
+ return {
226
+ ok: response.ok,
227
+ payload
228
+ };
229
+ }, [
230
+ user,
231
+ hostId,
232
+ listId
233
+ ]);
234
+ const handlePreview = useCallback(async ()=>{
235
+ if (busy || !addresses.length) return;
236
+ setBusy(true);
237
+ try {
238
+ const { ok, payload } = await post('list-members-preview', {
239
+ emails: addresses
240
+ });
241
+ if (!ok) {
242
+ var _ref;
243
+ return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The addresses could not be checked.', {
244
+ variant: 'warning',
245
+ allowDuplicate: true
246
+ });
247
+ }
248
+ setPreview(payload);
249
+ } catch (unused) {
250
+ enqueueSnackbar('The addresses could not be checked.', {
251
+ variant: 'warning',
252
+ allowDuplicate: true
253
+ });
254
+ } finally{
255
+ setBusy(false);
256
+ }
257
+ }, [
258
+ busy,
259
+ addresses,
260
+ post,
261
+ enqueueSnackbar
262
+ ]);
263
+ const handleFind = useCallback(async ()=>{
264
+ if (busy || !findRule) return;
265
+ setBusy(true);
266
+ try {
267
+ var _ref;
268
+ const { ok, payload } = await post('list-rule-preview', {
269
+ rule: findRule
270
+ });
271
+ if (!ok) {
272
+ var _ref1;
273
+ return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The audience could not be worked out.', {
274
+ variant: 'warning',
275
+ allowDuplicate: true
276
+ });
277
+ }
278
+ /*
279
+ * The verdicts come back from the SAME resolution the typed path runs,
280
+ * so what lands in `preview` here is the same object the consent
281
+ * readout below already knows how to draw. A search that returned its
282
+ * own shaped summary would be a second consent readout, and the second
283
+ * one is the one nobody reviews.
284
+ */ setFound(payload);
285
+ setPreview(payload);
286
+ setAttested(false);
287
+ setResults(null);
288
+ if (!Number((_ref = payload == null ? void 0 : payload.matched) != null ? _ref : 0)) {
289
+ enqueueSnackbar('These filters match nobody', {
290
+ variant: 'info',
291
+ persist: false
292
+ });
293
+ }
294
+ } catch (unused) {
295
+ enqueueSnackbar('The audience could not be worked out.', {
296
+ variant: 'warning',
297
+ allowDuplicate: true
298
+ });
299
+ } finally{
300
+ setBusy(false);
301
+ }
302
+ }, [
303
+ busy,
304
+ findRule,
305
+ post,
306
+ enqueueSnackbar
307
+ ]);
308
+ const handleAdd = useCallback(async ()=>{
309
+ if (busy || !preview) return;
310
+ setBusy(true);
311
+ try {
312
+ var _ref, _ref1;
313
+ const { ok, payload } = await post('list-members-add', _extends({
314
+ emails: addresses
315
+ }, addresses.length === 1 && addName.trim() ? {
316
+ name: addName.trim()
317
+ } : {}, {
318
+ // The operator's assertion, not a basis they named: the server
319
+ // derives a pass-through from each person's own record, and this flag
320
+ // can only ever add the attributable kind.
321
+ attestConsent: attested
322
+ }));
323
+ if (!ok) {
324
+ var _ref2;
325
+ return void enqueueSnackbar((_ref2 = payload == null ? void 0 : payload.error) != null ? _ref2 : 'Nobody was added to the list', {
326
+ variant: 'warning',
327
+ allowDuplicate: true
328
+ });
329
+ }
330
+ const added = Number((_ref = payload == null ? void 0 : payload.added) != null ? _ref : 0);
331
+ setResults((_ref1 = payload == null ? void 0 : payload.results) != null ? _ref1 : []);
332
+ setPreview(null);
333
+ if (added) {
334
+ setAddInput('');
335
+ setAddName('');
336
+ setFound(null);
337
+ onMembershipChanged == null ? void 0 : onMembershipChanged();
338
+ }
339
+ enqueueSnackbar(added === 1 ? 'One person added' : `${added} people added`, {
340
+ variant: added ? 'success' : 'warning',
341
+ persist: false
342
+ });
343
+ } catch (unused) {
344
+ enqueueSnackbar('Nobody was added to the list', {
345
+ variant: 'warning',
346
+ allowDuplicate: true
347
+ });
348
+ } finally{
349
+ setBusy(false);
350
+ }
351
+ }, [
352
+ busy,
353
+ preview,
354
+ addresses,
355
+ addName,
356
+ attested,
357
+ post,
358
+ enqueueSnackbar,
359
+ onMembershipChanged
360
+ ]);
361
+ const handleRemove = useCallback(async (member)=>{
362
+ var _member_email;
363
+ const accepted = await confirm({
364
+ title: 'Remove from this list?',
365
+ description: `${(_member_email = member.email) != null ? _member_email : 'This person'} stops being part of "${listName}" ` + 'and campaigns to this list stop reaching them. This is NOT an ' + 'unsubscribe: it does not add them to your suppression list, and ' + 'any other list they are on still reaches them. If they asked to ' + 'stop hearing from you, suppress the address instead.',
366
+ confirmationText: 'Remove',
367
+ confirmationButtonProps: {
368
+ color: 'error'
369
+ }
370
+ })// `confirm` resolves with no value and REJECTS on cancel, so gating on
371
+ // the resolved value alone would make this always proceed (AGL-950).
372
+ .then(()=>true).catch(()=>false);
373
+ if (!accepted) return;
374
+ try {
375
+ await deleteDoc(doc(firestore, scope[0], scope[1], 'lists', listId, 'members', member.$id));
376
+ onMembershipChanged == null ? void 0 : onMembershipChanged();
377
+ enqueueSnackbar('Removed from the list', {
378
+ variant: 'success',
379
+ persist: false
380
+ });
381
+ } catch (error) {
382
+ console.error(error);
383
+ enqueueSnackbar('They were not removed', {
384
+ variant: 'error'
385
+ });
386
+ }
387
+ }, [
388
+ confirm,
389
+ listName,
390
+ firestore,
391
+ scope,
392
+ listId,
393
+ enqueueSnackbar,
394
+ onMembershipChanged
395
+ ]);
396
+ const enrollable = preview ? preview.optedIn + preview.needAttestation : 0;
397
+ const columns = [
398
+ {
399
+ field: 'email',
400
+ headerName: 'Address',
401
+ flex: 1,
402
+ minWidth: 220,
403
+ valueGetter: (_value, row)=>{
404
+ var _row_email;
405
+ return (_row_email = row.email) != null ? _row_email : '—';
406
+ }
407
+ },
408
+ {
409
+ field: 'name',
410
+ headerName: 'Name',
411
+ flex: 1,
412
+ minWidth: 160,
413
+ valueGetter: (_value, row)=>{
414
+ var _row_name;
415
+ return (_row_name = row.name) != null ? _row_name : '—';
416
+ }
417
+ },
418
+ {
419
+ field: 'addedAt',
420
+ headerName: 'Joined',
421
+ width: 120,
422
+ // Sorted on the instant, drawn as a local date.
423
+ valueGetter: (_value, row)=>{
424
+ var _ref;
425
+ var _row_addedAt_toDate_getTime, _row_addedAt_toDate, _row_addedAt_toDate1, _row_addedAt;
426
+ return (_ref = (_row_addedAt = row.addedAt) == null ? void 0 : (_row_addedAt_toDate1 = _row_addedAt.toDate) == null ? void 0 : (_row_addedAt_toDate = _row_addedAt_toDate1.call(_row_addedAt)) == null ? void 0 : (_row_addedAt_toDate_getTime = _row_addedAt_toDate.getTime) == null ? void 0 : _row_addedAt_toDate_getTime.call(_row_addedAt_toDate)) != null ? _ref : 0;
427
+ },
428
+ renderCell: ({ row })=>{
429
+ var _row_addedAt;
430
+ return ((_row_addedAt = row.addedAt) == null ? void 0 : _row_addedAt.toDate) ? row.addedAt.toDate().toLocaleDateString() : '—';
431
+ }
432
+ },
433
+ {
434
+ field: 'via',
435
+ headerName: 'How',
436
+ width: 200,
437
+ valueGetter: (_value, row)=>row.via === 'rule' ? 'Rule' : 'Added',
438
+ /*
439
+ `via` says whether a rule put them here or a person did, which is what
440
+ decides whether they LEAVE on their own: the materializer reconciles its
441
+ own rows away and never touches a manual one. `source` is the finer
442
+ provenance underneath it.
443
+ */ renderCell: ({ row, value })=>/*#__PURE__*/ _jsxs(Stack, {
444
+ direction: "row",
445
+ spacing: 1,
446
+ sx: {
447
+ alignItems: 'center',
448
+ minWidth: 0
449
+ },
450
+ children: [
451
+ /*#__PURE__*/ _jsx(Chip, {
452
+ size: "small",
453
+ variant: "outlined",
454
+ label: value
455
+ }),
456
+ row.source ? /*#__PURE__*/ _jsx(Typography, {
457
+ variant: "caption",
458
+ color: "text.secondary",
459
+ noWrap: true,
460
+ children: row.source
461
+ }) : null
462
+ ]
463
+ })
464
+ },
465
+ {
466
+ field: 'consent',
467
+ headerName: 'Consent',
468
+ width: 190,
469
+ valueGetter: (_value, row)=>consentLabel(row, consentGroup).label,
470
+ renderCell: ({ row })=>{
471
+ const consent = consentLabel(row, consentGroup);
472
+ return /*#__PURE__*/ _jsx(Chip, {
473
+ size: "small",
474
+ variant: "outlined",
475
+ color: consent.color,
476
+ label: consent.label
477
+ });
478
+ }
479
+ },
480
+ /*
481
+ A SUBSCRIBER HAS NO PAGE of their own here, so the row opens nothing —
482
+ but taking somebody off an audience is the one act performed on one, and
483
+ it belongs in the trailing cluster with every other list's actions rather
484
+ than as a red text button sitting in the row.
485
+ */ listActionsColumn((row)=>{
486
+ var _row_email;
487
+ return /*#__PURE__*/ _jsx(ListRowActions, {
488
+ label: String((_row_email = row.email) != null ? _row_email : row.$id),
489
+ items: [
490
+ {
491
+ key: 'remove',
492
+ label: 'Remove from this list',
493
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
494
+ path: mdiAccountRemoveOutline.path,
495
+ size: 0.8
496
+ }),
497
+ destructive: true,
498
+ onClick: ()=>void handleRemove(row)
499
+ }
500
+ ]
501
+ });
502
+ }, {
503
+ width: 72
504
+ })
505
+ ];
506
+ return /*#__PURE__*/ _jsxs(Stack, {
507
+ spacing: 2,
508
+ sx: {
509
+ py: 2
510
+ },
511
+ children: [
512
+ findRule ? /*#__PURE__*/ _jsxs(Stack, {
513
+ spacing: 1,
514
+ sx: {
515
+ border: 1,
516
+ borderColor: 'divider',
517
+ borderRadius: 1,
518
+ p: 1.5
519
+ },
520
+ children: [
521
+ /*#__PURE__*/ _jsx(Typography, {
522
+ variant: "overline",
523
+ color: "text.secondary",
524
+ children: 'Find people'
525
+ }),
526
+ (ruleSummary != null ? ruleSummary : []).map((clause)=>/*#__PURE__*/ _jsx(Typography, {
527
+ variant: "body2",
528
+ children: clause
529
+ }, clause)),
530
+ /*#__PURE__*/ _jsx(Box, {
531
+ children: /*#__PURE__*/ _jsx(Button, {
532
+ size: "small",
533
+ variant: "outlined",
534
+ disabled: busy,
535
+ onClick: ()=>void handleFind(),
536
+ children: busy ? 'Searching…' : 'Find matching people'
537
+ })
538
+ }),
539
+ found ? /*#__PURE__*/ _jsx(Alert, {
540
+ severity: found.matched ? 'info' : 'warning',
541
+ children: (found.matched === 1 ? '1 person matches' : `${found.matched.toLocaleString()} people match`) + (found.truncated ? ` — this batch covers the first ${found.emails.length}. ` + 'Add them, then search again for the rest.' : '') + (found.complete ? '' : ' The search stopped at its read budget, so this is at ' + 'least that many rather than exactly.')
542
+ }) : null,
543
+ /*#__PURE__*/ _jsx(Typography, {
544
+ variant: "caption",
545
+ color: "text.secondary",
546
+ children: 'Whoever you add stays on this list. It does not keep growing — ' + 'change the membership to live on the edit page if you want ' + 'new matches enrolled automatically.'
547
+ })
548
+ ]
549
+ }) : null,
550
+ /*#__PURE__*/ _jsx(Typography, {
551
+ variant: "body2",
552
+ color: "text.secondary",
553
+ children: 'Add someone by typing their address, or paste a column of them. ' + 'Every address is checked against their consent record and both ' + 'suppression lists before anyone is added.'
554
+ }),
555
+ /*#__PURE__*/ _jsxs(Stack, {
556
+ direction: "row",
557
+ spacing: 1,
558
+ useFlexGap: true,
559
+ sx: {
560
+ flexWrap: 'wrap'
561
+ },
562
+ children: [
563
+ /*#__PURE__*/ _jsx(TextField, {
564
+ size: "small",
565
+ label: "Email addresses",
566
+ placeholder: "someone@example.com",
567
+ multiline: true,
568
+ minRows: 1,
569
+ maxRows: 6,
570
+ value: addInput,
571
+ onChange: (event)=>setAddInput(event.target.value),
572
+ helperText: addresses.length > 1 ? `${addresses.length} addresses` : 'One address, or paste many',
573
+ sx: {
574
+ flexGrow: 1,
575
+ minWidth: 260
576
+ }
577
+ }),
578
+ addresses.length > 1 ? null : /*#__PURE__*/ _jsx(TextField, {
579
+ size: "small",
580
+ label: "Name",
581
+ value: addName,
582
+ onChange: (event)=>setAddName(event.target.value),
583
+ sx: {
584
+ minWidth: 180
585
+ }
586
+ }),
587
+ /*#__PURE__*/ _jsx(Box, {
588
+ children: /*#__PURE__*/ _jsx(Button, {
589
+ size: "small",
590
+ variant: "outlined",
591
+ disabled: busy || !addresses.length || Boolean(preview),
592
+ onClick: ()=>void handlePreview(),
593
+ children: busy ? 'Checking…' : 'Check'
594
+ })
595
+ })
596
+ ]
597
+ }),
598
+ preview ? /*#__PURE__*/ _jsxs(Stack, {
599
+ spacing: 1,
600
+ children: [
601
+ /*#__PURE__*/ _jsx(Alert, {
602
+ severity: enrollable ? 'info' : 'warning',
603
+ children: [
604
+ preview.optedIn ? `${preview.optedIn} already opted in` : '',
605
+ preview.needAttestation ? `${preview.needAttestation} with no opt-in on record` : '',
606
+ preview.refused ? `${preview.refused} cannot be added at all` : ''
607
+ ].filter(Boolean).join(' · ')
608
+ }),
609
+ preview.verdicts.filter((verdict)=>verdict.refusal).map((verdict)=>/*#__PURE__*/ _jsx(Typography, {
610
+ variant: "caption",
611
+ color: "text.secondary",
612
+ children: `${verdict.input} — ${verdict.summary}`
613
+ }, verdict.input)),
614
+ preview.needAttestation ? /*#__PURE__*/ _jsx(FormControlLabel, {
615
+ control: /*#__PURE__*/ _jsx(Checkbox, {
616
+ checked: attested,
617
+ onChange: (event)=>setAttested(event.target.checked)
618
+ }),
619
+ label: /*#__PURE__*/ _jsx(Typography, {
620
+ variant: "body2",
621
+ children: `I have permission to send marketing email to the ` + `${preview.needAttestation} ` + `${preview.needAttestation === 1 ? 'person' : 'people'} ` + 'above with no opt-in on record. This is recorded against ' + 'my account, with the date.'
622
+ })
623
+ }) : null,
624
+ /*#__PURE__*/ _jsx(Box, {
625
+ children: /*#__PURE__*/ _jsx(Button, {
626
+ size: "small",
627
+ variant: "contained",
628
+ disabled: busy || !enrollable || preview.needAttestation > 0 && !attested,
629
+ onClick: ()=>void handleAdd(),
630
+ children: busy ? 'Adding…' : `Add ${enrollable}`
631
+ })
632
+ })
633
+ ]
634
+ }) : null,
635
+ (results == null ? void 0 : results.length) ? /*#__PURE__*/ _jsx(Stack, {
636
+ spacing: 0.5,
637
+ children: results.filter((result)=>!result.enrolled).map((result)=>{
638
+ var _result_error;
639
+ return /*#__PURE__*/ _jsx(Typography, {
640
+ variant: "caption",
641
+ color: "text.secondary",
642
+ children: `${result.input} — not added. ${(_result_error = result.error) != null ? _result_error : ''}`
643
+ }, result.input);
644
+ })
645
+ }) : null,
646
+ members.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
647
+ variant: "body2",
648
+ color: "text.secondary",
649
+ children: 'Nobody is on this list yet.'
650
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
651
+ children: [
652
+ /*#__PURE__*/ _jsx(ListTable, {
653
+ "aria-label": `Members of ${listName}`,
654
+ rows: members,
655
+ columns: columns,
656
+ rowHeight: TABLE_ROW_HEIGHT,
657
+ // Paged by the footer below, so the grid must not also slice.
658
+ hideFooter: true
659
+ }),
660
+ /*#__PURE__*/ _jsx(ListPagination, {
661
+ page: page,
662
+ pageSize: pageSize,
663
+ rowCount: members.length,
664
+ hasMore: hasMore,
665
+ onPageChange: setPage,
666
+ onPageSizeChange: setPageSize
667
+ })
668
+ ]
669
+ }),
670
+ /*#__PURE__*/ _jsx(Typography, {
671
+ variant: "caption",
672
+ color: "text.secondary",
673
+ children: '"No basis on record" means nobody has recorded this person\'s ' + 'permission for this site — it is not a refusal. To record one, put ' + 'their address in the box above and state that you have their ' + 'permission; the statement is kept against your account, with the ' + 'date.'
674
+ }),
675
+ /*#__PURE__*/ _jsx(Typography, {
676
+ variant: "caption",
677
+ color: "text.secondary",
678
+ children: 'Removing someone from a list is not an unsubscribe. It does not add ' + 'them to your suppression list, and it does not stop any other list ' + 'reaching them. If somebody asked you to stop, suppress the address.'
679
+ })
680
+ ]
681
+ });
682
+ }
683
+ ListMembersPanel.displayName = 'ListMembersPanel';
684
+ export default ListMembersPanel;
685
+
686
+ //# sourceMappingURL=list-members-panel.js.map