@aglyn/plugins-email 1.0.0-beta.221 → 1.0.0-beta.223
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/README.md +5 -4
- package/package.json +16 -17
- package/src/lib/components/email-template-detail.d.ts +5 -13
- package/src/lib/components/email-template-detail.js +29 -426
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/email-zones.d.ts +15 -0
- package/src/lib/components/email-zones.js +1 -0
- package/src/lib/components/email-zones.js.map +1 -1
- package/src/lib/components/list-detail-card.d.ts +0 -36
- package/src/lib/components/list-detail-card.js +125 -84
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-members-panel.d.ts +17 -0
- package/src/lib/components/list-members-panel.js +14 -0
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/site-package-email-preview.d.ts +13 -0
- package/src/lib/components/site-package-email-preview.js +47 -0
- package/src/lib/components/site-package-email-preview.js.map +1 -0
- package/src/lib/components/suppressions-card.js +75 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/components/use-org-email-topics.js +2 -9
- package/src/lib/components/use-org-email-topics.js.map +1 -1
- package/src/lib/constants/site-package.d.ts +21 -0
- package/src/lib/constants/site-package.js +21 -0
- package/src/lib/constants/site-package.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +12 -0
- package/src/lib/declarations.console-server.js +73 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/list-import.d.ts +64 -75
- package/src/lib/list-import.js +43 -158
- package/src/lib/list-import.js.map +1 -1
- package/src/lib/model/email-topic-catalog.d.ts +24 -0
- package/src/lib/model/email-topic-catalog.js +19 -0
- package/src/lib/model/email-topic-catalog.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -25
- package/src/lib/model/index.js +0 -27
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.d.ts +8 -0
- package/src/lib/plugin.js +76 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +39 -0
- package/src/lib/server/person-eraser.js +44 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server-console.js +9 -9
- package/src/lib/server-console.js.map +1 -1
- package/src/lib/server-list-gate.d.ts +7 -0
- package/src/lib/server-list-gate.js +10 -3
- package/src/lib/server-list-gate.js.map +1 -1
- package/src/lib/server-list-import.d.ts +20 -177
- package/src/lib/server-list-import.js +34 -594
- package/src/lib/server-list-import.js.map +1 -1
- package/src/lib/server.js +16 -29
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-send-tally.d.ts +29 -0
- package/src/lib/testing/stand-in-send-tally.js +52 -0
- package/src/lib/testing/stand-in-send-tally.js.map +1 -0
- package/src/lib/transfer/email-transfer-catalog.d.ts +151 -0
- package/src/lib/transfer/email-transfer-catalog.js +536 -0
- package/src/lib/transfer/email-transfer-catalog.js.map +1 -0
- package/src/lib/transfer/list-import-steps.d.ts +44 -0
- package/src/lib/transfer/list-import-steps.js +201 -0
- package/src/lib/transfer/list-import-steps.js.map +1 -0
- package/src/lib/transfer/list-members.server.d.ts +165 -0
- package/src/lib/transfer/list-members.server.js +911 -0
- package/src/lib/transfer/list-members.server.js.map +1 -0
- package/src/lib/transfer/suppressions.server.d.ts +69 -0
- package/src/lib/transfer/suppressions.server.js +282 -0
- package/src/lib/transfer/suppressions.server.js.map +1 -0
- package/src/lib/transfer/topics-package.d.ts +56 -0
- package/src/lib/transfer/topics-package.js +87 -0
- package/src/lib/transfer/topics-package.js.map +1 -0
- package/src/lib/transfer/topics-package.server.d.ts +33 -0
- package/src/lib/transfer/topics-package.server.js +171 -0
- package/src/lib/transfer/topics-package.server.js.map +1 -0
- package/src/lib/transfer/transfer-reads.server.d.ts +25 -0
- package/src/lib/transfer/transfer-reads.server.js +34 -0
- package/src/lib/transfer/transfer-reads.server.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +0 -22
- package/src/lib/components/list-import-drawer.js +0 -662
- package/src/lib/components/list-import-drawer.js.map +0 -1
- package/src/lib/model/template-report.d.ts +0 -158
- package/src/lib/model/template-report.js +0 -249
- package/src/lib/model/template-report.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
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 { CSV_UPLOAD_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 > CSV_UPLOAD_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":["CSV_UPLOAD_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":"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"}
|
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* ONE DESIGN, MEASURED ACROSS EVERY CAMPAIGN THAT USED IT.
|
|
19
|
-
*
|
|
20
|
-
* ## What this adds to `campaign-report.ts`
|
|
21
|
-
*
|
|
22
|
-
* A campaign report divides two numbers that were recorded by the same send.
|
|
23
|
-
* A design report divides two SUMS, and a sum has a membership: which
|
|
24
|
-
* campaigns went into it. That is the whole difficulty, and it is a new way
|
|
25
|
-
* for the repo's rule — every rate names its denominator — to be broken.
|
|
26
|
-
*
|
|
27
|
-
* ## The rule, restated for a sum
|
|
28
|
-
*
|
|
29
|
-
* **A rate's numerator and its denominator are summed over the SAME set of
|
|
30
|
-
* campaigns, and the screen is told how large that set is.**
|
|
31
|
-
*
|
|
32
|
-
* `delivered` is written only by the delivery webhook, so a design used by
|
|
33
|
-
* five campaigns may have three that recorded it and two that never did.
|
|
34
|
-
* Summing `uniqueOpens` over all five and dividing by `delivered` summed over
|
|
35
|
-
* three produces an open rate that can exceed 100% and is, in every case,
|
|
36
|
-
* the flattering substitution `campaign-report.ts` exists to refuse — carried
|
|
37
|
-
* out by addition instead of by picking the wrong field.
|
|
38
|
-
*
|
|
39
|
-
* So each rate here is built from a FILTERED subset, and
|
|
40
|
-
* {@link TemplateRate.denominatorLabel} names the subset as well as the
|
|
41
|
-
* quantity: `delivered across 3 of 5 campaigns`. A reader comparing this
|
|
42
|
-
* design's open rate with another's can see the two were taken over different
|
|
43
|
-
* populations without being told separately.
|
|
44
|
-
*
|
|
45
|
-
* ## Counts are over everything; rates are over what can be divided
|
|
46
|
-
*
|
|
47
|
-
* The COUNTS — opens, clicks, unsubscribes — are summed over every campaign,
|
|
48
|
-
* because a count is true whatever else was recorded. Only the rates narrow.
|
|
49
|
-
* The two are labelled differently on screen for that reason, and the caveats
|
|
50
|
-
* say how many campaigns each rate left out.
|
|
51
|
-
*
|
|
52
|
-
* ## Why this is a pure module
|
|
53
|
-
*
|
|
54
|
-
* Same reason as `campaign-report.ts`: a division written in JSX is a
|
|
55
|
-
* denominator chosen by whoever writes the next card. Here it is worse — the
|
|
56
|
-
* subset is invisible in the output unless something carries it — so the
|
|
57
|
-
* subset is data, computed once, and the screen renders it.
|
|
58
|
-
*/
|
|
59
|
-
import { type CampaignRate, type CampaignStats } from '@aglyn/shared-ui-email-campaigns/model/campaign-report';
|
|
60
|
-
/** A rate over a sum, carrying how many campaigns the sum covers. */
|
|
61
|
-
export type TemplateRate = CampaignRate;
|
|
62
|
-
/**
|
|
63
|
-
* One campaign that used the design, as the report reads it.
|
|
64
|
-
*
|
|
65
|
-
* `sentAtMs` is nullable because a campaign document can exist without ever
|
|
66
|
-
* having been sent — a scheduled campaign holds `sendAtMs` and no `sentAt`.
|
|
67
|
-
* Those carry no engagement and the reader is shown their status rather than
|
|
68
|
-
* a row of zeroes.
|
|
69
|
-
*/
|
|
70
|
-
export interface TemplateCampaign {
|
|
71
|
-
campaignId: string;
|
|
72
|
-
subject: string;
|
|
73
|
-
/** Epoch ms, or null for a campaign that has not been sent. */
|
|
74
|
-
sentAtMs: number | null;
|
|
75
|
-
/** `'sent'`, `'scheduled'`, `'canceled'`. A persisted value; not relabeled. */
|
|
76
|
-
status: string;
|
|
77
|
-
/** The audience KIND — `'leads'`, `'members'`, `'segment'`, `'list'`. */
|
|
78
|
-
audience: string;
|
|
79
|
-
/** Set when `audience` is `'list'`. */
|
|
80
|
-
listId?: string;
|
|
81
|
-
/** The list's name AS IT WAS at send time, when the send recorded one. */
|
|
82
|
-
listName?: string;
|
|
83
|
-
stats: CampaignStats | undefined;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* One audience the design was sent to.
|
|
87
|
-
*
|
|
88
|
-
* A LIST is named by the name the send recorded, not by the name the list
|
|
89
|
-
* carries today: a renamed or deleted list would otherwise rewrite the
|
|
90
|
-
* history of a campaign that went out months ago, and a deleted one would
|
|
91
|
-
* erase it. The same reasoning as the campaign report's populations.
|
|
92
|
-
*/
|
|
93
|
-
export interface TemplateAudience {
|
|
94
|
-
/** Stable key — the list id, or the audience kind for the built-ins. */
|
|
95
|
-
id: string;
|
|
96
|
-
label: string;
|
|
97
|
-
/** How many campaigns using this design went to this audience. */
|
|
98
|
-
campaigns: number;
|
|
99
|
-
/** Addresses those campaigns ADDRESSED, summed. */
|
|
100
|
-
addressed: number;
|
|
101
|
-
/**
|
|
102
|
-
* The named list has since been deleted or renamed out of reach.
|
|
103
|
-
*
|
|
104
|
-
* Only ever true for a `list` audience, and only when the send recorded no
|
|
105
|
-
* name — an older send, before the name was stored. The screen says "list
|
|
106
|
-
* no longer named" rather than printing a raw document id as if it were
|
|
107
|
-
* something a merchant would recognise.
|
|
108
|
-
*/
|
|
109
|
-
unnamed?: boolean;
|
|
110
|
-
}
|
|
111
|
-
/** Why a number the design report would otherwise show is being withheld. */
|
|
112
|
-
export interface TemplateCaveat {
|
|
113
|
-
id: 'no-campaigns' | 'delivery-partial' | 'click-tracking-partial' | 'campaigns-truncated' | 'audience-truncated';
|
|
114
|
-
message: string;
|
|
115
|
-
}
|
|
116
|
-
/** Everything the design report screen renders. */
|
|
117
|
-
export interface TemplateReport {
|
|
118
|
-
/** Campaigns that used this design AND were sent. */
|
|
119
|
-
sentCampaigns: number;
|
|
120
|
-
/** Campaigns that used this design, sent or not. */
|
|
121
|
-
totalCampaigns: number;
|
|
122
|
-
/** Epoch ms of the most recent send, or null. */
|
|
123
|
-
lastSentAtMs: number | null;
|
|
124
|
-
recipients: number;
|
|
125
|
-
sent: number;
|
|
126
|
-
/** Summed over the campaigns that recorded it; null when none did. */
|
|
127
|
-
delivered: number | null;
|
|
128
|
-
opens: number;
|
|
129
|
-
clicks: number;
|
|
130
|
-
uniqueOpens: number | null;
|
|
131
|
-
uniqueClicks: number | null;
|
|
132
|
-
bounced: number;
|
|
133
|
-
complained: number;
|
|
134
|
-
unsubscribes: number;
|
|
135
|
-
/** Rates, each `null` when its denominator is zero or unrecorded. */
|
|
136
|
-
rates: {
|
|
137
|
-
delivery: TemplateRate | null;
|
|
138
|
-
open: TemplateRate | null;
|
|
139
|
-
click: TemplateRate | null;
|
|
140
|
-
clickToOpen: TemplateRate | null;
|
|
141
|
-
bounce: TemplateRate | null;
|
|
142
|
-
complaint: TemplateRate | null;
|
|
143
|
-
unsubscribe: TemplateRate | null;
|
|
144
|
-
};
|
|
145
|
-
audiences: TemplateAudience[];
|
|
146
|
-
caveats: TemplateCaveat[];
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Aggregates every campaign that used one design.
|
|
150
|
-
*
|
|
151
|
-
* @param campaigns every campaign document naming this design, sent or not.
|
|
152
|
-
* @param truncated the query that produced `campaigns` stopped at its read
|
|
153
|
-
* ceiling, so every total here is a FLOOR. Reported as a caveat rather
|
|
154
|
-
* than hidden, on the same reasoning as `audienceSizeTruncated`: a
|
|
155
|
-
* figure that is at least this large is useful, and a figure presented
|
|
156
|
-
* as complete when it is not is worse than none.
|
|
157
|
-
*/
|
|
158
|
-
export declare function templateReport(campaigns: readonly TemplateCampaign[], truncated?: boolean): TemplateReport;
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
/**
|
|
3
|
-
* @license
|
|
4
|
-
* Copyright 2026 Aglyn LLC
|
|
5
|
-
*
|
|
6
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
* you may not use this file except in compliance with the License.
|
|
8
|
-
* You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
* See the License for the specific language governing permissions and
|
|
16
|
-
* limitations under the License.
|
|
17
|
-
*/ /**
|
|
18
|
-
* ONE DESIGN, MEASURED ACROSS EVERY CAMPAIGN THAT USED IT.
|
|
19
|
-
*
|
|
20
|
-
* ## What this adds to `campaign-report.ts`
|
|
21
|
-
*
|
|
22
|
-
* A campaign report divides two numbers that were recorded by the same send.
|
|
23
|
-
* A design report divides two SUMS, and a sum has a membership: which
|
|
24
|
-
* campaigns went into it. That is the whole difficulty, and it is a new way
|
|
25
|
-
* for the repo's rule — every rate names its denominator — to be broken.
|
|
26
|
-
*
|
|
27
|
-
* ## The rule, restated for a sum
|
|
28
|
-
*
|
|
29
|
-
* **A rate's numerator and its denominator are summed over the SAME set of
|
|
30
|
-
* campaigns, and the screen is told how large that set is.**
|
|
31
|
-
*
|
|
32
|
-
* `delivered` is written only by the delivery webhook, so a design used by
|
|
33
|
-
* five campaigns may have three that recorded it and two that never did.
|
|
34
|
-
* Summing `uniqueOpens` over all five and dividing by `delivered` summed over
|
|
35
|
-
* three produces an open rate that can exceed 100% and is, in every case,
|
|
36
|
-
* the flattering substitution `campaign-report.ts` exists to refuse — carried
|
|
37
|
-
* out by addition instead of by picking the wrong field.
|
|
38
|
-
*
|
|
39
|
-
* So each rate here is built from a FILTERED subset, and
|
|
40
|
-
* {@link TemplateRate.denominatorLabel} names the subset as well as the
|
|
41
|
-
* quantity: `delivered across 3 of 5 campaigns`. A reader comparing this
|
|
42
|
-
* design's open rate with another's can see the two were taken over different
|
|
43
|
-
* populations without being told separately.
|
|
44
|
-
*
|
|
45
|
-
* ## Counts are over everything; rates are over what can be divided
|
|
46
|
-
*
|
|
47
|
-
* The COUNTS — opens, clicks, unsubscribes — are summed over every campaign,
|
|
48
|
-
* because a count is true whatever else was recorded. Only the rates narrow.
|
|
49
|
-
* The two are labelled differently on screen for that reason, and the caveats
|
|
50
|
-
* say how many campaigns each rate left out.
|
|
51
|
-
*
|
|
52
|
-
* ## Why this is a pure module
|
|
53
|
-
*
|
|
54
|
-
* Same reason as `campaign-report.ts`: a division written in JSX is a
|
|
55
|
-
* denominator chosen by whoever writes the next card. Here it is worse — the
|
|
56
|
-
* subset is invisible in the output unless something carries it — so the
|
|
57
|
-
* subset is data, computed once, and the screen renders it.
|
|
58
|
-
*/ import { campaignRate } from "@aglyn/shared-ui-email-campaigns/model/campaign-report";
|
|
59
|
-
import { emailAudienceLabel } from "@aglyn/shared-ui-email-campaigns/model/email-record";
|
|
60
|
-
/** A campaign that actually went out. The only kind engagement can describe. */ function wasSent(campaign) {
|
|
61
|
-
return campaign.sentAtMs !== null;
|
|
62
|
-
}
|
|
63
|
-
/** Sums one stats field over a set of campaigns, defaulting absent to zero. */ function total(campaigns, field) {
|
|
64
|
-
return campaigns.reduce((running, campaign)=>{
|
|
65
|
-
var _ref;
|
|
66
|
-
var _campaign_stats;
|
|
67
|
-
return running + Number((_ref = (_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats[field]) != null ? _ref : 0);
|
|
68
|
-
}, 0);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* `${quantity} across N of M campaigns` — the denominator label for a sum.
|
|
72
|
-
*
|
|
73
|
-
* The `of M` half is what makes a narrowed rate readable. `delivered across 3
|
|
74
|
-
* campaigns` is true and still invites the reader to assume the design has
|
|
75
|
-
* three; `across 3 of 5` says on its face that two are missing from this
|
|
76
|
-
* number and not from the counts above it.
|
|
77
|
-
*/ function acrossLabel(quantity, covered, all) {
|
|
78
|
-
return covered === all ? `${quantity} across ${covered} ${covered === 1 ? 'campaign' : 'campaigns'}` : `${quantity} across ${covered} of ${all} campaigns`;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Aggregates every campaign that used one design.
|
|
82
|
-
*
|
|
83
|
-
* @param campaigns every campaign document naming this design, sent or not.
|
|
84
|
-
* @param truncated the query that produced `campaigns` stopped at its read
|
|
85
|
-
* ceiling, so every total here is a FLOOR. Reported as a caveat rather
|
|
86
|
-
* than hidden, on the same reasoning as `audienceSizeTruncated`: a
|
|
87
|
-
* figure that is at least this large is useful, and a figure presented
|
|
88
|
-
* as complete when it is not is worse than none.
|
|
89
|
-
*/ export function templateReport(campaigns, truncated = false) {
|
|
90
|
-
const sentCampaigns = campaigns.filter(wasSent);
|
|
91
|
-
const all = sentCampaigns.length;
|
|
92
|
-
/*
|
|
93
|
-
* THE SUBSETS. Each rate divides sums taken over one of these and nothing
|
|
94
|
-
* else, which is what makes the numerator and denominator describe the same
|
|
95
|
-
* population — see this module's header.
|
|
96
|
-
*/ const withDelivery = sentCampaigns.filter((campaign)=>{
|
|
97
|
-
var _campaign_stats;
|
|
98
|
-
return ((_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.delivered) !== undefined;
|
|
99
|
-
});
|
|
100
|
-
const withUniqueOpens = withDelivery.filter((campaign)=>{
|
|
101
|
-
var _campaign_stats;
|
|
102
|
-
return ((_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.uniqueOpens) !== undefined;
|
|
103
|
-
});
|
|
104
|
-
/*
|
|
105
|
-
* Click rates need BOTH a delivery denominator and the record that the send
|
|
106
|
-
* carried an HTML part. A send with no HTML part reports zero clicks
|
|
107
|
-
* whatever recipients did, so including it in a click rate's denominator
|
|
108
|
-
* drags the rate down by however many untrackable sends this design has —
|
|
109
|
-
* a measurement of our own sending code presented as a measurement of the
|
|
110
|
-
* audience. `campaign-report.ts` refuses the same substitution for one
|
|
111
|
-
* campaign; a sum must refuse it per campaign, not for the design as a
|
|
112
|
-
* whole, or one old send would withhold the design's whole click rate.
|
|
113
|
-
*/ const withClickTracking = withDelivery.filter((campaign)=>{
|
|
114
|
-
var _campaign_stats, _campaign_stats1;
|
|
115
|
-
return ((_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.clickTracked) === true && ((_campaign_stats1 = campaign.stats) == null ? void 0 : _campaign_stats1.uniqueClicks) !== undefined;
|
|
116
|
-
});
|
|
117
|
-
const withOpenersAndClicks = withClickTracking.filter((campaign)=>{
|
|
118
|
-
var _campaign_stats;
|
|
119
|
-
return ((_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.uniqueOpens) !== undefined;
|
|
120
|
-
});
|
|
121
|
-
const sent = total(sentCampaigns, 'sent');
|
|
122
|
-
const delivered = withDelivery.length ? total(withDelivery, 'delivered') : null;
|
|
123
|
-
const uniqueOpens = withUniqueOpens.length ? total(withUniqueOpens, 'uniqueOpens') : null;
|
|
124
|
-
const uniqueClicks = withClickTracking.length ? total(withClickTracking, 'uniqueClicks') : null;
|
|
125
|
-
const rates = {
|
|
126
|
-
/*
|
|
127
|
-
* Over the campaigns that recorded a delivery, and `sent` summed over
|
|
128
|
-
* those SAME campaigns rather than over all of them. The alternative —
|
|
129
|
-
* delivered over the design's whole `sent` — reads as a delivery failure
|
|
130
|
-
* proportional to how many of this design's sends predate the webhook.
|
|
131
|
-
*/ delivery: delivered === null ? null : campaignRate(delivered, total(withDelivery, 'sent'), acrossLabel('sent', withDelivery.length, all)),
|
|
132
|
-
open: campaignRate(uniqueOpens != null ? uniqueOpens : undefined, withUniqueOpens.length ? total(withUniqueOpens, 'delivered') : undefined, acrossLabel('delivered', withUniqueOpens.length, all)),
|
|
133
|
-
click: campaignRate(uniqueClicks != null ? uniqueClicks : undefined, withClickTracking.length ? total(withClickTracking, 'delivered') : undefined, acrossLabel('delivered', withClickTracking.length, all)),
|
|
134
|
-
clickToOpen: campaignRate(withOpenersAndClicks.length ? total(withOpenersAndClicks, 'uniqueClicks') : undefined, withOpenersAndClicks.length ? total(withOpenersAndClicks, 'uniqueOpens') : undefined, acrossLabel('unique openers', withOpenersAndClicks.length, all)),
|
|
135
|
-
/*
|
|
136
|
-
* Bounces and their rate are over every sent campaign, because both
|
|
137
|
-
* halves are recorded for all of them: `sent` by the send itself and
|
|
138
|
-
* `bounced` by a webhook whose absence is a genuine zero — no bounce
|
|
139
|
-
* event means nothing bounced, unlike `delivered`, whose absence means
|
|
140
|
-
* nothing was measured.
|
|
141
|
-
*/ bounce: campaignRate(total(sentCampaigns, 'bounced'), sent, acrossLabel('sent', all, all)),
|
|
142
|
-
complaint: campaignRate(withDelivery.length ? total(withDelivery, 'complained') : undefined, delivered != null ? delivered : undefined, acrossLabel('delivered', withDelivery.length, all)),
|
|
143
|
-
unsubscribe: campaignRate(withDelivery.length ? total(withDelivery, 'unsubscribes') : undefined, delivered != null ? delivered : undefined, acrossLabel('delivered', withDelivery.length, all))
|
|
144
|
-
};
|
|
145
|
-
const caveats = [];
|
|
146
|
-
if (!all) {
|
|
147
|
-
caveats.push({
|
|
148
|
-
id: 'no-campaigns',
|
|
149
|
-
message: campaigns.length ? 'No campaign using this design has been sent yet, so there is ' + 'nothing to measure. The campaigns below are scheduled or were ' + 'canceled before they went out.' : 'This design has never been sent, so there is nothing to measure.'
|
|
150
|
-
});
|
|
151
|
-
} else if (withDelivery.length < all) {
|
|
152
|
-
caveats.push({
|
|
153
|
-
id: 'delivery-partial',
|
|
154
|
-
message: `${all - withDelivery.length} of ${all} campaigns using this design ` + 'recorded no delivery events, so they are left out of every rate ' + 'taken over delivered — open, click, complaint and unsubscribe. ' + 'Their counts are still included above.'
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
if (all && withClickTracking.length < withDelivery.length) {
|
|
158
|
-
caveats.push({
|
|
159
|
-
id: 'click-tracking-partial',
|
|
160
|
-
message: `${withDelivery.length - withClickTracking.length} of these campaigns ` + 'did not record carrying an HTML part. Click tracking rewrites links ' + 'in the HTML, so those sends report zero clicks whatever recipients ' + 'did, and they are left out of the click rates rather than counted ' + 'as sends nobody clicked.'
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
if (truncated) {
|
|
164
|
-
caveats.push({
|
|
165
|
-
id: 'campaigns-truncated',
|
|
166
|
-
message: 'This design has been used by more campaigns than one read returns, ' + 'so every total here is a floor — the real figures are at least ' + 'this large.'
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
if (sentCampaigns.some((campaign)=>{
|
|
170
|
-
var _campaign_stats;
|
|
171
|
-
return (_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.audienceSizeTruncated;
|
|
172
|
-
})) {
|
|
173
|
-
caveats.push({
|
|
174
|
-
id: 'audience-truncated',
|
|
175
|
-
message: 'At least one of these sends stopped audience resolution at its read ' + 'ceiling, so the audience figures it contributed are floors.'
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
sentCampaigns: all,
|
|
180
|
-
totalCampaigns: campaigns.length,
|
|
181
|
-
lastSentAtMs: sentCampaigns.reduce((latest, campaign)=>{
|
|
182
|
-
var _campaign_sentAtMs;
|
|
183
|
-
return latest === null || ((_campaign_sentAtMs = campaign.sentAtMs) != null ? _campaign_sentAtMs : 0) > latest ? campaign.sentAtMs : latest;
|
|
184
|
-
}, null),
|
|
185
|
-
recipients: total(sentCampaigns, 'recipients'),
|
|
186
|
-
sent,
|
|
187
|
-
delivered,
|
|
188
|
-
opens: total(sentCampaigns, 'opens'),
|
|
189
|
-
clicks: total(sentCampaigns, 'clicks'),
|
|
190
|
-
uniqueOpens,
|
|
191
|
-
uniqueClicks,
|
|
192
|
-
bounced: total(sentCampaigns, 'bounced'),
|
|
193
|
-
complained: total(sentCampaigns, 'complained'),
|
|
194
|
-
unsubscribes: total(sentCampaigns, 'unsubscribes'),
|
|
195
|
-
rates,
|
|
196
|
-
audiences: templateAudiences(sentCampaigns),
|
|
197
|
-
caveats
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* The audiences the design actually went to, largest first.
|
|
202
|
-
*
|
|
203
|
-
* Keyed on the LIST ID for a list send and on the audience kind otherwise, so
|
|
204
|
-
* two campaigns to the same list are one row and two campaigns to "all leads"
|
|
205
|
-
* are one row — but a list send and a segment send are never merged, because
|
|
206
|
-
* they are different questions about who received this design.
|
|
207
|
-
*
|
|
208
|
-
* A list send whose campaign recorded no list id at all is dropped rather
|
|
209
|
-
* than filed under a generic "List" heading: a row that cannot name which
|
|
210
|
-
* list is a row that answers the question wrongly.
|
|
211
|
-
*/ function templateAudiences(campaigns) {
|
|
212
|
-
const rows = new Map();
|
|
213
|
-
for (const campaign of campaigns){
|
|
214
|
-
var _ref;
|
|
215
|
-
var _campaign_stats;
|
|
216
|
-
const isList = campaign.audience === 'list';
|
|
217
|
-
if (isList && !campaign.listId) continue;
|
|
218
|
-
const id = isList ? `list:${campaign.listId}` : campaign.audience;
|
|
219
|
-
const existing = rows.get(id);
|
|
220
|
-
const addressed = Number((_ref = (_campaign_stats = campaign.stats) == null ? void 0 : _campaign_stats.recipients) != null ? _ref : 0);
|
|
221
|
-
if (existing) {
|
|
222
|
-
existing.campaigns += 1;
|
|
223
|
-
existing.addressed += addressed;
|
|
224
|
-
// A later send that DID record the name fills one that did not, so a
|
|
225
|
-
// list mailed twice is named whenever any of those sends named it.
|
|
226
|
-
if (existing.unnamed && campaign.listName) {
|
|
227
|
-
existing.label = campaign.listName;
|
|
228
|
-
delete existing.unnamed;
|
|
229
|
-
}
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
rows.set(id, _extends({
|
|
233
|
-
id,
|
|
234
|
-
// Named once, in `email-record.ts`, so a list row on this table and
|
|
235
|
-
// the same list on a message's own page cannot disagree about what to
|
|
236
|
-
// call it.
|
|
237
|
-
label: emailAudienceLabel(campaign),
|
|
238
|
-
campaigns: 1,
|
|
239
|
-
addressed
|
|
240
|
-
}, isList && !campaign.listName ? {
|
|
241
|
-
unnamed: true
|
|
242
|
-
} : {}));
|
|
243
|
-
}
|
|
244
|
-
return [
|
|
245
|
-
...rows.values()
|
|
246
|
-
].sort((a, b)=>b.addressed - a.addressed || a.label.localeCompare(b.label));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
//# sourceMappingURL=template-report.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/template-report.ts"],"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\n/**\n * ONE DESIGN, MEASURED ACROSS EVERY CAMPAIGN THAT USED IT.\n *\n * ## What this adds to `campaign-report.ts`\n *\n * A campaign report divides two numbers that were recorded by the same send.\n * A design report divides two SUMS, and a sum has a membership: which\n * campaigns went into it. That is the whole difficulty, and it is a new way\n * for the repo's rule — every rate names its denominator — to be broken.\n *\n * ## The rule, restated for a sum\n *\n * **A rate's numerator and its denominator are summed over the SAME set of\n * campaigns, and the screen is told how large that set is.**\n *\n * `delivered` is written only by the delivery webhook, so a design used by\n * five campaigns may have three that recorded it and two that never did.\n * Summing `uniqueOpens` over all five and dividing by `delivered` summed over\n * three produces an open rate that can exceed 100% and is, in every case,\n * the flattering substitution `campaign-report.ts` exists to refuse — carried\n * out by addition instead of by picking the wrong field.\n *\n * So each rate here is built from a FILTERED subset, and\n * {@link TemplateRate.denominatorLabel} names the subset as well as the\n * quantity: `delivered across 3 of 5 campaigns`. A reader comparing this\n * design's open rate with another's can see the two were taken over different\n * populations without being told separately.\n *\n * ## Counts are over everything; rates are over what can be divided\n *\n * The COUNTS — opens, clicks, unsubscribes — are summed over every campaign,\n * because a count is true whatever else was recorded. Only the rates narrow.\n * The two are labelled differently on screen for that reason, and the caveats\n * say how many campaigns each rate left out.\n *\n * ## Why this is a pure module\n *\n * Same reason as `campaign-report.ts`: a division written in JSX is a\n * denominator chosen by whoever writes the next card. Here it is worse — the\n * subset is invisible in the output unless something carries it — so the\n * subset is data, computed once, and the screen renders it.\n */\n\nimport {\n campaignRate,\n type CampaignRate,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n emailAudienceLabel,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\n\n/** A rate over a sum, carrying how many campaigns the sum covers. */\nexport type TemplateRate = CampaignRate\n\n/**\n * One campaign that used the design, as the report reads it.\n *\n * `sentAtMs` is nullable because a campaign document can exist without ever\n * having been sent — a scheduled campaign holds `sendAtMs` and no `sentAt`.\n * Those carry no engagement and the reader is shown their status rather than\n * a row of zeroes.\n */\nexport interface TemplateCampaign {\n campaignId: string\n subject: string\n /** Epoch ms, or null for a campaign that has not been sent. */\n sentAtMs: number | null\n /** `'sent'`, `'scheduled'`, `'canceled'`. A persisted value; not relabeled. */\n status: string\n /** The audience KIND — `'leads'`, `'members'`, `'segment'`, `'list'`. */\n audience: string\n /** Set when `audience` is `'list'`. */\n listId?: string\n /** The list's name AS IT WAS at send time, when the send recorded one. */\n listName?: string\n stats: CampaignStats | undefined\n}\n\n/**\n * One audience the design was sent to.\n *\n * A LIST is named by the name the send recorded, not by the name the list\n * carries today: a renamed or deleted list would otherwise rewrite the\n * history of a campaign that went out months ago, and a deleted one would\n * erase it. The same reasoning as the campaign report's populations.\n */\nexport interface TemplateAudience {\n /** Stable key — the list id, or the audience kind for the built-ins. */\n id: string\n label: string\n /** How many campaigns using this design went to this audience. */\n campaigns: number\n /** Addresses those campaigns ADDRESSED, summed. */\n addressed: number\n /**\n * The named list has since been deleted or renamed out of reach.\n *\n * Only ever true for a `list` audience, and only when the send recorded no\n * name — an older send, before the name was stored. The screen says \"list\n * no longer named\" rather than printing a raw document id as if it were\n * something a merchant would recognise.\n */\n unnamed?: boolean\n}\n\n/** Why a number the design report would otherwise show is being withheld. */\nexport interface TemplateCaveat {\n id:\n | 'no-campaigns'\n | 'delivery-partial'\n | 'click-tracking-partial'\n | 'campaigns-truncated'\n | 'audience-truncated'\n message: string\n}\n\n/** Everything the design report screen renders. */\nexport interface TemplateReport {\n /** Campaigns that used this design AND were sent. */\n sentCampaigns: number\n /** Campaigns that used this design, sent or not. */\n totalCampaigns: number\n /** Epoch ms of the most recent send, or null. */\n lastSentAtMs: number | null\n\n /*========================================\n * COUNTS — over every sent campaign.\n *=======================================*/\n recipients: number\n sent: number\n /** Summed over the campaigns that recorded it; null when none did. */\n delivered: number | null\n opens: number\n clicks: number\n uniqueOpens: number | null\n uniqueClicks: number | null\n bounced: number\n complained: number\n unsubscribes: number\n\n /** Rates, each `null` when its denominator is zero or unrecorded. */\n rates: {\n delivery: TemplateRate | null\n open: TemplateRate | null\n click: TemplateRate | null\n clickToOpen: TemplateRate | null\n bounce: TemplateRate | null\n complaint: TemplateRate | null\n unsubscribe: TemplateRate | null\n }\n\n audiences: TemplateAudience[]\n caveats: TemplateCaveat[]\n}\n\n/** A campaign that actually went out. The only kind engagement can describe. */\nfunction wasSent(campaign: TemplateCampaign): boolean {\n return campaign.sentAtMs !== null\n}\n\n/** Sums one stats field over a set of campaigns, defaulting absent to zero. */\nfunction total(\n campaigns: readonly TemplateCampaign[],\n field: keyof CampaignStats,\n): number {\n return campaigns.reduce(\n (running, campaign) => running + Number(campaign.stats?.[field] ?? 0),\n 0,\n )\n}\n\n/**\n * `${quantity} across N of M campaigns` — the denominator label for a sum.\n *\n * The `of M` half is what makes a narrowed rate readable. `delivered across 3\n * campaigns` is true and still invites the reader to assume the design has\n * three; `across 3 of 5` says on its face that two are missing from this\n * number and not from the counts above it.\n */\nfunction acrossLabel(quantity: string, covered: number, all: number): string {\n return covered === all\n ? `${quantity} across ${covered} ${covered === 1 ? 'campaign' : 'campaigns'}`\n : `${quantity} across ${covered} of ${all} campaigns`\n}\n\n/**\n * Aggregates every campaign that used one design.\n *\n * @param campaigns every campaign document naming this design, sent or not.\n * @param truncated the query that produced `campaigns` stopped at its read\n * ceiling, so every total here is a FLOOR. Reported as a caveat rather\n * than hidden, on the same reasoning as `audienceSizeTruncated`: a\n * figure that is at least this large is useful, and a figure presented\n * as complete when it is not is worse than none.\n */\nexport function templateReport(\n campaigns: readonly TemplateCampaign[],\n truncated = false,\n): TemplateReport {\n const sentCampaigns = campaigns.filter(wasSent)\n const all = sentCampaigns.length\n\n /*\n * THE SUBSETS. Each rate divides sums taken over one of these and nothing\n * else, which is what makes the numerator and denominator describe the same\n * population — see this module's header.\n */\n const withDelivery = sentCampaigns.filter(\n (campaign) => campaign.stats?.delivered !== undefined,\n )\n const withUniqueOpens = withDelivery.filter(\n (campaign) => campaign.stats?.uniqueOpens !== undefined,\n )\n /*\n * Click rates need BOTH a delivery denominator and the record that the send\n * carried an HTML part. A send with no HTML part reports zero clicks\n * whatever recipients did, so including it in a click rate's denominator\n * drags the rate down by however many untrackable sends this design has —\n * a measurement of our own sending code presented as a measurement of the\n * audience. `campaign-report.ts` refuses the same substitution for one\n * campaign; a sum must refuse it per campaign, not for the design as a\n * whole, or one old send would withhold the design's whole click rate.\n */\n const withClickTracking = withDelivery.filter(\n (campaign) =>\n campaign.stats?.clickTracked === true &&\n campaign.stats?.uniqueClicks !== undefined,\n )\n const withOpenersAndClicks = withClickTracking.filter(\n (campaign) => campaign.stats?.uniqueOpens !== undefined,\n )\n\n const sent = total(sentCampaigns, 'sent')\n const delivered = withDelivery.length\n ? total(withDelivery, 'delivered')\n : null\n const uniqueOpens = withUniqueOpens.length\n ? total(withUniqueOpens, 'uniqueOpens')\n : null\n const uniqueClicks = withClickTracking.length\n ? total(withClickTracking, 'uniqueClicks')\n : null\n\n const rates: TemplateReport['rates'] = {\n /*\n * Over the campaigns that recorded a delivery, and `sent` summed over\n * those SAME campaigns rather than over all of them. The alternative —\n * delivered over the design's whole `sent` — reads as a delivery failure\n * proportional to how many of this design's sends predate the webhook.\n */\n delivery: delivered === null\n ? null\n : campaignRate(\n delivered,\n total(withDelivery, 'sent'),\n acrossLabel('sent', withDelivery.length, all),\n ),\n open: campaignRate(\n uniqueOpens ?? undefined,\n withUniqueOpens.length ? total(withUniqueOpens, 'delivered') : undefined,\n acrossLabel('delivered', withUniqueOpens.length, all),\n ),\n click: campaignRate(\n uniqueClicks ?? undefined,\n withClickTracking.length\n ? total(withClickTracking, 'delivered')\n : undefined,\n acrossLabel('delivered', withClickTracking.length, all),\n ),\n clickToOpen: campaignRate(\n withOpenersAndClicks.length\n ? total(withOpenersAndClicks, 'uniqueClicks')\n : undefined,\n withOpenersAndClicks.length\n ? total(withOpenersAndClicks, 'uniqueOpens')\n : undefined,\n acrossLabel('unique openers', withOpenersAndClicks.length, all),\n ),\n /*\n * Bounces and their rate are over every sent campaign, because both\n * halves are recorded for all of them: `sent` by the send itself and\n * `bounced` by a webhook whose absence is a genuine zero — no bounce\n * event means nothing bounced, unlike `delivered`, whose absence means\n * nothing was measured.\n */\n bounce: campaignRate(\n total(sentCampaigns, 'bounced'),\n sent,\n acrossLabel('sent', all, all),\n ),\n complaint: campaignRate(\n withDelivery.length ? total(withDelivery, 'complained') : undefined,\n delivered ?? undefined,\n acrossLabel('delivered', withDelivery.length, all),\n ),\n unsubscribe: campaignRate(\n withDelivery.length ? total(withDelivery, 'unsubscribes') : undefined,\n delivered ?? undefined,\n acrossLabel('delivered', withDelivery.length, all),\n ),\n }\n\n const caveats: TemplateCaveat[] = []\n if (!all) {\n caveats.push({\n id: 'no-campaigns',\n message: campaigns.length\n ? 'No campaign using this design has been sent yet, so there is ' +\n 'nothing to measure. The campaigns below are scheduled or were ' +\n 'canceled before they went out.'\n : 'This design has never been sent, so there is nothing to measure.',\n })\n } else if (withDelivery.length < all) {\n caveats.push({\n id: 'delivery-partial',\n message:\n `${all - withDelivery.length} of ${all} campaigns using this design ` +\n 'recorded no delivery events, so they are left out of every rate ' +\n 'taken over delivered — open, click, complaint and unsubscribe. ' +\n 'Their counts are still included above.',\n })\n }\n if (all && withClickTracking.length < withDelivery.length) {\n caveats.push({\n id: 'click-tracking-partial',\n message:\n `${withDelivery.length - withClickTracking.length} of these campaigns ` +\n 'did not record carrying an HTML part. Click tracking rewrites links ' +\n 'in the HTML, so those sends report zero clicks whatever recipients ' +\n 'did, and they are left out of the click rates rather than counted ' +\n 'as sends nobody clicked.',\n })\n }\n if (truncated) {\n caveats.push({\n id: 'campaigns-truncated',\n message:\n 'This design has been used by more campaigns than one read returns, ' +\n 'so every total here is a floor — the real figures are at least ' +\n 'this large.',\n })\n }\n if (sentCampaigns.some((campaign) => campaign.stats?.audienceSizeTruncated)) {\n caveats.push({\n id: 'audience-truncated',\n message:\n 'At least one of these sends stopped audience resolution at its read ' +\n 'ceiling, so the audience figures it contributed are floors.',\n })\n }\n\n return {\n sentCampaigns: all,\n totalCampaigns: campaigns.length,\n lastSentAtMs: sentCampaigns.reduce<number | null>(\n (latest, campaign) =>\n latest === null || (campaign.sentAtMs ?? 0) > latest\n ? campaign.sentAtMs\n : latest,\n null,\n ),\n recipients: total(sentCampaigns, 'recipients'),\n sent,\n delivered,\n opens: total(sentCampaigns, 'opens'),\n clicks: total(sentCampaigns, 'clicks'),\n uniqueOpens,\n uniqueClicks,\n bounced: total(sentCampaigns, 'bounced'),\n complained: total(sentCampaigns, 'complained'),\n unsubscribes: total(sentCampaigns, 'unsubscribes'),\n rates,\n audiences: templateAudiences(sentCampaigns),\n caveats,\n }\n}\n\n/**\n * The audiences the design actually went to, largest first.\n *\n * Keyed on the LIST ID for a list send and on the audience kind otherwise, so\n * two campaigns to the same list are one row and two campaigns to \"all leads\"\n * are one row — but a list send and a segment send are never merged, because\n * they are different questions about who received this design.\n *\n * A list send whose campaign recorded no list id at all is dropped rather\n * than filed under a generic \"List\" heading: a row that cannot name which\n * list is a row that answers the question wrongly.\n */\nfunction templateAudiences(\n campaigns: readonly TemplateCampaign[],\n): TemplateAudience[] {\n const rows = new Map<string, TemplateAudience>()\n for (const campaign of campaigns) {\n const isList = campaign.audience === 'list'\n if (isList && !campaign.listId) continue\n const id = isList ? `list:${campaign.listId}` : campaign.audience\n const existing = rows.get(id)\n const addressed = Number(campaign.stats?.recipients ?? 0)\n if (existing) {\n existing.campaigns += 1\n existing.addressed += addressed\n // A later send that DID record the name fills one that did not, so a\n // list mailed twice is named whenever any of those sends named it.\n if (existing.unnamed && campaign.listName) {\n existing.label = campaign.listName\n delete existing.unnamed\n }\n continue\n }\n rows.set(id, {\n id,\n // Named once, in `email-record.ts`, so a list row on this table and\n // the same list on a message's own page cannot disagree about what to\n // call it.\n label: emailAudienceLabel(campaign),\n campaigns: 1,\n addressed,\n ...(isList && !campaign.listName ? { unnamed: true as const } : {}),\n })\n }\n return [...rows.values()].sort(\n (a, b) => b.addressed - a.addressed || a.label.localeCompare(b.label),\n )\n}\n"],"names":["campaignRate","emailAudienceLabel","wasSent","campaign","sentAtMs","total","campaigns","field","reduce","running","Number","stats","acrossLabel","quantity","covered","all","templateReport","truncated","sentCampaigns","filter","length","withDelivery","delivered","undefined","withUniqueOpens","uniqueOpens","withClickTracking","clickTracked","uniqueClicks","withOpenersAndClicks","sent","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","caveats","push","id","message","some","audienceSizeTruncated","totalCampaigns","lastSentAtMs","latest","recipients","opens","clicks","bounced","complained","unsubscribes","audiences","templateAudiences","rows","Map","isList","audience","listId","existing","get","addressed","unnamed","listName","label","set","values","sort","a","b","localeCompare"],"mappings":"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"}
|