@aglyn/plugins-email 1.0.0-beta.220 → 1.0.0-beta.222

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 (82) hide show
  1. package/README.md +5 -4
  2. package/package.json +16 -17
  3. package/src/lib/components/email-template-detail.d.ts +5 -13
  4. package/src/lib/components/email-template-detail.js +29 -426
  5. package/src/lib/components/email-template-detail.js.map +1 -1
  6. package/src/lib/components/email-zones.d.ts +15 -0
  7. package/src/lib/components/email-zones.js +1 -0
  8. package/src/lib/components/email-zones.js.map +1 -1
  9. package/src/lib/components/list-detail-card.d.ts +0 -36
  10. package/src/lib/components/list-detail-card.js +125 -84
  11. package/src/lib/components/list-detail-card.js.map +1 -1
  12. package/src/lib/components/list-members-panel.d.ts +17 -0
  13. package/src/lib/components/list-members-panel.js +14 -0
  14. package/src/lib/components/list-members-panel.js.map +1 -1
  15. package/src/lib/components/site-package-email-preview.d.ts +13 -0
  16. package/src/lib/components/site-package-email-preview.js +47 -0
  17. package/src/lib/components/site-package-email-preview.js.map +1 -0
  18. package/src/lib/components/suppressions-card.js +75 -6
  19. package/src/lib/components/suppressions-card.js.map +1 -1
  20. package/src/lib/components/use-org-email-topics.js +2 -9
  21. package/src/lib/components/use-org-email-topics.js.map +1 -1
  22. package/src/lib/constants/site-package.d.ts +21 -0
  23. package/src/lib/constants/site-package.js +21 -0
  24. package/src/lib/constants/site-package.js.map +1 -0
  25. package/src/lib/declarations.console-server.d.ts +12 -0
  26. package/src/lib/declarations.console-server.js +73 -0
  27. package/src/lib/declarations.console-server.js.map +1 -1
  28. package/src/lib/list-import.d.ts +64 -75
  29. package/src/lib/list-import.js +43 -158
  30. package/src/lib/list-import.js.map +1 -1
  31. package/src/lib/model/email-topic-catalog.d.ts +24 -0
  32. package/src/lib/model/email-topic-catalog.js +19 -0
  33. package/src/lib/model/email-topic-catalog.js.map +1 -1
  34. package/src/lib/model/index.d.ts +0 -25
  35. package/src/lib/model/index.js +0 -27
  36. package/src/lib/model/index.js.map +1 -1
  37. package/src/lib/plugin.d.ts +8 -0
  38. package/src/lib/plugin.js +76 -2
  39. package/src/lib/plugin.js.map +1 -1
  40. package/src/lib/server/person-eraser.d.ts +39 -0
  41. package/src/lib/server/person-eraser.js +44 -0
  42. package/src/lib/server/person-eraser.js.map +1 -0
  43. package/src/lib/server-console.js +9 -9
  44. package/src/lib/server-console.js.map +1 -1
  45. package/src/lib/server-list-gate.d.ts +7 -0
  46. package/src/lib/server-list-gate.js +10 -3
  47. package/src/lib/server-list-gate.js.map +1 -1
  48. package/src/lib/server-list-import.d.ts +20 -177
  49. package/src/lib/server-list-import.js +34 -594
  50. package/src/lib/server-list-import.js.map +1 -1
  51. package/src/lib/server.js +16 -29
  52. package/src/lib/server.js.map +1 -1
  53. package/src/lib/testing/stand-in-send-tally.d.ts +29 -0
  54. package/src/lib/testing/stand-in-send-tally.js +52 -0
  55. package/src/lib/testing/stand-in-send-tally.js.map +1 -0
  56. package/src/lib/transfer/email-transfer-catalog.d.ts +151 -0
  57. package/src/lib/transfer/email-transfer-catalog.js +536 -0
  58. package/src/lib/transfer/email-transfer-catalog.js.map +1 -0
  59. package/src/lib/transfer/list-import-steps.d.ts +44 -0
  60. package/src/lib/transfer/list-import-steps.js +201 -0
  61. package/src/lib/transfer/list-import-steps.js.map +1 -0
  62. package/src/lib/transfer/list-members.server.d.ts +165 -0
  63. package/src/lib/transfer/list-members.server.js +911 -0
  64. package/src/lib/transfer/list-members.server.js.map +1 -0
  65. package/src/lib/transfer/suppressions.server.d.ts +69 -0
  66. package/src/lib/transfer/suppressions.server.js +282 -0
  67. package/src/lib/transfer/suppressions.server.js.map +1 -0
  68. package/src/lib/transfer/topics-package.d.ts +56 -0
  69. package/src/lib/transfer/topics-package.js +87 -0
  70. package/src/lib/transfer/topics-package.js.map +1 -0
  71. package/src/lib/transfer/topics-package.server.d.ts +33 -0
  72. package/src/lib/transfer/topics-package.server.js +171 -0
  73. package/src/lib/transfer/topics-package.server.js.map +1 -0
  74. package/src/lib/transfer/transfer-reads.server.d.ts +25 -0
  75. package/src/lib/transfer/transfer-reads.server.js +34 -0
  76. package/src/lib/transfer/transfer-reads.server.js.map +1 -0
  77. package/src/lib/components/list-import-drawer.d.ts +0 -22
  78. package/src/lib/components/list-import-drawer.js +0 -662
  79. package/src/lib/components/list-import-drawer.js.map +0 -1
  80. package/src/lib/model/template-report.d.ts +0 -158
  81. package/src/lib/model/template-report.js +0 -249
  82. package/src/lib/model/template-report.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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\nimport {\n buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { mdiBullhornOutline, mdiEyeOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n documentId,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n CAMPAIGN_SEND_CONTAINER_FIELD,\n campaignSendVisibleTo,\n orgCampaignSendsPath,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport {\n emailSendTimeMs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport {\n campaignSendDisplay,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { templateProvenance } from '../model/template-provenance'\nimport {\n templateReport,\n type TemplateCampaign,\n} from '../model/template-report'\nimport EmailDesignPreview from './email-design-preview'\nimport { EMAIL_TEMPLATE_RECIPIENTS_ZONE } from './email-zones'\nimport {\n Figure,\n RateRow,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\n\n/**\n * How many of a template's messages one read covers.\n *\n * A CEILING on the read, not a page size: the report is a single figure per\n * quantity, so there is nothing to page through. Past it every total is a\n * floor and the report says so — the same treatment `audienceSizeTruncated`\n * gets, and for the same reason.\n */\nexport const TEMPLATE_CAMPAIGN_CEILING = 100\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'The template as an inbox receives it, drawn by the same renderer the ' +\n 'send path uses. Merge tokens are left standing — a real send fills them ' +\n 'from each recipient.',\n})\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'One email template: what it looks like, every message sent from it, ' +\n 'and what those messages did — each rate over the population it was ' +\n 'measured against.',\n})\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * A message row on this page: what the report needs, plus what the ROW needs.\n *\n * `TemplateCampaign` is the report's shape and stays that — it is the input to\n * `templateReport`, and a field the figures do not read has no business in it.\n * The send time this table sorts on and the campaign its row menu offers are\n * both properties of the row rather than of the report.\n */\ntype TemplateMessage = TemplateCampaign & {\n scheduledForMs: number\n /** Absent on a message written before campaigns grouped their emails. */\n emailCampaignId?: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document, its published version, and the messages naming this\n * template — bounded at {@link TEMPLATE_CAMPAIGN_CEILING}. The recipients\n * table is its own card and its own request, so a reader who came for the\n * preview does not pay for the delivery-log query.\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n /*\n * A campaign's page is another plugin's, so its address is asked for: the\n * owner publishes where a `campaign` is read and this page links there.\n * Null where nobody does, or before the console's URL has resolved.\n */\n const campaignHref = (campaignId: string): string | null =>\n orgSlug && subdomain\n ? pluginRecordHref('campaign', { orgSlug, host: subdomain }, campaignId)\n : null\n const RecipientsZone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n /*\n * The messages sent from this template, ordered by DOCUMENT ID.\n *\n * Not by date, and that is forced rather than chosen: a sent message\n * carries `sentAt`, a scheduled one carries `sendAtMs`, and no writer\n * stamps a `createdAt` — so `orderBy` on either would not mis-sort this\n * list, it would DROP every message written by the other branch. Ordering\n * on the document id is the one ordering every message satisfies, and the\n * rows are sorted by date below over a window this component already holds.\n * The index the filters need is `visibleTo` + `templateScreenId`, declared\n * in `cloud/firebase-firestore.indexes.json`.\n *\n * One MORE than the ceiling, so truncation is a fact rather than a guess.\n *\n * The sends are the ORGANIZATION'S, each stamped with the one site it is\n * sent as. The `visibleTo` clause names this site: it keeps a sibling\n * site's sends of a same-id design out, and it is what makes the read\n * provable for a collaborator scoped to this site — the rules evaluate a\n * list per document, and an unfiltered one is refused whole. Nothing is\n * read until the site's org is known.\n */\n const { orgId } = useOrgDataScope({ hostId })\n const sendScope = useMemo(() => campaignSendVisibleTo(hostId), [hostId])\n const { data: messageDocs } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(\n collection(firestore, ...orgCampaignSendsPath(orgId)),\n where('visibleTo', 'array-contains-any', sendScope),\n where('templateScreenId', '==', screenId),\n orderBy(documentId()),\n limit(TEMPLATE_CAMPAIGN_CEILING + 1),\n )\n : null,\n [firestore, orgId, sendScope, screenId],\n { idField: '$id' },\n )\n\n const truncated = (messageDocs?.length ?? 0) > TEMPLATE_CAMPAIGN_CEILING\n const messages = useMemo<TemplateCampaign[]>(\n () =>\n (messageDocs ?? [])\n .slice(0, TEMPLATE_CAMPAIGN_CEILING)\n .map((message: any) => ({\n campaignId: String(message.$id),\n subject: String(message.subject || 'Untitled email'),\n /*\n * A message that has not gone out has NO send time, and that is\n * what `null` means here — not \"unknown\". The report measures\n * engagement over sent messages only, so a scheduled one\n * contributes to the list below and to no figure above it.\n */\n sentAtMs:\n String(message.status ?? '') === 'sent'\n ? emailSendTimeMs(message) || null\n : null,\n status: String(message.status ?? 'sent'),\n audience: String(message.audience ?? ''),\n ...(message.listId ? { listId: String(message.listId) } : {}),\n ...(message.listName ? { listName: String(message.listName) } : {}),\n stats: message.stats,\n scheduledForMs: emailSendTimeMs(message),\n /*\n * The campaign this message belongs to, carried through so the row\n * can offer it. A message written before campaigns grouped anything\n * names no container, which is why it is optional rather than\n * defaulted to the message's own id — a menu item that navigated to\n * the message you are already looking at would be worse than one\n * that says the message belongs to no campaign.\n */\n ...(message[CAMPAIGN_SEND_CONTAINER_FIELD]\n ? {\n emailCampaignId: String(\n message[CAMPAIGN_SEND_CONTAINER_FIELD],\n ),\n }\n : {}),\n })) as TemplateMessage[],\n [messageDocs],\n )\n const orderedMessages = useMemo(\n () =>\n [...(messages as TemplateMessage[])].sort(\n (a, b) => b.scheduledForMs - a.scheduledForMs,\n ),\n [messages],\n )\n const report = useMemo(\n () => templateReport(messages, truncated),\n [messages, truncated],\n )\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const messageHref = (message: TemplateMessage) =>\n `${basePath}/messages/${message.campaignId}`\n\n /**\n * What one message sent from this template can be opened into.\n *\n * Its own report, and the campaign that grouped it. The template is the page\n * the reader is standing on, so it is not offered a third time. A message\n * sent before campaigns existed belongs to no container, and the item says\n * so rather than vanishing.\n */\n const messageActions = (message: TemplateMessage): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open report',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: messageHref(message),\n },\n {\n key: 'campaign',\n label: 'Open its campaign',\n icon: <MdiIcon path={mdiBullhornOutline.path} size={0.8} />,\n // The one href here that is not under this surface's own base path.\n href:\n (message.emailCampaignId && campaignHref(message.emailCampaignId)) ||\n undefined,\n disabled:\n !message.emailCampaignId || !campaignHref(message.emailCampaignId),\n disabledReason: message.emailCampaignId\n ? 'This site’s console URL has not resolved yet'\n : 'Sent before campaigns grouped their emails, so it belongs to none',\n },\n ]\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n {report.caveats.map((caveat) => (\n <Alert key={caveat.id} severity=\"info\">\n {caveat.message}\n </Alert>\n ))}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n <Divider />\n\n {/*==========================================\n * WHAT HAPPENED TO THE MAIL, SUMMED.\n *\n * Counts first and rates second, because a rate is only readable\n * once you know what it was taken over — and because a count here\n * covers every message while a rate covers only the messages that\n * recorded its denominator. The rate labels say which.\n *=========================================*/}\n <Section title=\"Delivery\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Emails\"\n value={report.sentCampaigns}\n note=\"sent from this template\"\n />\n <Figure\n label=\"Addressed\"\n value={report.recipients}\n note=\"after each send's cap\"\n />\n <Figure\n label=\"Sent\"\n value={report.sent}\n note=\"accepted by the provider\"\n />\n <Figure\n label=\"Delivered\"\n value={report.delivered}\n note=\"accepted by the receiving server\"\n />\n <Figure label=\"Bounced\" value={report.bounced} note=\"of sent\" />\n <Figure\n label=\"Marked as spam\"\n value={report.complained}\n note=\"of delivered\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Engagement\">\n <Stack\n direction=\"row\"\n spacing={4}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Figure\n label=\"Opens\"\n value={report.opens}\n note=\"every open, repeats included\"\n />\n <Figure\n label=\"Readers who opened\"\n value={report.uniqueOpens}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Clicks\"\n value={report.clicks}\n note=\"every click, repeats included\"\n />\n <Figure\n label=\"Readers who clicked\"\n value={report.uniqueClicks}\n note=\"distinct recipients\"\n />\n <Figure\n label=\"Unsubscribed\"\n value={report.unsubscribes}\n note=\"through these emails' links\"\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Rates\">\n <Stack spacing={1}>\n <RateRow label=\"Delivery rate\" rate={report.rates.delivery} />\n <RateRow label=\"Open rate\" rate={report.rates.open} />\n <RateRow label=\"Click rate\" rate={report.rates.click} />\n <RateRow\n label=\"Click-to-open rate\"\n rate={report.rates.clickToOpen}\n />\n <RateRow label=\"Bounce rate\" rate={report.rates.bounce} />\n <RateRow label=\"Complaint rate\" rate={report.rates.complaint} />\n <RateRow\n label=\"Unsubscribe rate\"\n rate={report.rates.unsubscribe}\n />\n </Stack>\n </Section>\n\n <Divider />\n\n <Section title=\"Who this went to\">\n {report.audiences.length ? (\n <>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Audience'}</TableCell>\n <TableCell align=\"right\">{'Emails'}</TableCell>\n <TableCell align=\"right\">{'Addressed'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {report.audiences.map((audience) => (\n <TableRow key={audience.id}>\n <TableCell>{audience.label}</TableCell>\n <TableCell align=\"right\">\n {audience.campaigns.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\" sx={{ fontWeight: 'bold' }}>\n {audience.addressed.toLocaleString()}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/*\n * The naming rule, stated rather than left to be discovered\n * from a list whose name no longer matches the one in the\n * audiences section.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each list is named as it was when the email was sent, so ' +\n 'renaming or deleting a list does not rewrite what a past ' +\n 'send went to.'}\n </Typography>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No email sent from this template has gone out yet.'}\n </Typography>\n )}\n </Section>\n\n {orderedMessages.length ? (\n <>\n <Divider />\n <Section title=\"Emails using this template\">\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Subject'}</TableCell>\n <TableCell>{'State'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell align=\"right\">{'Addressed'}</TableCell>\n <TableCell align=\"right\">{'Opens'}</TableCell>\n <TableCell align=\"right\">{'Clicks'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {orderedMessages.map((message) => (\n <TableRow\n key={message.campaignId}\n hover\n onClick={() => router.push(messageHref(message))}\n sx={{ cursor: 'pointer' }}\n >\n <TableCell>\n {/*\n The row's own handler would fire too and push the\n same route twice — one history entry per back\n press.\n */}\n <AppLink\n href={messageHref(message)}\n onClick={(event: {\n stopPropagation: () => void\n }) => event.stopPropagation()}\n >\n {message.subject}\n </AppLink>\n </TableCell>\n <TableCell>\n {/*\n What the message is DOING. One delivering an\n audience larger than one batch is written back as\n `scheduled` between runs, so the stored status\n reads \"Scheduled\" about a send already in progress.\n */}\n <Chip\n size=\"small\"\n label={campaignSendDisplay(message as never).label}\n />\n </TableCell>\n <TableCell>\n {message.scheduledForMs\n ? new Date(\n message.scheduledForMs,\n ).toLocaleString()\n : '—'}\n </TableCell>\n <TableCell align=\"right\">\n {Number(\n message.stats?.recipients ?? 0,\n ).toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {Number(message.stats?.opens ?? 0).toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">\n {Number(message.stats?.clicks ?? 0).toLocaleString()}\n </TableCell>\n <TableCell\n align=\"right\"\n sx={{ width: 56 }}\n onClick={(event) => event.stopPropagation()}\n >\n <RowActionsMenu\n label={String(message.subject)}\n items={messageActions(message)}\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n </>\n ) : null}\n </Stack>\n </CardDisplay>\n\n {/*\n Its own boundary: the table's code arrives after the report above it\n has drawn, and must not send that report back to a loading state.\n */}\n {RecipientsZone ? (\n <Suspense fallback={null}>\n <RecipientsZone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","mdiBullhornOutline","mdiEyeOutline","AppLink","CardDisplay","MdiIcon","RowActionsMenu","ScrollTable","useConsoleHostRoute","useFirestore","useFirestoreCollection","useFirestoreDoc","useOrgDataScope","Alert","Button","Chip","Divider","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","doc","documentId","limit","orderBy","query","updateDoc","where","useRouter","Suspense","useMemo","useState","useSnackbar","CAMPAIGN_SEND_CONTAINER_FIELD","campaignSendVisibleTo","orgCampaignSendsPath","emailSendTimeMs","campaignSendDisplay","templateProvenance","templateReport","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","Figure","RateRow","Section","useConsoleWidgetSlot","pluginRecordHref","TEMPLATE_CAMPAIGN_CEILING","previewDocsHelp","anchor","excerpt","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","campaignHref","campaignId","host","RecipientsZone","firestore","router","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","orgId","sendScope","messageDocs","truncated","length","messages","slice","map","message","String","$id","subject","sentAtMs","audience","listId","listName","stats","scheduledForMs","emailCampaignId","orderedMessages","sort","a","b","report","provenance","messageHref","messageActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","listUrl","besignerUrl","SCREEN_BESIGNER","headerActions","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","color","startIcon","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","note","severity","warn","caveats","caveat","id","value","onChange","event","target","minWidth","helperText","onClick","useFlexGap","flexWrap","sentCampaigns","recipients","sent","delivered","bounced","complained","opens","uniqueOpens","clicks","uniqueClicks","unsubscribes","rate","rates","delivery","open","click","clickToOpen","bounce","complaint","unsubscribe","audiences","align","campaigns","toLocaleString","fontWeight","addressed","hover","push","cursor","stopPropagation","Date","Number","width","items","fallback","slot","nodes","loading","Boolean","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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\nimport {\n buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { templateProvenance } from '../model/template-provenance'\nimport EmailDesignPreview from './email-design-preview'\nimport {\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './email-zones'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'The template as an inbox receives it, drawn by the same renderer the ' +\n 'send path uses. Merge tokens are left standing — a real send fills them ' +\n 'from each recipient.',\n})\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'One email template: its name, where it came from, and what it looks ' +\n 'like. The emails sent from it are reported beneath, by the plugin that ' +\n 'sends them.',\n})\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document and its published version. The messages sent from the\n * template are a send's, and sends are the campaign owner's: this page hosts\n * two zones that plugin fills — the report of what those messages did, and\n * who received them — handing over the site, the template and this page's\n * base path, and reads none of the sends itself (AGL-3080).\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const Zone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n </Stack>\n </CardDisplay>\n\n {/*\n What the emails sent from this template did, then who received them:\n each its own boundary, because each card's code arrives after the\n card above it has drawn and must not send it back to a loading state.\n */}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_REPORT_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n basePath={basePath}\n />\n </Suspense>\n ) : null}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","MdiIcon","useConsoleHostRoute","useFirestore","useFirestoreDoc","Alert","Button","Stack","TextField","Typography","doc","updateDoc","Suspense","useMemo","useState","useSnackbar","templateProvenance","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","Section","useConsoleWidgetSlot","previewDocsHelp","anchor","excerpt","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","Zone","firestore","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","provenance","listUrl","besignerUrl","SCREEN_BESIGNER","host","headerActions","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","href","size","color","startIcon","path","disabled","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","undefined","note","severity","warn","label","value","String","onChange","event","target","minWidth","helperText","onClick","fallback","slot","id","nodes","loading","Boolean","subject","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
@@ -42,6 +42,21 @@ export interface EmailMessagesZoneProps {
42
42
  orgMount?: ConsolePluginOrgMount;
43
43
  }
44
44
  export declare const EMAIL_MESSAGES_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<EmailMessagesZoneProps>;
45
+ /**
46
+ * What the messages sent from one template did, on the template's page.
47
+ *
48
+ * The template is this plugin's; the messages sent from it are the sends of
49
+ * the plugin that owns campaigns, so the figures, the audiences and the table
50
+ * of those messages are that plugin's to read and draw. Handed the site, the
51
+ * template, and this page's base path, which every message link hangs beneath.
52
+ */
53
+ export interface EmailTemplateReportZoneProps {
54
+ hostId: string;
55
+ screenId: string;
56
+ /** The Emails page's own path, under the site. */
57
+ basePath: string;
58
+ }
59
+ export declare const EMAIL_TEMPLATE_REPORT_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<EmailTemplateReportZoneProps>;
45
60
  /**
46
61
  * Who received the messages built from one template, under its report.
47
62
  *
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ import { definePluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
17
17
  export const EMAIL_MESSAGES_ZONE = definePluginZone('emailMessages');
18
+ export const EMAIL_TEMPLATE_REPORT_ZONE = definePluginZone('emailTemplateReport');
18
19
  export const EMAIL_TEMPLATE_RECIPIENTS_ZONE = definePluginZone('emailTemplateRecipients');
19
20
 
20
21
  //# sourceMappingURL=email-zones.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-zones.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 */\nimport type { ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * The Messages section of the Emails page.\n *\n * A message is one email that was or will be sent. Writing one, scheduling\n * it, sending it and reporting what came of it are a campaign's work — the\n * routes that do all of it are served by whichever plugin owns campaigns — so\n * this plugin hosts the section and that plugin draws it. What is handed over\n * is what the shell handed this page: the site, this page's own base path,\n * and the segments under `messages` (`[]` the list, `[id]` one message,\n * `[id, 'edit']` its composer).\n *\n * The same zone serves the ORGANIZATION's Emails page, `/[orgSlug]/emails`,\n * where there is no site: `hostId` is `null` and `orgMount` names the org and\n * its sites, so the widget can list every site's messages and ask which site\n * a new one is sent as. Every message link a widget draws hangs beneath\n * `basePath` at both levels — `${basePath}/messages/{id}`.\n */\nexport interface EmailMessagesZoneProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The Emails page's own path, under the site or the organization. */\n basePath: string\n detail: readonly string[]\n /** The organization and its sites — present only when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const EMAIL_MESSAGES_ZONE =\n definePluginZone<EmailMessagesZoneProps>('emailMessages')\n\n/**\n * Who received the messages built from one template, under its report.\n *\n * The template is this plugin's; the recipients of a send are read through\n * the campaign owner's route, so the table is that plugin's to draw.\n */\nexport interface EmailTemplateRecipientsZoneProps {\n hostId: string\n screenId: string\n}\n\nexport const EMAIL_TEMPLATE_RECIPIENTS_ZONE =\n definePluginZone<EmailTemplateRecipientsZoneProps>('emailTemplateRecipients')\n"],"names":["definePluginZone","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AA6B3E,OAAO,MAAMC,sBACXD,iBAAyC,iBAAgB;AAa3D,OAAO,MAAME,iCACXF,iBAAmD,2BAA0B"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-zones.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 */\nimport type { ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * The Messages section of the Emails page.\n *\n * A message is one email that was or will be sent. Writing one, scheduling\n * it, sending it and reporting what came of it are a campaign's work — the\n * routes that do all of it are served by whichever plugin owns campaigns — so\n * this plugin hosts the section and that plugin draws it. What is handed over\n * is what the shell handed this page: the site, this page's own base path,\n * and the segments under `messages` (`[]` the list, `[id]` one message,\n * `[id, 'edit']` its composer).\n *\n * The same zone serves the ORGANIZATION's Emails page, `/[orgSlug]/emails`,\n * where there is no site: `hostId` is `null` and `orgMount` names the org and\n * its sites, so the widget can list every site's messages and ask which site\n * a new one is sent as. Every message link a widget draws hangs beneath\n * `basePath` at both levels — `${basePath}/messages/{id}`.\n */\nexport interface EmailMessagesZoneProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The Emails page's own path, under the site or the organization. */\n basePath: string\n detail: readonly string[]\n /** The organization and its sites — present only when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const EMAIL_MESSAGES_ZONE =\n definePluginZone<EmailMessagesZoneProps>('emailMessages')\n\n/**\n * What the messages sent from one template did, on the template's page.\n *\n * The template is this plugin's; the messages sent from it are the sends of\n * the plugin that owns campaigns, so the figures, the audiences and the table\n * of those messages are that plugin's to read and draw. Handed the site, the\n * template, and this page's base path, which every message link hangs beneath.\n */\nexport interface EmailTemplateReportZoneProps {\n hostId: string\n screenId: string\n /** The Emails page's own path, under the site. */\n basePath: string\n}\n\nexport const EMAIL_TEMPLATE_REPORT_ZONE =\n definePluginZone<EmailTemplateReportZoneProps>('emailTemplateReport')\n\n/**\n * Who received the messages built from one template, under its report.\n *\n * The template is this plugin's; the recipients of a send are read through\n * the campaign owner's route, so the table is that plugin's to draw.\n */\nexport interface EmailTemplateRecipientsZoneProps {\n hostId: string\n screenId: string\n}\n\nexport const EMAIL_TEMPLATE_RECIPIENTS_ZONE =\n definePluginZone<EmailTemplateRecipientsZoneProps>('emailTemplateRecipients')\n"],"names":["definePluginZone","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AA6B3E,OAAO,MAAMC,sBACXD,iBAAyC,iBAAgB;AAiB3D,OAAO,MAAME,6BACXF,iBAA+C,uBAAsB;AAavE,OAAO,MAAMG,iCACXH,iBAAmD,2BAA0B"}
@@ -1,39 +1,3 @@
1
- /**
2
- * ONE AUDIENCE: who it is, how it is built, and every person on it.
3
- *
4
- * ## Why a route rather than an expanded row
5
- *
6
- * The membership used to unfold inside the list table, which cost three
7
- * things a merchant actually uses. A list was not linkable, so "check the
8
- * Wholesale audience" could not be sent to anybody. The back button walked out
9
- * of the whole surface rather than out of the list. And the list's own
10
- * settings had to live under a table they were not about.
11
- *
12
- * ## What it reads
13
- *
14
- * The list document — one document, whatever the audience's size — plus one
15
- * server aggregate for the subscriber total, plus the paged membership window
16
- * the table below draws. The membership is PII and there is one document per
17
- * subscriber, so it is read HERE, where somebody asked for it, and nowhere
18
- * that merely lists audiences.
19
- *
20
- * ## The rule is stated, not just flagged
21
- *
22
- * A dynamic list is a claim about who is on it, and "Rule" as a chip is not
23
- * that claim. The summary spells out every dimension the rule actually
24
- * carries, so the answer to "why is this person not in the audience" is on the
25
- * screen the question gets asked on.
26
- *
27
- * ## On the organization's page, the site it is worked AS
28
- *
29
- * The list is the organization's, but adding somebody to it is done as one
30
- * site — the enrollment and the consent it records are that site's — and the
31
- * Consent column reads what each person agreed to with one site's group. So
32
- * with no site in the URL the page asks which, defaulting to the site the
33
- * list's filters draw from, then the reader's pick this session, then the
34
- * org's only site. Nothing below the picker is built until there is an
35
- * answer: a consent column for no site would be a column about nobody.
36
- */
37
1
  import { type ConsentGroup } from '@aglyn/aglyn';
38
2
  export interface ListDetailCardProps {
39
3
  /** The site, or `null` on the organization's Emails page. */
@@ -51,8 +51,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
51
51
  * list's filters draw from, then the reader's pick this session, then the
52
52
  * org's only site. Nothing below the picker is built until there is an
53
53
  * answer: a consent column for no site would be a column about nobody.
54
- */ import { consentGroupForHost, normalizeDynamicListRule, PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
55
- import { mdiPencilOutline, mdiTrayArrowUp } from "@aglyn/shared-data-mdi";
54
+ */ import { TransferResumeImport } from "@aglyn/aglyn/app-utils/transfer-resume-import";
55
+ import { consentGroupForHost, normalizeDynamicListRule, PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
56
+ import { useTransferLauncher } from "@aglyn/aglyn/app-utils/transfer-launcher-context";
57
+ import { mdiPencilOutline, mdiTrayArrowDown, mdiTrayArrowUp } from "@aglyn/shared-data-mdi";
56
58
  import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
57
59
  import { Button, Chip, Stack, Typography } from "@mui/material";
58
60
  import { collection, doc, getCountFromServer } from "firebase/firestore";
@@ -60,10 +62,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
60
62
  import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
61
63
  import { describeDynamicListRule } from "./dynamic-list-rule-fields.js";
62
64
  import { OrgSiteSelect, useEmailDataScope, useEmailOrgMount } from "./email-org-mount.js";
63
- import ListImportDrawer from "./list-import-drawer.js";
65
+ import { listMembersResourceKey } from "../transfer/email-transfer-catalog.js";
64
66
  import ListMembersPanel from "./list-members-panel.js";
65
67
  export function ListDetailCard(props) {
66
- var _ref, _list_name, _list_name1;
68
+ var _ref, _ref1, _list_name;
67
69
  var _list_lastEvaluatedAt;
68
70
  const { hostId, listId, basePath, org } = props;
69
71
  const firestore = useFirestore();
@@ -118,12 +120,24 @@ export function ListDetailCard(props) {
118
120
  org
119
121
  ]);
120
122
  /*
121
- * Importing is a DRAWER opened from the header, not a control inside the
122
- * membership panel. It is a multi-step act — choose a file, read what is in
123
- * it, state that you have permission — and the middle step is the one that
124
- * must not be cramped, because it carries the screening warnings and the
125
- * consent readout the attestation is given against.
126
- */ const [importing, setImporting] = useState(false);
123
+ * IMPORT AND EXPORT open the console's transfer wizard and export dialog
124
+ * (AGL-3529) on this list — `email.list-members:<listId>`, one list at a
125
+ * time — worked as the site chosen above. The wizard carries everything the import's
126
+ * controls are: the preview, the screening, the statement of permission,
127
+ * the staged and resumable write, the undo. Outside the console shell
128
+ * there is no launcher, and the buttons are not drawn.
129
+ */ const transfer = useTransferLauncher();
130
+ const [memberFilter, setMemberFilter] = useState(null);
131
+ const listName = String((_ref1 = list == null ? void 0 : list['name']) != null ? _ref1 : '');
132
+ // Each button only for whom the route takes it (AGL-3554), on the site the
133
+ // list enrolls as — the workspace's verdict until one is chosen.
134
+ const transferTarget = {
135
+ resource: listMembersResourceKey(listId),
136
+ scope: 'host',
137
+ hostId: enrollHostId || null
138
+ };
139
+ const canExportMembers = Boolean(transfer == null ? void 0 : transfer.can('export', transferTarget));
140
+ const canImportMembers = Boolean(transfer == null ? void 0 : transfer.can('import', transferTarget));
127
141
  const audiencesHref = `${basePath}/audiences`;
128
142
  const headerActions = /*#__PURE__*/ _jsxs(Stack, {
129
143
  direction: "row",
@@ -156,7 +170,35 @@ export function ListDetailCard(props) {
156
170
  }),
157
171
  children: 'Edit list'
158
172
  })),
159
- /*#__PURE__*/ _jsx(Button, {
173
+ transfer && canExportMembers ? /*#__PURE__*/ _jsx(Button, {
174
+ size: "small",
175
+ color: "primary",
176
+ variant: "outlined",
177
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
178
+ path: mdiTrayArrowDown.path,
179
+ size: 0.8
180
+ }),
181
+ // Consent is read as a site; on the org page one has to be chosen.
182
+ disabled: !enrollHostId,
183
+ onClick: ()=>transfer.openExport(_extends({
184
+ resource: listMembersResourceKey(listId),
185
+ scope: 'host',
186
+ hostId: enrollHostId
187
+ }, listName ? {
188
+ title: `Export ${listName}`
189
+ } : {}, memberFilter ? {
190
+ filter: memberFilter
191
+ } : {})),
192
+ children: 'Export'
193
+ }) : null,
194
+ transfer && enrollHostId ? /*#__PURE__*/ _jsx(TransferResumeImport, _extends({
195
+ target: transferTarget
196
+ }, listName ? {
197
+ title: `Import into ${listName}`
198
+ } : {}, {
199
+ onFinished: onMembershipChanged
200
+ })) : null,
201
+ transfer && canImportMembers ? /*#__PURE__*/ _jsx(Button, {
160
202
  size: "small",
161
203
  color: "primary",
162
204
  variant: "contained",
@@ -166,9 +208,17 @@ export function ListDetailCard(props) {
166
208
  }),
167
209
  // An import enrolls as a site; on the org page one has to be chosen.
168
210
  disabled: !enrollHostId,
169
- onClick: ()=>setImporting(true),
211
+ onClick: ()=>transfer.openImport(_extends({
212
+ resource: listMembersResourceKey(listId),
213
+ scope: 'host',
214
+ hostId: enrollHostId
215
+ }, listName ? {
216
+ title: `Import into ${listName}`
217
+ } : {}, {
218
+ onFinished: onMembershipChanged
219
+ })),
170
220
  children: 'Import'
171
- })
221
+ }) : null
172
222
  ]
