@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 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-import-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * BRINGING AN EXISTING LIST IN — the screen.\n *\n * `docs/specs/email-competitive-gaps.md` G5: a customer arriving from another\n * product has a list and, until now, no way to bring it. The server half is\n * `server-list-import.ts`; this is the surface that walks an operator through\n * it.\n *\n * ## A drawer, because an import is three decisions and not a field\n *\n * Choose the file, read what is in it, then say whether you have these\n * people's permission. Stacked above the membership table those three steps\n * would have nowhere to grow — which is exactly how the audience card's\n * create form came to offer four of a rule's nine fields — and the middle\n * step is the one that must not be cramped: it is where the screening\n * warnings and the consent readout are shown, and both exist to be READ\n * before the operator attests.\n *\n * Creating is a drawer and picking is a dialog, so this composes\n * `NavigationDrawerComponent` directly, the same way the suppressions card's\n * Add does — the console's wrapper around it lives in an application a plugin\n * library may not import.\n *\n * ## The numbers on screen are the server's, always\n *\n * Nothing here parses a file, screens it, or decides anything about consent.\n * Every count and every sentence comes back from a route that read the\n * merchant's actual contacts and both suppression lists. A screen that\n * computed its own would be a second copy of the rule on the one surface\n * whose job is to tell the operator the truth about what is about to happen.\n *\n * ## The run is a loop of bounded requests, and it says so\n *\n * A large file is not one request. The drawer calls `email/list-import-run`\n * until it answers `complete`, showing the cursor against the total as it\n * goes, so an import of forty thousand addresses is a progress bar rather\n * than a spinner that either finishes or does not. Closing the drawer stops\n * the loop and loses nothing: the job is durable, and reopening offers to\n * resume it from where it stopped.\n */\n\nimport { LIST_IMPORT_MAX_CHARACTERS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { HelpTip } from '@aglyn/shared-ui-jsx'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n AlertTitle,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n IconButton,\n LinearProgress,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nexport interface ListImportDrawerProps {\n open: boolean\n onClose: () => void\n hostId: string\n listId: string\n listName: string\n /**\n * Called whenever the run enrolls anybody.\n *\n * The audience page's subscriber total is a server aggregate taken once, so\n * an import that ran to completion behind a stale count would read as\n * having done nothing. Fired per run rather than once at the end: a large\n * import is watched, and a total that only moves at the end is a total that\n * looks stuck.\n */\n onMembershipChanged?: () => void\n}\n\n/** One address's verdict, as the preview reports it. */\ninterface AddressVerdict {\n input: string\n email: string | null\n refusal: string | null\n requiresAttestation: boolean\n summary: string\n}\n\n/** What the mechanical screening found. Signals, not verdicts. */\ninterface Screening {\n roleAccounts: number\n roleAccountSamples: string[]\n purchaseTellColumns: string[]\n declaresBasis: boolean\n}\n\n/** `email/list-import-preview`'s answer. */\ninterface ImportPreview {\n columns: string[]\n usable: number\n unusable: number\n duplicates: number\n overCeiling: boolean\n ceiling: number\n unusableSamples: string[]\n screening: Screening\n sampleSize: number\n verdicts: AddressVerdict[]\n optedIn: number\n needAttestation: number\n refused: number\n}\n\n/** The durable job, as `status` and `run` report it. */\ninterface ImportJob {\n importId: string\n status: string\n total: number\n cursor: number\n enrolled: number\n refused: number\n refusals: Record<string, number>\n attested: boolean\n}\n\n/** What a refusal reason is called on screen. */\nconst REFUSAL_LABELS: Record<string, string> = {\n declined: 'Declined marketing email',\n 'no-basis': 'No opt-in on record',\n 'suppressed-host': 'Unsubscribed from this site',\n 'suppressed-platform': 'Bounced or reported as spam',\n 'unroutable-address': 'Not a valid address',\n 'no-address': 'No address',\n}\n\nexport function ListImportDrawer(props: ListImportDrawerProps) {\n const { open, onClose, hostId, listId, listName, onMembershipChanged } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n const [text, setText] = useState('')\n const [fileName, setFileName] = useState('')\n const [preview, setPreview] = useState<ImportPreview | null>(null)\n const [attested, setAttested] = useState(false)\n const [job, setJob] = useState<ImportJob | null>(null)\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state: the loop is an `async` function that has already\n * captured its closure, so a state value it read would be the value from\n * the render that started it — the loop would go on issuing requests after\n * the operator closed the drawer, which is the one thing \"closing stops it\"\n * has to mean.\n */\n const abandoned = useRef(false)\n\n const post = useCallback(\n async (route: string, body: Record<string, unknown>) => {\n const response = await authorizedFetch(user, `/api/email/${route}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, listId, ...body }),\n })\n const payload = await response.json().catch(() => ({}))\n return { ok: response.ok, payload }\n },\n [user, hostId, listId],\n )\n\n /*\n * An unfinished import is looked for when the DRAWER OPENS and at no other\n * time — never on the page's mount. A merchant reading their audiences pays\n * nothing for a feature they did not reach for, which is the read-cost rule\n * `emails-console-read-cost.spec.tsx` meters.\n */\n useEffect(() => {\n if (!open) return undefined\n abandoned.current = false\n let active = true\n void post('list-import-status', {}).then(({ ok, payload }) => {\n if (!active || !ok) return\n const found = payload?.job as ImportJob | null\n if (found && found.status !== 'complete') setJob(found)\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const reset = useCallback(() => {\n setText('')\n setFileName('')\n setPreview(null)\n setAttested(false)\n setJob(null)\n }, [])\n\n const handleClose = useCallback(() => {\n // Stops the loop; the job survives and the next open offers to resume it.\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setFileName(file.name)\n setText(contents)\n setPreview(null)\n setAttested(false)\n },\n [enqueueSnackbar],\n )\n\n const handlePreview = useCallback(async () => {\n if (busy || !text.trim()) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-preview', { text })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The file could not be read.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setPreview(payload as ImportPreview)\n setAttested(false)\n } catch {\n enqueueSnackbar('The file could not be read.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, text, post, enqueueSnackbar])\n\n /**\n * Drives one job to completion, one bounded request at a time.\n *\n * The loop's exit conditions are `complete`, an error, and the operator\n * closing the drawer. It never guesses how many runs a file needs — the\n * server answers `complete` when the cursor has reached the total, which is\n * the only party that knows.\n */\n const runToCompletion = useCallback(\n async (importId: string) => {\n setBusy(true)\n try {\n for (;;) {\n if (abandoned.current) return\n const { ok, payload } = await post('list-import-run', { importId })\n if (!ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n setJob((previous) => ({\n importId,\n status: payload?.complete ? 'complete' : 'running',\n total: Number(payload?.total ?? previous?.total ?? 0),\n cursor: Number(payload?.cursor ?? 0),\n enrolled:\n (previous?.enrolled ?? 0) + Number(payload?.ranEnrolled ?? 0),\n refused:\n (previous?.refused ?? 0) + Number(payload?.ranRefused ?? 0),\n refusals: mergeCounts(\n previous?.refusals ?? {},\n (payload?.refusals ?? {}) as Record<string, number>,\n ),\n attested: previous?.attested ?? false,\n }))\n if (Number(payload?.ranEnrolled ?? 0)) onMembershipChanged?.()\n if (payload?.complete) {\n return void enqueueSnackbar('Import finished', {\n variant: 'success',\n persist: false,\n })\n }\n }\n } finally {\n setBusy(false)\n }\n },\n [post, enqueueSnackbar, onMembershipChanged],\n )\n\n const handleStart = useCallback(async () => {\n if (busy || !preview) return\n setBusy(true)\n try {\n const { ok, payload } = await post('list-import-start', {\n text,\n // The operator's assertion, not a basis they named: the server derives\n // a pass-through from each person's own record, and this flag can only\n // ever add the attributable kind.\n attestConsent: attested,\n })\n if (!ok) {\n setBusy(false)\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not be started.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const started: ImportJob = {\n importId: String(payload?.importId ?? ''),\n status: 'running',\n total: Number(payload?.total ?? 0),\n cursor: 0,\n enrolled: 0,\n refused: 0,\n refusals: {},\n attested: payload?.attested === true,\n }\n setJob(started)\n setBusy(false)\n await runToCompletion(started.importId)\n } catch {\n setBusy(false)\n enqueueSnackbar('The import could not be started.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n }, [busy, preview, text, attested, post, enqueueSnackbar, runToCompletion])\n\n const running = !!job && job.status !== 'complete'\n const finished = !!job && job.status === 'complete'\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {`Import into \"${listName}\"`}\n </Typography>\n {/*\n The help affordance every console surface carries, pointed at the\n section that explains what happens to an imported address — which\n is the question an operator has at exactly this moment.\n */}\n <HelpTip\n {...pluginDocsHelp('emailCampaigns', { anchor: '#import-a-list' })}\n />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {job ? (\n <ImportProgressPanel\n job={job}\n busy={busy}\n onResume={() => void runToCompletion(job.importId)}\n onAnother={reset}\n />\n ) : (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'A CSV with an email column, or one address per line. Every ' +\n 'address goes through the same checks as one you type in ' +\n 'by hand: anyone who unsubscribed, bounced, or told you ' +\n 'they do not want marketing email is refused, and nothing ' +\n 'you upload can override that.'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,.txt,text/csv,text/plain\"\n hidden\n onChange={(event) =>\n void handleFile(event.target.files?.[0])\n }\n />\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n </Stack>\n <TextField\n label=\"Or paste the list\"\n value={text}\n onChange={(event) => {\n setText(event.target.value)\n setFileName('')\n /*\n * Any edit invalidates the answer. A preview belongs to the\n * exact text it was run for, and leaving a stale one on\n * screen would show counts, and an attestation checkbox,\n * for a file the operator has since changed.\n */\n setPreview(null)\n setAttested(false)\n }}\n multiline\n minRows={4}\n maxRows={12}\n fullWidth\n helperText={\n 'One address per line, or paste the whole CSV including ' +\n 'its header row.'\n }\n />\n {preview ? (\n <ImportReviewPanel\n preview={preview}\n attested={attested}\n onAttestedChange={setAttested}\n />\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n {preview ? (\n <Button\n variant=\"contained\"\n disabled={busy || preview.usable === 0}\n onClick={() => void handleStart()}\n >\n {attested\n ? `Import ${preview.usable} addresses`\n : `Import the ${preview.optedIn} with an opt-in`}\n </Button>\n ) : (\n <Button\n variant=\"contained\"\n disabled={busy || !text.trim()}\n onClick={() => void handlePreview()}\n >\n {busy ? 'Checking…' : 'Check this file'}\n </Button>\n )}\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nListImportDrawer.displayName = 'ListImportDrawer'\n\n/** Adds two refusal tallies together. */\nfunction mergeCounts(\n base: Record<string, number>,\n extra: Record<string, number>,\n): Record<string, number> {\n const merged = { ...base }\n for (const [key, value] of Object.entries(extra)) {\n merged[key] = (merged[key] ?? 0) + Number(value ?? 0)\n }\n return merged\n}\n\n/**\n * WHAT IS IN THE FILE, and the one question the operator has to answer.\n *\n * Three blocks in the order they should be read: what the file contains, what\n * the screening noticed, and what the consent gate said about a sample. The\n * attestation control comes last on purpose — it is the act, and an act\n * offered above its own evidence is an act taken without it.\n */\nfunction ImportReviewPanel(props: {\n preview: ImportPreview\n attested: boolean\n onAttestedChange: (value: boolean) => void\n}) {\n const { preview, attested, onAttestedChange } = props\n const { screening } = preview\n return (\n <Stack spacing={2}>\n <Alert severity=\"info\">\n <AlertTitle>{`${preview.usable} addresses in this file`}</AlertTitle>\n <Stack spacing={0.5}>\n {preview.duplicates ? (\n <Typography variant=\"body2\">\n {`${preview.duplicates} repeated addresses were collapsed — the ` +\n 'same person twice is one member.'}\n </Typography>\n ) : null}\n {preview.unusable ? (\n <Typography variant=\"body2\">\n {`${preview.unusable} lines are not email addresses and will be ` +\n `skipped${\n preview.unusableSamples.length\n ? `, starting with: ${preview.unusableSamples\n .slice(0, 3)\n .join(', ')}`\n : ''\n }.`}\n </Typography>\n ) : null}\n {preview.overCeiling ? (\n <Typography variant=\"body2\">\n {`This file names more than ${preview.ceiling} addresses. Only ` +\n 'the first are staged — split the file and import the rest ' +\n 'separately. Nothing already on the list is affected.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {screening.purchaseTellColumns.length ? (\n <Alert severity=\"warning\">\n <AlertTitle>{'This file looks bought or appended'}</AlertTitle>\n {`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` +\n 'Purchased, rented and appended lists are not allowed, and ' +\n 'importing one puts every site sending through this domain at ' +\n 'risk. If the column name is a coincidence, carry on.'}\n </Alert>\n ) : null}\n\n {screening.roleAccounts ? (\n <Alert severity=\"warning\">\n <AlertTitle>\n {`${screening.roleAccounts} shared mailboxes`}\n </AlertTitle>\n {`Addresses like ${screening.roleAccountSamples\n .slice(0, 3)\n .join(', ')} reach a mailbox several people read, or nobody. ` +\n 'They are a common sign of a list built by collecting addresses ' +\n 'rather than by people signing up. They will be imported if you ' +\n 'go ahead.'}\n </Alert>\n ) : null}\n\n <Alert severity={preview.needAttestation ? 'warning' : 'success'}>\n <AlertTitle>\n {preview.sampleSize < preview.usable\n ? `Checked the first ${preview.sampleSize} of ${preview.usable}`\n : 'Checked every address'}\n </AlertTitle>\n <Stack spacing={0.5}>\n <Typography variant=\"body2\">\n {`${preview.optedIn} already have an opt-in on record, ` +\n `${preview.needAttestation} have no opt-in on record, and ` +\n `${preview.refused} cannot be added at all.`}\n </Typography>\n {preview.sampleSize < preview.usable ? (\n <Typography variant=\"body2\">\n {'The rest are checked the same way as the import runs, and ' +\n 'the result is reported as it goes.'}\n </Typography>\n ) : null}\n </Stack>\n </Alert>\n\n {preview.verdicts.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Address'}</TableCell>\n <TableCell>{'What we know'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.verdicts.slice(0, 10).map((verdict, index) => (\n <TableRow key={`${verdict.input}-${index}`}>\n <TableCell>{verdict.email ?? verdict.input}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {verdict.summary}\n </Typography>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n\n {preview.needAttestation ? (\n <FormControlLabel\n control={\n <Checkbox\n checked={attested}\n onChange={(event) => onAttestedChange(event.target.checked)}\n />\n }\n label={\n <Typography variant=\"body2\">\n {'I have these people’s permission to send them marketing ' +\n 'email, and I can produce the record of it if asked. This ' +\n 'statement is stored against my account, with today’s date, ' +\n 'on every address it admits.'}\n {screening.declaresBasis\n ? ' The opt-in source and date your file declares are kept with it.'\n : ''}\n </Typography>\n }\n />\n ) : null}\n {preview.needAttestation && !attested ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Without that statement the import adds only the addresses that ' +\n 'already have an opt-in on record. Nobody else is added, and ' +\n 'nothing is deleted.'}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportProgressPanel(props: {\n job: ImportJob\n busy: boolean\n onResume: () => void\n onAnother: () => void\n}) {\n const { job, busy, onResume, onAnother } = props\n const done = job.status === 'complete'\n const percent = job.total ? Math.min((job.cursor / job.total) * 100, 100) : 0\n const refusals = Object.entries(job.refusals).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {done\n ? `Finished. ${job.enrolled} of ${job.total} addresses were added.`\n : `${job.cursor} of ${job.total} addresses checked.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {refusals.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Not added'}</TableCell>\n <TableCell align=\"right\">{'Addresses'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {refusals.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{REFUSAL_LABELS[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {!done ? (\n <Alert severity=\"info\">\n {'Large files are added in batches, so this takes a few minutes. ' +\n 'You can close this — the import picks up where it stopped when ' +\n 'you come back.'}\n </Alert>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {done ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : (\n <Button variant=\"contained\" disabled={busy} onClick={onResume}>\n {busy ? 'Importing…' : 'Resume import'}\n </Button>\n )}\n </Stack>\n </Stack>\n )\n}\n\nexport default ListImportDrawer\n"],"names":["LIST_IMPORT_MAX_CHARACTERS","pluginDocsHelp","HelpTip","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","Alert","AlertTitle","Button","Checkbox","Chip","FormControlLabel","IconButton","LinearProgress","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useRef","useState","useUser","authorizedFetch","REFUSAL_LABELS","declined","ListImportDrawer","props","open","onClose","hostId","listId","listName","onMembershipChanged","data","user","enqueueSnackbar","text","setText","fileName","setFileName","preview","setPreview","attested","setAttested","job","setJob","busy","setBusy","abandoned","post","route","body","response","method","headers","JSON","stringify","payload","json","catch","ok","undefined","current","active","then","found","status","reset","handleClose","handleFile","file","contents","length","variant","allowDuplicate","name","handlePreview","trim","error","runToCompletion","importId","previous","complete","total","Number","cursor","enrolled","ranEnrolled","refused","ranRefused","refusals","mergeCounts","persist","handleStart","attestConsent","started","String","running","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","gutterY","spacing","ImportProgressPanel","onResume","onAnother","direction","alignItems","disabled","input","type","accept","hidden","onChange","event","target","files","size","label","value","multiline","minRows","maxRows","fullWidth","helperText","ImportReviewPanel","onAttestedChange","justifyContent","usable","optedIn","displayName","base","extra","merged","key","Object","entries","screening","severity","duplicates","unusable","unusableSamples","slice","join","overCeiling","ceiling","purchaseTellColumns","roleAccounts","roleAccountSamples","needAttestation","sampleSize","verdicts","map","verdict","index","email","summary","control","checked","declaresBasis","done","percent","Math","min","filter","count","borderRadius","align","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,SAASA,0BAA0B,EAAEC,cAAc,QAAQ,eAAc;AACzE,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,kBAAkB,QAAQ,2BAA0B;AAC7D,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACjE,SAASC,yBAAyB,QAAQ,8DAA6D;AACvG,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,KAAK,EACLC,UAAU,EACVC,MAAM,EACNC,QAAQ,EACRC,IAAI,EACJC,gBAAgB,EAChBC,UAAU,EACVC,cAAc,EACdC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,eAAe,QAAQ,2CAA0C;AAkE1E,+CAA+C,GAC/C,MAAMC,iBAAyC;IAC7CC,UAAU;IACV,YAAY;IACZ,mBAAmB;IACnB,uBAAuB;IACvB,sBAAsB;IACtB,cAAc;AAChB;AAEA,OAAO,SAASC,iBAAiBC,KAA4B;IAC3D,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,mBAAmB,EAAE,GAAGN;IACzE,MAAM,EAAEO,MAAMC,IAAI,EAAE,GAAGb;IACvB,MAAM,EAAEc,eAAe,EAAE,GAAGlC;IAE5B,MAAM,CAACmC,MAAMC,QAAQ,GAAGjB,SAAS;IACjC,MAAM,CAACkB,UAAUC,YAAY,GAAGnB,SAAS;IACzC,MAAM,CAACoB,SAASC,WAAW,GAAGrB,SAA+B;IAC7D,MAAM,CAACsB,UAAUC,YAAY,GAAGvB,SAAS;IACzC,MAAM,CAACwB,KAAKC,OAAO,GAAGzB,SAA2B;IACjD,MAAM,CAAC0B,MAAMC,QAAQ,GAAG3B,SAAS;IACjC;;;;;;;;GAQC,GACD,MAAM4B,YAAY7B,OAAO;IAEzB,MAAM8B,OAAOhC,YACX,OAAOiC,OAAeC;QACpB,MAAMC,WAAW,MAAM9B,gBAAgBY,MAAM,CAAC,WAAW,EAAEgB,OAAO,EAAE;YAClEG,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CH,MAAMI,KAAKC,SAAS,CAAC;gBAAE3B;gBAAQC;eAAWqB;QAC5C;QACA,MAAMM,UAAU,MAAML,SAASM,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACpD,OAAO;YAAEC,IAAIR,SAASQ,EAAE;YAAEH;QAAQ;IACpC,GACA;QAACvB;QAAML;QAAQC;KAAO;IAGxB;;;;;GAKC,GACDZ,UAAU;QACR,IAAI,CAACS,MAAM,OAAOkC;QAClBb,UAAUc,OAAO,GAAG;QACpB,IAAIC,SAAS;QACb,KAAKd,KAAK,sBAAsB,CAAC,GAAGe,IAAI,CAAC,CAAC,EAAEJ,EAAE,EAAEH,OAAO,EAAE;YACvD,IAAI,CAACM,UAAU,CAACH,IAAI;YACpB,MAAMK,QAAQR,2BAAAA,QAASb,GAAG;YAC1B,IAAIqB,SAASA,MAAMC,MAAM,KAAK,YAAYrB,OAAOoB;QACnD;QACA,OAAO;YACLF,SAAS;QACX;IACA,uDAAuD;IACzD,GAAG;QAACpC;KAAK;IAET,MAAMwC,QAAQlD,YAAY;QACxBoB,QAAQ;QACRE,YAAY;QACZE,WAAW;QACXE,YAAY;QACZE,OAAO;IACT,GAAG,EAAE;IAEL,MAAMuB,cAAcnD,YAAY;QAC9B,0EAA0E;QAC1E+B,UAAUc,OAAO,GAAG;QACpBlC;IACF,GAAG;QAACA;KAAQ;IAEZ,MAAMyC,aAAapD,YACjB,OAAOqD;QACL,IAAI,CAACA,MAAM;QACX,MAAMC,WAAW,MAAMD,KAAKlC,IAAI,GAAGuB,KAAK,CAAC,IAAM;QAC/C,IAAIY,SAASC,MAAM,GAAGhF,4BAA4B;YAChD,OAAO,KAAK2C,gBACV,uEACE,WACF;gBAAEsC,SAAS;gBAAWC,gBAAgB;YAAK;QAE/C;QACAnC,YAAY+B,KAAKK,IAAI;QACrBtC,QAAQkC;QACR9B,WAAW;QACXE,YAAY;IACd,GACA;QAACR;KAAgB;IAGnB,MAAMyC,gBAAgB3D,YAAY;QAChC,IAAI6B,QAAQ,CAACV,KAAKyC,IAAI,IAAI;QAC1B9B,QAAQ;QACR,IAAI;YACF,MAAM,EAAEa,EAAE,EAAEH,OAAO,EAAE,GAAG,MAAMR,KAAK,uBAAuB;gBAAEb;YAAK;YACjE,IAAI,CAACwB,IAAI;;gBACP,OAAO,KAAKzB,wBACVsB,2BAAAA,QAASqB,KAAK,mBAAI,+BAClB;oBAAEL,SAAS;oBAAWC,gBAAgB;gBAAK;YAE/C;YACAjC,WAAWgB;YACXd,YAAY;QACd,EAAE,eAAM;YACNR,gBAAgB,+BAA+B;gBAC7CsC,SAAS;gBACTC,gBAAgB;YAClB;QACF,SAAU;YACR3B,QAAQ;QACV;IACF,GAAG;QAACD;QAAMV;QAAMa;QAAMd;KAAgB;IAEtC;;;;;;;GAOC,GACD,MAAM4C,kBAAkB9D,YACtB,OAAO+D;QACLjC,QAAQ;QACR,IAAI;YACF,OAAS;;gBACP,IAAIC,UAAUc,OAAO,EAAE;gBACvB,MAAM,EAAEF,EAAE,EAAEH,OAAO,EAAE,GAAG,MAAMR,KAAK,mBAAmB;oBAAE+B;gBAAS;gBACjE,IAAI,CAACpB,IAAI;;oBACP,OAAO,KAAKzB,yBACVsB,2BAAAA,QAASqB,KAAK,oBAAI,kCAClB;wBAAEL,SAAS;wBAAWC,gBAAgB;oBAAK;gBAE/C;gBACA7B,OAAO,CAACoC;wBAGQxB;2BAHM;wBACpBuB;wBACAd,QAAQT,CAAAA,2BAAAA,QAASyB,QAAQ,IAAG,aAAa;wBACzCC,OAAOC,QAAO3B,gBAAAA,2BAAAA,QAAS0B,KAAK,oBAAIF,4BAAAA,SAAUE,KAAK,YAAjC1B,OAAqC;wBACnD4B,QAAQD,gBAAO3B,2BAAAA,QAAS4B,MAAM,oBAAI;wBAClCC,UACE,UAACL,4BAAAA,SAAUK,QAAQ,oBAAI,KAAKF,gBAAO3B,2BAAAA,QAAS8B,WAAW,oBAAI;wBAC7DC,SACE,UAACP,4BAAAA,SAAUO,OAAO,oBAAI,KAAKJ,gBAAO3B,2BAAAA,QAASgC,UAAU,oBAAI;wBAC3DC,UAAUC,qBACRV,4BAAAA,SAAUS,QAAQ,oBAAI,CAAC,YACtBjC,2BAAAA,QAASiC,QAAQ,oBAAI,CAAC;wBAEzBhD,QAAQ,WAAEuC,4BAAAA,SAAUvC,QAAQ,oBAAI;oBAClC;;gBACA,IAAI0C,eAAO3B,2BAAAA,QAAS8B,WAAW,mBAAI,IAAIvD,uCAAAA;gBACvC,IAAIyB,2BAAAA,QAASyB,QAAQ,EAAE;oBACrB,OAAO,KAAK/C,gBAAgB,mBAAmB;wBAC7CsC,SAAS;wBACTmB,SAAS;oBACX;gBACF;YACF;QACF,SAAU;YACR7C,QAAQ;QACV;IACF,GACA;QAACE;QAAMd;QAAiBH;KAAoB;IAG9C,MAAM6D,cAAc5E,YAAY;QAC9B,IAAI6B,QAAQ,CAACN,SAAS;QACtBO,QAAQ;QACR,IAAI;;YACF,MAAM,EAAEa,EAAE,EAAEH,OAAO,EAAE,GAAG,MAAMR,KAAK,qBAAqB;gBACtDb;gBACA,uEAAuE;gBACvE,uEAAuE;gBACvE,kCAAkC;gBAClC0D,eAAepD;YACjB;YACA,IAAI,CAACkB,IAAI;;gBACPb,QAAQ;gBACR,OAAO,KAAKZ,yBACVsB,2BAAAA,QAASqB,KAAK,oBAAI,oCAClB;oBAAEL,SAAS;oBAAWC,gBAAgB;gBAAK;YAE/C;YACA,MAAMqB,UAAqB;gBACzBf,UAAUgB,eAAOvC,2BAAAA,QAASuB,QAAQ,mBAAI;gBACtCd,QAAQ;gBACRiB,OAAOC,gBAAO3B,2BAAAA,QAAS0B,KAAK,oBAAI;gBAChCE,QAAQ;gBACRC,UAAU;gBACVE,SAAS;gBACTE,UAAU,CAAC;gBACXhD,UAAUe,CAAAA,2BAAAA,QAASf,QAAQ,MAAK;YAClC;YACAG,OAAOkD;YACPhD,QAAQ;YACR,MAAMgC,gBAAgBgB,QAAQf,QAAQ;QACxC,EAAE,eAAM;YACNjC,QAAQ;YACRZ,gBAAgB,oCAAoC;gBAClDsC,SAAS;gBACTC,gBAAgB;YAClB;QACF;IACF,GAAG;QAAC5B;QAAMN;QAASJ;QAAMM;QAAUO;QAAMd;QAAiB4C;KAAgB;IAE1E,MAAMkB,UAAU,CAAC,CAACrD,OAAOA,IAAIsB,MAAM,KAAK;IACxC,MAAMgC,WAAW,CAAC,CAACtD,OAAOA,IAAIsB,MAAM,KAAK;IAEzC,qBACE,KAACnE;QACC4B,MAAMA;QACNwE,QAAO;QACP1B,SAAQ;QACR7C,SAASwC;QACTgC,aAAa;YAAEC,OAAO;QAAU;QAChCC,0BACE;;8BACE,MAAC9F;oBACC6F,OAAM;oBACNE,MAAK;oBACLC,SAASpC;oBACTqC,IAAI;wBAAEC,IAAI;oBAAE;;sCAEZ,KAAC7G;4BAAQ8G,MAAMhH,mBAAmBgH,IAAI;;sCACtC,KAAC7G;sCAAQ;;;;8BAEX,KAACkB;oBAAWyD,SAAQ;oBAAKmC,WAAU;8BAChC,CAAC,aAAa,EAAE7E,SAAS,CAAC,CAAC;;8BAO9B,KAACrC,sBACKD,eAAe,kBAAkB;oBAAE0G,QAAQ;gBAAiB;;;QAItEU,2BACE,KAACzG;YAAOqE,SAAQ;YAAW4B,OAAM;YAAUG,SAASpC;sBACjD8B,WAAW,SAAS;;kBAIzB,cAAA,KAACtG;YAAUkH,OAAO;sBAChB,cAAA,KAACpG;gBAAMqG,SAAS;0BACbnE,oBACC,KAACoE;oBACCpE,KAAKA;oBACLE,MAAMA;oBACNmE,UAAU,IAAM,KAAKlC,gBAAgBnC,IAAIoC,QAAQ;oBACjDkC,WAAW/C;mCAGb;;sCACE,KAACnD;4BAAWyD,SAAQ;4BAAQ4B,OAAM;sCAC/B,gEACC,6DACA,4DACA,8DACA;;sCAEJ,MAAC3F;4BAAMyG,WAAU;4BAAMJ,SAAS;4BAAGN,IAAI;gCAAEW,YAAY;4BAAS;;8CAC5D,MAAChH;oCAAOqE,SAAQ;oCAAWmC,WAAU;oCAAQS,UAAUvE;;wCACpD;sDACD,KAACwE;4CACCC,MAAK;4CACLC,QAAO;4CACPC,MAAM;4CACNC,UAAU,CAACC;oDACOA;uDAAhB,KAAKtD,YAAWsD,sBAAAA,MAAMC,MAAM,CAACC,KAAK,qBAAlBF,mBAAoB,CAAC,EAAE;;;;;gCAI5CrF,yBACC,KAAChC;oCAAKwH,MAAK;oCAAQrD,SAAQ;oCAAWsD,OAAOzF;qCAC3C;;;sCAEN,KAACvB;4BACCgH,OAAM;4BACNC,OAAO5F;4BACPsF,UAAU,CAACC;gCACTtF,QAAQsF,MAAMC,MAAM,CAACI,KAAK;gCAC1BzF,YAAY;gCACZ;;;;;mBAKC,GACDE,WAAW;gCACXE,YAAY;4BACd;4BACAsF,SAAS;4BACTC,SAAS;4BACTC,SAAS;4BACTC,SAAS;4BACTC,YACE,4DACA;;wBAGH7F,wBACC,KAAC8F;4BACC9F,SAASA;4BACTE,UAAUA;4BACV6F,kBAAkB5F;6BAElB;sCACJ,KAACjC;4BACCyG,WAAU;4BACVJ,SAAS;4BACTN,IAAI;gCAAE+B,gBAAgB;4BAAW;sCAEhChG,wBACC,KAACpC;gCACCqE,SAAQ;gCACR4C,UAAUvE,QAAQN,QAAQiG,MAAM,KAAK;gCACrCjC,SAAS,IAAM,KAAKX;0CAEnBnD,WACG,CAAC,OAAO,EAAEF,QAAQiG,MAAM,CAAC,UAAU,CAAC,GACpC,CAAC,WAAW,EAAEjG,QAAQkG,OAAO,CAAC,eAAe,CAAC;+CAGpD,KAACtI;gCACCqE,SAAQ;gCACR4C,UAAUvE,QAAQ,CAACV,KAAKyC,IAAI;gCAC5B2B,SAAS,IAAM,KAAK5B;0CAEnB9B,OAAO,cAAc;;;;;;;;AAU1C;AACArB,iBAAiBkH,WAAW,GAAG;AAE/B,uCAAuC,GACvC,SAAShD,YACPiD,IAA4B,EAC5BC,KAA6B;IAE7B,MAAMC,SAAS,aAAKF;IACpB,KAAK,MAAM,CAACG,KAAKf,MAAM,IAAIgB,OAAOC,OAAO,CAACJ,OAAQ;YACjCC;QAAfA,MAAM,CAACC,IAAI,GAAG,EAACD,cAAAA,MAAM,CAACC,IAAI,YAAXD,cAAe,KAAK1D,OAAO4C,gBAAAA,QAAS;IACrD;IACA,OAAOc;AACT;AAEA;;;;;;;CAOC,GACD,SAASR,kBAAkB5G,KAI1B;IACC,MAAM,EAAEc,OAAO,EAAEE,QAAQ,EAAE6F,gBAAgB,EAAE,GAAG7G;IAChD,MAAM,EAAEwH,SAAS,EAAE,GAAG1G;IACtB,qBACE,MAAC9B;QAAMqG,SAAS;;0BACd,MAAC7G;gBAAMiJ,UAAS;;kCACd,KAAChJ;kCAAY,GAAGqC,QAAQiG,MAAM,CAAC,uBAAuB,CAAC;;kCACvD,MAAC/H;wBAAMqG,SAAS;;4BACbvE,QAAQ4G,UAAU,iBACjB,KAACpI;gCAAWyD,SAAQ;0CACjB,GAAGjC,QAAQ4G,UAAU,CAAC,yCAAyC,CAAC,GAC/D;iCAEF;4BACH5G,QAAQ6G,QAAQ,iBACf,KAACrI;gCAAWyD,SAAQ;0CACjB,GAAGjC,QAAQ6G,QAAQ,CAAC,2CAA2C,CAAC,GAC/D,CAAC,OAAO,EACN7G,QAAQ8G,eAAe,CAAC9E,MAAM,GAC1B,CAAC,iBAAiB,EAAEhC,QAAQ8G,eAAe,CACxCC,KAAK,CAAC,GAAG,GACTC,IAAI,CAAC,OAAO,GACf,GACL,CAAC,CAAC;iCAEL;4BACHhH,QAAQiH,WAAW,iBAClB,KAACzI;gCAAWyD,SAAQ;0CACjB,CAAC,0BAA0B,EAAEjC,QAAQkH,OAAO,CAAC,iBAAiB,CAAC,GAC9D,+DACA;iCAEF;;;;;YAIPR,UAAUS,mBAAmB,CAACnF,MAAM,iBACnC,MAACtE;gBAAMiJ,UAAS;;kCACd,KAAChJ;kCAAY;;oBACZ,CAAC,oBAAoB,EAAE+I,UAAUS,mBAAmB,CAACH,IAAI,CAAC,MAAM,EAAE,CAAC,GAClE,+DACA,kEACA;;iBAEF;YAEHN,UAAUU,YAAY,iBACrB,MAAC1J;gBAAMiJ,UAAS;;kCACd,KAAChJ;kCACE,GAAG+I,UAAUU,YAAY,CAAC,iBAAiB,CAAC;;oBAE9C,CAAC,eAAe,EAAEV,UAAUW,kBAAkB,CAC5CN,KAAK,CAAC,GAAG,GACTC,IAAI,CAAC,MAAM,iDAAiD,CAAC,GAC9D,oEACA,oEACA;;iBAEF;0BAEJ,MAACtJ;gBAAMiJ,UAAU3G,QAAQsH,eAAe,GAAG,YAAY;;kCACrD,KAAC3J;kCACEqC,QAAQuH,UAAU,GAAGvH,QAAQiG,MAAM,GAChC,CAAC,kBAAkB,EAAEjG,QAAQuH,UAAU,CAAC,IAAI,EAAEvH,QAAQiG,MAAM,EAAE,GAC9D;;kCAEN,MAAC/H;wBAAMqG,SAAS;;0CACd,KAAC/F;gCAAWyD,SAAQ;0CACjB,GAAGjC,QAAQkG,OAAO,CAAC,mCAAmC,CAAC,GACtD,GAAGlG,QAAQsH,eAAe,CAAC,+BAA+B,CAAC,GAC3D,GAAGtH,QAAQgD,OAAO,CAAC,wBAAwB,CAAC;;4BAE/ChD,QAAQuH,UAAU,GAAGvH,QAAQiG,MAAM,iBAClC,KAACzH;gCAAWyD,SAAQ;0CACjB,+DACC;iCAEF;;;;;YAIPjC,QAAQwH,QAAQ,CAACxF,MAAM,iBACtB,MAACxE;gBAAY8H,MAAK;;kCAChB,KAACjH;kCACC,cAAA,MAACC;;8CACC,KAACF;8CAAW;;8CACZ,KAACA;8CAAW;;;;;kCAGhB,KAACD;kCACE6B,QAAQwH,QAAQ,CAACT,KAAK,CAAC,GAAG,IAAIU,GAAG,CAAC,CAACC,SAASC;gCAE7BD;iDADd,MAACpJ;;kDACC,KAACF;mDAAWsJ,iBAAAA,QAAQE,KAAK,YAAbF,iBAAiBA,QAAQ5C,KAAK;;kDAC1C,KAAC1G;kDACC,cAAA,KAACI;4CAAWyD,SAAQ;4CAAQ4B,OAAM;sDAC/B6D,QAAQG,OAAO;;;;+BAJP,GAAGH,QAAQ5C,KAAK,CAAC,CAAC,EAAE6C,OAAO;;;;iBAW9C;YAEH3H,QAAQsH,eAAe,iBACtB,KAACvJ;gBACC+J,uBACE,KAACjK;oBACCkK,SAAS7H;oBACTgF,UAAU,CAACC,QAAUY,iBAAiBZ,MAAMC,MAAM,CAAC2C,OAAO;;gBAG9DxC,qBACE,MAAC/G;oBAAWyD,SAAQ;;wBACjB,6DACC,8DACA,gEACA;wBACDyE,UAAUsB,aAAa,GACpB,qEACA;;;iBAIR;YACHhI,QAAQsH,eAAe,IAAI,CAACpH,yBAC3B,KAAC1B;gBAAWyD,SAAQ;gBAAQ4B,OAAM;0BAC/B,oEACC,iEACA;iBAEF;;;AAGV;AAEA,8CAA8C,GAC9C,SAASW,oBAAoBtF,KAK5B;IACC,MAAM,EAAEkB,GAAG,EAAEE,IAAI,EAAEmE,QAAQ,EAAEC,SAAS,EAAE,GAAGxF;IAC3C,MAAM+I,OAAO7H,IAAIsB,MAAM,KAAK;IAC5B,MAAMwG,UAAU9H,IAAIuC,KAAK,GAAGwF,KAAKC,GAAG,CAAC,AAAChI,IAAIyC,MAAM,GAAGzC,IAAIuC,KAAK,GAAI,KAAK,OAAO;IAC5E,MAAMO,WAAWsD,OAAOC,OAAO,CAACrG,IAAI8C,QAAQ,EAAEmF,MAAM,CAAC,CAAC,GAAGC,MAAM,GAAKA,QAAQ;IAC5E,qBACE,MAACpK;QAAMqG,SAAS;;0BACd,KAAC/F;gBAAWyD,SAAQ;gBAAQ4B,OAAM;0BAC/BoE,OACG,CAAC,UAAU,EAAE7H,IAAI0C,QAAQ,CAAC,IAAI,EAAE1C,IAAIuC,KAAK,CAAC,sBAAsB,CAAC,GACjE,GAAGvC,IAAIyC,MAAM,CAAC,IAAI,EAAEzC,IAAIuC,KAAK,CAAC,mBAAmB,CAAC;;0BAExD,KAAC1E;gBACCgE,SAAQ;gBACRuD,OAAO0C;gBACPjE,IAAI;oBAAEsE,cAAc;gBAAE;;YAEvBrF,SAASlB,MAAM,iBACd,MAACxE;gBAAY8H,MAAK;;kCAChB,KAACjH;kCACC,cAAA,MAACC;;8CACC,KAACF;8CAAW;;8CACZ,KAACA;oCAAUoK,OAAM;8CAAS;;;;;kCAG9B,KAACrK;kCACE+E,SAASuE,GAAG,CAAC,CAAC,CAACgB,QAAQH,MAAM;gCAEdvJ;iDADd,MAACT;;kDACC,KAACF;mDAAWW,yBAAAA,cAAc,CAAC0J,OAAO,YAAtB1J,yBAA0B0J;;kDACtC,KAACrK;wCAAUoK,OAAM;kDAASF;;;+BAFbG;;;;iBAOnB;YACH,CAACR,qBACA,KAACvK;gBAAMiJ,UAAS;0BACb,oEACC,oEACA;iBAEF;0BACJ,KAACzI;gBAAMyG,WAAU;gBAAMJ,SAAS;gBAAGN,IAAI;oBAAE+B,gBAAgB;gBAAW;0BACjEiC,qBACC,KAACrK;oBAAOqE,SAAQ;oBAAW+B,SAASU;8BACjC;mCAGH,KAAC9G;oBAAOqE,SAAQ;oBAAY4C,UAAUvE;oBAAM0D,SAASS;8BAClDnE,OAAO,eAAe;;;;;AAMnC;AAEA,eAAerB,iBAAgB"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Who is on one list, and the three things you can do about it.
3
+ *
4
+ * The audience card could create a list and delete a list. It could not show
5
+ * you a single name on one. This panel is the rest of it: read the membership,
6
+ * add somebody, take somebody off. The list's own settings — its name, and the
7
+ * rule behind a dynamic one — belong to the edit route, because they are
8
+ * facts about the LIST rather than about who is on it.
9
+ *
10
+ * ## It mounts only on a list's own page
11
+ *
12
+ * The audience detail route renders this for the ONE list being read. A
13
+ * membership listener per row would open one per list on the audiences table,
14
+ * on a surface where most visits are to read the list of lists — the
15
+ * read-on-mount shape `emails-console-read-cost.spec.tsx` meters. Nothing here
16
+ * reads anything until a list is opened, and the add form reads nothing until
17
+ * an address is typed.
18
+ *
19
+ * ## Whether somebody may be ADDED is the server's answer, always
20
+ *
21
+ * Every sentence about an address being typed in is `email/list-members-preview`
22
+ * answering from that person's actual record and both suppression lists. This
23
+ * component decides none of it. A screen that decided for itself would be a
24
+ * second copy of the rule on the one surface whose job is to tell the operator
25
+ * the truth about what is about to happen — including, and especially, that
26
+ * the product knows nothing about this person's permission.
27
+ *
28
+ * ## The membership table reads consent through the SEND-TIME reader
29
+ *
30
+ * The `Consent` column answers the same question `performCampaignSend` asks of
31
+ * the same document, so it asks it with the same function —
32
+ * {@link readMarketingBasis}, against the consent group this console is being
33
+ * viewed as. A column that picked the fields out of the row itself is a second
34
+ * reader of a stored shape, and the two only agree until the shape moves.
35
+ *
36
+ * ## Removing is not suppressing, and the screen says so
37
+ *
38
+ * Taking somebody off a list is the operator changing their mind about an
39
+ * audience. It is not the person asking to stop being mailed, it does not stop
40
+ * another list reaching them, and it writes nothing to the suppression list.
41
+ * Those are different acts with different records, and a console that let them
42
+ * blur is a console where "remove" is quietly relied on as an unsubscribe.
43
+ */
44
+ import { type ConsentGroup, type DynamicListRule } from '@aglyn/aglyn';
45
+ export interface ListMembersPanelProps {
46
+ hostId: string;
47
+ /**
48
+ * The controller this membership is being read AS — the declared group of
49
+ * sites that are one sender, or this site alone.
50
+ *
51
+ * Required and undefaulted, for the reason {@link readMarketingBasis} makes
52
+ * it required: a basis runs to a brand, so a surface that has not said which
53
+ * brand is asking cannot report one. Defaulting to the group of one here
54
+ * would compile and would then answer a declared group's questions with a
55
+ * single site's facts — reporting a sibling's refusal as no record at all,
56
+ * and a pooled grant as another brand's.
57
+ */
58
+ consentGroup: ConsentGroup;
59
+ /** `['orgs', orgId]` — the resolved org scope the card already holds. */
60
+ scope: readonly [string, string];
61
+ listId: string;
62
+ listName: string;
63
+ /**
64
+ * Called after a membership is added or removed.
65
+ *
66
+ * The audience page's subscriber total is a server aggregate, taken once.
67
+ * Left alone it would go on reporting the figure from before the add — a
68
+ * stale total directly above a live table that disagrees with it, which
69
+ * reads as the add having failed. The page re-takes the aggregate on this
70
+ * signal and at no other time, so nothing is re-counted while somebody is
71
+ * only reading.
72
+ */
73
+ onMembershipChanged?: () => void;
74
+ /**
75
+ * The list's saved filters, when it has any and its membership is FIXED.
76
+ *
77
+ * Present, the panel offers to find the people they select and add them —
78
+ * the same act as typing addresses, with the search doing the typing. Absent
79
+ * (a live list, or one with no filters saved), the add form is the address
80
+ * box alone: a live list's membership is the sweep's to decide, and offering
81
+ * to add its own matches by hand would produce hand-added copies of rows the
82
+ * sweep already owns.
83
+ */
84
+ findRule?: DynamicListRule | null;
85
+ /** The same filters in sentences, so the button says what it will find. */
86
+ ruleSummary?: readonly string[];
87
+ }
88
+ /** Free text — a paste, a typed address, commas or newlines — to addresses. */
89
+ export declare function splitAddresses(value: string): string[];
90
+ export declare function ListMembersPanel(props: ListMembersPanelProps): import("react").JSX.Element;
91
+ export declare namespace ListMembersPanel {
92
+ var displayName: string;
93
+ }
94
+ export default ListMembersPanel;