173
223
  });
174
224
  if (!list) {
@@ -210,7 +260,7 @@ export function ListDetailCard(props) {
210
260
  const hasFilters = rule.sources.length > 0;
211
261
  const summary = hasFilters ? describeDynamicListRule(rule) : [];
212
262
  /* The card, named so the page chrome above it is a plain list of
213
- what this surface publishes upward. */ const card = /*#__PURE__*/ _jsxs(CardDisplay, {
263
+ what this surface publishes upward. */ const card = /*#__PURE__*/ _jsx(CardDisplay, {
214
264
  header: 'Audience',
215
265
  help: pluginDocsHelp('emailCampaigns', {
216
266
  anchor: '#list-members'
@@ -221,81 +271,72 @@ export function ListDetailCard(props) {
221
271
  HeaderProps: {
222
272
  action: headerActions
223
273
  },
224
- children: [
225
- /*#__PURE__*/ _jsxs(Stack, {
226
- spacing: 2,
227
- children: [
228
- /*#__PURE__*/ _jsxs(Stack, {
229
- direction: "row",
230
- spacing: 1,
231
- useFlexGap: true,
232
- sx: {
233
- flexWrap: 'wrap',
234
- alignItems: 'center'
235
- },
236
- children: [
237
- dynamic ? /*#__PURE__*/ _jsx(Chip, {
238
- size: "small",
239
- color: "primary",
240
- variant: "outlined",
241
- label: ((_list_lastEvaluatedAt = list['lastEvaluatedAt']) == null ? void 0 : _list_lastEvaluatedAt.toDate) ? `Rule · ${list['lastEvaluatedAt'].toDate().toLocaleString()}` : 'Rule · not yet evaluated'
242
- }) : /*#__PURE__*/ _jsx(Chip, {
243
- size: "small",
244
- variant: "outlined",
245
- label: "Fixed membership"
246
- }),
247
- /*#__PURE__*/ _jsx(Typography, {
248
- variant: "body2",
249
- color: "text.secondary",
250
- children: count === null ? 'Counting subscribers…' : count === 1 ? '1 subscriber' : `${count.toLocaleString()} subscribers`
251
- })
252
- ]
253
- }),
254
- dynamic && hasFilters ? /*#__PURE__*/ _jsx(Stack, {
255
- spacing: 0.5,
256
- children: summary.map((clause)=>/*#__PURE__*/ _jsx(Typography, {
257
- variant: "body2",
258
- color: "text.secondary",
259
- children: clause
260
- }, clause))
261
- }) : null,
262
- orgMount ? /*#__PURE__*/ _jsx(OrgSiteSelect, {
263
- mount: orgMount,
264
- label: "Enroll as",
265
- value: enrollHostId,
266
- onChange: setChosenHostId,
267
- helperText: 'People you add are enrolled as this site, and the Consent ' + 'column reads what they agreed to with it.'
268
- }) : null,
269
- orgMount && !enrollHostId ? /*#__PURE__*/ _jsx(Typography, {
270
- variant: "body2",
271
- color: "text.secondary",
272
- children: 'Choose the site to work this audience as. Whether someone may ' + 'be emailed depends on what they agreed to with a site, so the ' + 'members and their consent are read for one site at a time.'
273
- }) : scope && consentGroup ? /*#__PURE__*/ _jsx(ListMembersPanel, {
274
- hostId: enrollHostId,
275
- consentGroup: consentGroup,
276
- scope: scope,
277
- listId: listId,
278
- listName: String((_list_name = list['name']) != null ? _list_name : ''),
279
- onMembershipChanged: onMembershipChanged,
280
- /*
274
+ children: /*#__PURE__*/ _jsxs(Stack, {
275
+ spacing: 2,
276
+ children: [
277
+ /*#__PURE__*/ _jsxs(Stack, {
278
+ direction: "row",
279
+ spacing: 1,
280
+ useFlexGap: true,
281
+ sx: {
282
+ flexWrap: 'wrap',
283
+ alignItems: 'center'
284
+ },
285
+ children: [
286
+ dynamic ? /*#__PURE__*/ _jsx(Chip, {
287
+ size: "small",
288
+ color: "primary",
289
+ variant: "outlined",
290
+ label: ((_list_lastEvaluatedAt = list['lastEvaluatedAt']) == null ? void 0 : _list_lastEvaluatedAt.toDate) ? `Rule · ${list['lastEvaluatedAt'].toDate().toLocaleString()}` : 'Rule · not yet evaluated'
291
+ }) : /*#__PURE__*/ _jsx(Chip, {
292
+ size: "small",
293
+ variant: "outlined",
294
+ label: "Fixed membership"
295
+ }),
296
+ /*#__PURE__*/ _jsx(Typography, {
297
+ variant: "body2",
298
+ color: "text.secondary",
299
+ children: count === null ? 'Counting subscribers…' : count === 1 ? '1 subscriber' : `${count.toLocaleString()} subscribers`
300
+ })
301
+ ]
302
+ }),
303
+ dynamic && hasFilters ? /*#__PURE__*/ _jsx(Stack, {
304
+ spacing: 0.5,
305
+ children: summary.map((clause)=>/*#__PURE__*/ _jsx(Typography, {
306
+ variant: "body2",
307
+ color: "text.secondary",
308
+ children: clause
309
+ }, clause))
310
+ }) : null,
311
+ orgMount ? /*#__PURE__*/ _jsx(OrgSiteSelect, {
312
+ mount: orgMount,
313
+ label: "Enroll as",
314
+ value: enrollHostId,
315
+ onChange: setChosenHostId,
316
+ helperText: 'People you add are enrolled as this site, and the Consent ' + 'column reads what they agreed to with it.'
317
+ }) : null,
318
+ orgMount && !enrollHostId ? /*#__PURE__*/ _jsx(Typography, {
319
+ variant: "body2",
320
+ color: "text.secondary",
321
+ children: 'Choose the site to work this audience as. Whether someone may ' + 'be emailed depends on what they agreed to with a site, so the ' + 'members and their consent are read for one site at a time.'
322
+ }) : scope && consentGroup ? /*#__PURE__*/ _jsx(ListMembersPanel, {
323
+ hostId: enrollHostId,
324
+ consentGroup: consentGroup,
325
+ scope: scope,
326
+ listId: listId,
327
+ listName: String((_list_name = list['name']) != null ? _list_name : ''),
328
+ onMembershipChanged: onMembershipChanged,
329
+ /*
281
330
  Offered only on a FIXED list. A live list's membership is the
282
331
  sweep's to decide, and hand-adding its own matches would write
283
332
  `via: 'manual'` copies of rows the sweep already owns — rows it
284
333
  then may not reconcile away when the person stops matching.
285
334
  */ findRule: dynamic || !hasFilters ? null : rule,
286
- ruleSummary: summary
287
- }, enrollHostId) : null
288
- ]
289
- }),
290
- importing && enrollHostId ? /*#__PURE__*/ _jsx(ListImportDrawer, {
291
- open: true,
292
- onClose: ()=>setImporting(false),
293
- hostId: enrollHostId,
294
- listId: listId,
295
- listName: String((_list_name1 = list['name']) != null ? _list_name1 : ''),
296
- onMembershipChanged: onMembershipChanged
297
- }) : null
298
- ]
335
+ ruleSummary: summary,
336
+ onFilterChange: setMemberFilter
337
+ }, enrollHostId) : null
338
+ ]
339
+ })
299
340
  });
300
341
  return /*#__PURE__*/ _jsxs(_Fragment, {
301
342
  children: [