@aglyn/plugins-email 1.0.0-beta.195 → 1.0.0-beta.197

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 (50) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/consent-group-access.d.ts +33 -0
  3. package/src/lib/components/consent-group-access.js +73 -0
  4. package/src/lib/components/consent-group-access.js.map +1 -0
  5. package/src/lib/components/consent-group-change-progress.d.ts +55 -0
  6. package/src/lib/components/consent-group-change-progress.js +282 -0
  7. package/src/lib/components/consent-group-change-progress.js.map +1 -0
  8. package/src/lib/components/consent-group-confirmation-card.d.ts +6 -16
  9. package/src/lib/components/consent-group-confirmation-card.js +7 -50
  10. package/src/lib/components/consent-group-confirmation-card.js.map +1 -1
  11. package/src/lib/components/consent-group-dialog.d.ts +53 -0
  12. package/src/lib/components/consent-group-dialog.js +495 -0
  13. package/src/lib/components/consent-group-dialog.js.map +1 -0
  14. package/src/lib/components/consent-group-editing.d.ts +127 -0
  15. package/src/lib/components/consent-group-editing.js +225 -0
  16. package/src/lib/components/consent-group-editing.js.map +1 -0
  17. package/src/lib/components/consent-group-review.d.ts +62 -0
  18. package/src/lib/components/consent-group-review.js +353 -0
  19. package/src/lib/components/consent-group-review.js.map +1 -0
  20. package/src/lib/components/consent-group-site-picker.d.ts +35 -0
  21. package/src/lib/components/consent-group-site-picker.js +190 -0
  22. package/src/lib/components/consent-group-site-picker.js.map +1 -0
  23. package/src/lib/components/consent-groups-api.d.ts +158 -0
  24. package/src/lib/components/consent-groups-api.js +250 -0
  25. package/src/lib/components/consent-groups-api.js.map +1 -0
  26. package/src/lib/components/consent-groups-card.d.ts +47 -0
  27. package/src/lib/components/consent-groups-card.js +351 -0
  28. package/src/lib/components/consent-groups-card.js.map +1 -0
  29. package/src/lib/components/email-screens-card.js +1 -1
  30. package/src/lib/components/email-screens-card.js.map +1 -1
  31. package/src/lib/components/emails-console-page.d.ts +1 -1
  32. package/src/lib/components/emails-console-page.js +62 -27
  33. package/src/lib/components/emails-console-page.js.map +1 -1
  34. package/src/lib/components/emails-console-sections.d.ts +1 -1
  35. package/src/lib/components/emails-console-sections.js +14 -1
  36. package/src/lib/components/emails-console-sections.js.map +1 -1
  37. package/src/lib/components/org-email-templates-card.js +1 -1
  38. package/src/lib/components/org-email-templates-card.js.map +1 -1
  39. package/src/lib/components/site-consent-group-card.d.ts +23 -0
  40. package/src/lib/components/site-consent-group-card.js +80 -0
  41. package/src/lib/components/site-consent-group-card.js.map +1 -0
  42. package/src/lib/components/suppressions-card.js +28 -7
  43. package/src/lib/components/suppressions-card.js.map +1 -1
  44. package/src/lib/server-console.js +4 -2
  45. package/src/lib/server-console.js.map +1 -1
  46. package/src/lib/server-list-gate.d.ts +9 -0
  47. package/src/lib/server-list-gate.js +23 -9
  48. package/src/lib/server-list-gate.js.map +1 -1
  49. package/src/lib/server-list-import.js +5 -2
  50. package/src/lib/server-list-import.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.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 { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, Timestamp, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\n\n/** How many of the site's screens one read of this list covers. */\nconst TEMPLATE_CEILING = 200\n\n/*\n * What the templates grid's Filters panel offers (AGL-3317). The card holds\n * the templates its capped window read, so it answers the panel over all of\n * them; the notice under the table says when the cap truncated the window.\n * Origin reads `originKey`, the provenance the column draws.\n */\nconst TEMPLATE_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text', 'templateName'),\n inMemoryListField('origin', 'select', 'originKey'),\n]\nconst TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n origin: 'Origin',\n}\nconst TEMPLATE_FILTER_OPTIONS = {\n origin: [\n { value: 'local', label: 'Yours' },\n { value: 'installed', label: 'Installed' },\n ],\n}\nconst TEMPLATE_SEARCH_FIELDS = ['templateName'] as const\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## Why the read is a CEILING with a probe\n *\n * `screens` holds every kind of screen the site has and the email ones are\n * picked out here, so the cap cannot be a page: a page of the collection is\n * not a page of this list. `collectionCeiling` orders on the document name,\n * which is the one ordering that cannot drop a screen — `orderBy('displayName')`\n * matches only documents that HAVE the field, so a screen created without a\n * name would vanish rather than sort oddly. It reads one past the ceiling so\n * the card can SAY when the site holds more than it drew, and the footer under\n * the table pages the window the card already has.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const { data: screenDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'screens'),\n TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readScreens, truncated } = ceilingedWindow<any>(\n screenDocs,\n TEMPLATE_CEILING,\n )\n const emailScreens = useMemo(\n () =>\n [...readScreens]\n .filter((screen: any) => !screen.deletedAt && screen.kind === 'email')\n .sort((a: any, b: any) =>\n String(a.displayName ?? '').localeCompare(\n String(b.displayName ?? ''),\n ),\n )\n .map((screen: any) => ({\n ...screen,\n templateName: String(screen.displayName ?? 'Untitled template'),\n originKey:\n templateProvenance(screen).origin === 'installed' ? 'installed' : 'local',\n })),\n [readScreens],\n )\n const templateFilter = useListRowsFilter<any>({\n rows: emailScreens,\n fields: TEMPLATE_FILTER_FIELDS,\n options: TEMPLATE_FILTER_OPTIONS,\n headers: TEMPLATE_FILTER_HEADERS,\n search: TEMPLATE_SEARCH_FIELDS,\n })\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {\n deletedAt: Timestamp.now(),\n })\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The templates are a window this card already holds, so the grid pages\n * it. The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) => row.originKey,\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#create-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips {...templateFilter.chipsProps} />\n <ListTable\n aria-label=\"Email templates\"\n rows={templateFilter.rows}\n columns={templateFilter.filterColumns(columns)}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // The panel and the search are the grid's; the card answers\n // them over every template its window read.\n {...templateFilter.gridProps}\n noRowsLabel=\"No templates match these filters\"\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`This site holds more than ${TEMPLATE_CEILING} screens, and the ` +\n 'templates listed are drawn from the first of them. Any beyond ' +\n 'that are not in this list.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListRowActions","ListTable","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","useListRowsFilter","useSnackbar","ceilingedWindow","collectionCeiling","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Stack","Typography","collection","doc","Timestamp","updateDoc","useRouter","useMemo","templateProvenance","createEmailScreen","TEMPLATE_CEILING","TEMPLATE_FILTER_FIELDS","TEMPLATE_FILTER_HEADERS","displayName","origin","TEMPLATE_FILTER_OPTIONS","value","label","TEMPLATE_SEARCH_FIELDS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","data","screenDocs","idField","rows","readScreens","truncated","emailScreens","filter","screen","deletedAt","kind","sort","a","b","String","localeCompare","map","templateName","originKey","templateFilter","fields","options","headers","search","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","rowActions","key","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","length","chipsProps","aria-label","filterColumns","rowHeight","onOpen","_id","gridProps","noRowsLabel","severity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.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 { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, Timestamp, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\n\n/** How many of the site's screens one read of this list covers. */\nconst TEMPLATE_CEILING = 200\n\n/*\n * What the templates grid's Filters panel offers (AGL-3317). The card holds\n * the templates its capped window read, so it answers the panel over all of\n * them; the notice under the table says when the cap truncated the window.\n * Origin reads `originKey`, the provenance the column draws.\n */\nconst TEMPLATE_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text', 'templateName'),\n inMemoryListField('origin', 'select', 'originKey'),\n]\nconst TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n origin: 'Origin',\n}\nconst TEMPLATE_FILTER_OPTIONS = {\n origin: [\n { value: 'local', label: 'Yours' },\n { value: 'installed', label: 'Installed' },\n ],\n}\nconst TEMPLATE_SEARCH_FIELDS = ['templateName'] as const\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## Why the read is a CEILING with a probe\n *\n * `screens` holds every kind of screen the site has and the email ones are\n * picked out here, so the cap cannot be a page: a page of the collection is\n * not a page of this list. `collectionCeiling` orders on the document name,\n * which is the one ordering that cannot drop a screen — `orderBy('displayName')`\n * matches only documents that HAVE the field, so a screen created without a\n * name would vanish rather than sort oddly. It reads one past the ceiling so\n * the card can SAY when the site holds more than it drew, and the footer under\n * the table pages the window the card already has.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const { data: screenDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'screens'),\n TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readScreens, truncated } = ceilingedWindow<any>(\n screenDocs,\n TEMPLATE_CEILING,\n )\n const emailScreens = useMemo(\n () =>\n [...readScreens]\n .filter((screen: any) => !screen.deletedAt && screen.kind === 'email')\n .sort((a: any, b: any) =>\n String(a.displayName ?? '').localeCompare(\n String(b.displayName ?? ''),\n ),\n )\n .map((screen: any) => ({\n ...screen,\n templateName: String(screen.displayName ?? 'Untitled template'),\n originKey:\n templateProvenance(screen).origin === 'installed' ? 'installed' : 'local',\n })),\n [readScreens],\n )\n const templateFilter = useListRowsFilter<any>({\n rows: emailScreens,\n fields: TEMPLATE_FILTER_FIELDS,\n options: TEMPLATE_FILTER_OPTIONS,\n headers: TEMPLATE_FILTER_HEADERS,\n search: TEMPLATE_SEARCH_FIELDS,\n })\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {\n deletedAt: Timestamp.now(),\n })\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The templates are a window this card already holds, so the grid pages\n * it. The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) => row.originKey,\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips {...templateFilter.chipsProps} />\n <ListTable\n aria-label=\"Email templates\"\n rows={templateFilter.rows}\n columns={templateFilter.filterColumns(columns)}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // The panel and the search are the grid's; the card answers\n // them over every template its window read.\n {...templateFilter.gridProps}\n noRowsLabel=\"No templates match these filters\"\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`This site holds more than ${TEMPLATE_CEILING} screens, and the ` +\n 'templates listed are drawn from the first of them. Any beyond ' +\n 'that are not in this list.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListRowActions","ListTable","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","useListRowsFilter","useSnackbar","ceilingedWindow","collectionCeiling","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Stack","Typography","collection","doc","Timestamp","updateDoc","useRouter","useMemo","templateProvenance","createEmailScreen","TEMPLATE_CEILING","TEMPLATE_FILTER_FIELDS","TEMPLATE_FILTER_HEADERS","displayName","origin","TEMPLATE_FILTER_OPTIONS","value","label","TEMPLATE_SEARCH_FIELDS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","data","screenDocs","idField","rows","readScreens","truncated","emailScreens","filter","screen","deletedAt","kind","sort","a","b","String","localeCompare","map","templateName","originKey","templateFilter","fields","options","headers","search","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","rowActions","key","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","length","chipsProps","aria-label","filterColumns","rowHeight","onOpen","_id","gridProps","noRowsLabel","severity"],"mappings":"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"}
@@ -27,7 +27,7 @@ import { type ConsolePluginPageProps } from '@aglyn/aglyn';
27
27
  *
28
28
  * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.
29
29
  * Handed no site, it publishes the org mount to its cards and renders
30
- * {@link orgSectionBody} instead: the same six sections, over every site.
30
+ * {@link orgSectionBody} instead: the same seven sections, over every site.
31
31
  */
32
32
  export declare function EmailsConsolePage(props: ConsolePluginPageProps): import("react").JSX.Element;
33
33
  export declare namespace EmailsConsolePage {
@@ -21,7 +21,9 @@ import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot
21
21
  import { HubSections } from "@aglyn/shared-ui-next";
22
22
  import { Stack } from "@mui/material";
23
23
  import { useMemo } from "react";
24
+ import { useConsentGroupAccess } from "./consent-group-access.js";
24
25
  import ConsentGroupConfirmationCard from "./consent-group-confirmation-card.js";
26
+ import ConsentGroupsCard from "./consent-groups-card.js";
25
27
  import EmailScreensCard from "./email-screens-card.js";
26
28
  import EmailTemplateDetail from "./email-template-detail.js";
27
29
  import EmailTopicDetail from "./email-topic-detail.js";
@@ -36,8 +38,9 @@ import OrgSiteSuppressions from "./org-site-suppressions.js";
36
38
  import OrgSuppressionsCard from "./org-suppressions-card.js";
37
39
  import SendingDomainDetail from "./sending-domain-detail.js";
38
40
  import SendingDomainsCard from "./sending-domains-card.js";
41
+ import SiteConsentGroupCard from "./site-consent-group-card.js";
39
42
  import SuppressionsCard from "./suppressions-card.js";
40
- import { EmailOrgMountProvider } from "./email-org-mount.js";
43
+ import { EmailOrgMountProvider, useEmailOrgMount } from "./email-org-mount.js";
41
44
  import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
42
45
  /**
43
46
  * The Messages section: a zone, drawn by whichever plugin owns campaigns.
@@ -51,6 +54,31 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
51
54
  slot: EMAIL_MESSAGES_ZONE.id
52
55
  }, props)) : null;
53
56
  }
57
+ /**
58
+ * The organization's Consent groups section: the editor, and under it the
59
+ * switch over how a group's sites treat each other's confirmations.
60
+ *
61
+ * Both ask whether the reader may change consent groups, and the two routes
62
+ * behind them ask the same two permissions, so the section reads the
63
+ * reader's membership once and hands the answer to both.
64
+ */ function OrgConsentGroupsSection(props) {
65
+ var _useEmailOrgMount;
66
+ const orgId = (_useEmailOrgMount = useEmailOrgMount()) == null ? void 0 : _useEmailOrgMount.orgId;
67
+ const access = useConsentGroupAccess(orgId || undefined);
68
+ return /*#__PURE__*/ _jsxs(Stack, {
69
+ spacing: 3,
70
+ children: [
71
+ /*#__PURE__*/ _jsx(ConsentGroupsCard, {
72
+ org: props.org,
73
+ access: access
74
+ }),
75
+ /*#__PURE__*/ _jsx(ConsentGroupConfirmationCard, {
76
+ org: props.org,
77
+ access: access
78
+ })
79
+ ]
80
+ });
81
+ }
54
82
  /**
55
83
  * The body of one emails section, built only when that section is the one
56
84
  * being read (AGL-2501).
@@ -69,7 +97,10 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
69
97
  * Only the audience detail needs it today — it is what its membership table
70
98
  * reads consent FOR — but it is resolved at the page because the answer is a
71
99
  * property of the org and this is the one place holding the org document.
72
- */ consentGroup) {
100
+ */ consentGroup, /**
101
+ * The org document, from which the Consent groups section reads the org's
102
+ * slug and any change in flight.
103
+ */ org) {
73
104
  switch(section){
74
105
  case 'messages':
75
106
  /*
@@ -168,6 +199,14 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
168
199
  hostId: hostId,
169
200
  basePath: basePath
170
201
  });
202
+ case 'consent-groups':
203
+ // Which sender this site is. Declared for the organization, so here it
204
+ // is said and linked, never changed.
205
+ return /*#__PURE__*/ _jsx(SiteConsentGroupCard, {
206
+ hostId: hostId,
207
+ consentGroup: consentGroup,
208
+ org: org
209
+ });
171
210
  case 'suppressions':
172
211
  return /*#__PURE__*/ _jsx(SuppressionsCard, {
173
212
  hostId: hostId
@@ -179,13 +218,13 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
179
218
  /**
180
219
  * The body of one ORGANIZATION-level section, at `/[orgSlug]/emails`.
181
220
  *
182
- * The same six sections as a site's, each answering over the organization.
183
- * Two of them are already the org's — an audience and a topic are shared by
184
- * every site — so their cards render unchanged with no site. The other four
185
- * are site facts: a message is sent as one site, a template is one site's
186
- * screen, a sending identity and a suppression list are one site's. Those
187
- * sections read each site in turn, a page of sites at a time, and link a row
188
- * into the site that holds it.
221
+ * The same seven sections as a site's, each answering over the organization.
222
+ * Three of them are already the org's — an audience and a topic are shared by
223
+ * every site, and a consent group is declared for several — so their cards
224
+ * render with no site. The other four are site facts: a message is sent as
225
+ * one site, a template is one site's screen, a sending identity and a
226
+ * suppression list are one site's. Those sections read each site in turn, a
227
+ * page of sites at a time, and link a row into the site that holds it.
189
228
  *
190
229
  * Built only when that section is the one being read, for the reason
191
230
  * {@link sectionBody} is: every card opens its reads on mount.
@@ -225,22 +264,9 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
225
264
  hostId: null,
226
265
  topicId: detail[0],
227
266
  basePath: basePath
228
- }) : /*
229
- * The catalog, whose topics each set their own confirmation, and under
230
- * it the organization's one switch over how a declared consent group
231
- * treats a confirmation one of its sites is waiting on. Only here: the
232
- * switch is the organization's, so a site's Topics page has none.
233
- */ /*#__PURE__*/ _jsxs(Stack, {
234
- spacing: 3,
235
- children: [
236
- /*#__PURE__*/ _jsx(EmailTopicsCard, {
237
- hostId: null,
238
- basePath: basePath
239
- }),
240
- /*#__PURE__*/ _jsx(ConsentGroupConfirmationCard, {
241
- org: org
242
- })
243
- ]
267
+ }) : /*#__PURE__*/ _jsx(EmailTopicsCard, {
268
+ hostId: null,
269
+ basePath: basePath
244
270
  });
245
271
  case 'sending':
246
272
  /*
@@ -253,6 +279,15 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
253
279
  }) : /*#__PURE__*/ _jsx(OrgSendingCard, {
254
280
  basePath: basePath
255
281
  });
282
+ case 'consent-groups':
283
+ /*
284
+ * The declaration itself, and under it the organization's one switch
285
+ * over how a declared group treats a confirmation one of its sites is
286
+ * waiting on — the switch belongs with the groups it governs rather
287
+ * than with the topics that each set their own confirmation.
288
+ */ return /*#__PURE__*/ _jsx(OrgConsentGroupsSection, {
289
+ org: org
290
+ });
256
291
  case 'suppressions':
257
292
  /*
258
293
  * `…/suppressions` totals every site's list; `…/suppressions/{hostId}`
@@ -292,7 +327,7 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
292
327
  *
293
328
  * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.
294
329
  * Handed no site, it publishes the org mount to its cards and renders
295
- * {@link orgSectionBody} instead: the same six sections, over every site.
330
+ * {@link orgSectionBody} instead: the same seven sections, over every site.
296
331
  */ export function EmailsConsolePage(props) {
297
332
  const { hostId, orgMount, org, section, sections, basePath, segments } = props;
298
333
  /*==========================================
@@ -337,7 +372,7 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
337
372
  }
338
373
  return /*#__PURE__*/ _jsx(HubSections, {
339
374
  sections: sections,
340
- children: sectionBody(section, hostId, detail, basePath, consentGroup)
375
+ children: sectionBody(section, hostId, detail, basePath, consentGroup, org)
341
376
  });
342
377
  }
343
378
  EmailsConsolePage.displayName = 'EmailsConsolePage';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-page.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 consentGroupForHost,\n type ConsentGroup,\n type ConsolePluginOrgMount,\n type ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport ConsentGroupConfirmationCard from './consent-group-confirmation-card'\nimport EmailScreensCard from './email-screens-card'\nimport EmailTemplateDetail from './email-template-detail'\nimport EmailTopicDetail from './email-topic-detail'\nimport EmailTopicsCard from './email-topics-card'\nimport ListDetailCard from './list-detail-card'\nimport ListEditCard from './list-edit-card'\nimport ListsCard from './lists-card'\nimport OrgEmailTemplatesCard from './org-email-templates-card'\nimport OrgSendingCard from './org-sending-card'\nimport OrgSendingDomainDetail from './org-sending-domain-detail'\nimport OrgSiteSuppressions from './org-site-suppressions'\nimport OrgSuppressionsCard from './org-suppressions-card'\nimport SendingDomainDetail from './sending-domain-detail'\nimport SendingDomainsCard from './sending-domains-card'\nimport SuppressionsCard from './suppressions-card'\nimport type { EmailsConsoleSectionId } from './emails-console-sections'\nimport { EmailOrgMountProvider } from './email-org-mount'\nimport { EMAIL_MESSAGES_ZONE, type EmailMessagesZoneProps } from './email-zones'\n\n/**\n * The Messages section: a zone, drawn by whichever plugin owns campaigns.\n *\n * Its own component because a zone's renderer comes from a hook and\n * `sectionBody` is a plain function — and so that, like every other branch\n * there, it is constructed only while Messages is the section being read.\n */\nfunction EmailMessagesSection(props: EmailMessagesZoneProps) {\n const Zone = useConsoleWidgetSlot()\n return Zone ? <Zone slot={EMAIL_MESSAGES_ZONE.id} {...props} /> : null\n}\n\n/**\n * The body of one emails section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: EmailsConsoleSectionId,\n hostId: string,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n /**\n * The controller the surface is being viewed as, resolved once by the page.\n *\n * Only the audience detail needs it today — it is what its membership table\n * reads consent FOR — but it is resolved at the page because the answer is a\n * property of the org and this is the one place holding the org document.\n */\n consentGroup: ConsentGroup,\n): ReactNode {\n switch (section) {\n case 'messages':\n /*\n * `/emails/messages/{emailId}` is ONE MESSAGE — the thing that was or\n * will be sent, as against the campaign that groups messages and the\n * template they are built from. A ROUTE rather than an expanded row: it\n * is linkable, which is what a merchant wants to paste into a message\n * about last week's send, and its preview, link table and recipient\n * list are reads the list above it must not pay for.\n *\n * The page links OUT to the campaign this message belongs to, which is\n * a section of the Marketing console.\n */\n /*\n * `…/{emailId}/edit` WRITES the email and `…/{emailId}` reports on\n * it — two jobs with two shapes, and creating stays a drawer on the\n * list. Which of the three is drawn is decided from `detail` by the\n * widget, which builds only the one it is asked for: the composer's\n * listens are not paid for by somebody reading a report.\n */\n return (\n <EmailMessagesSection\n hostId={hostId}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * `/emails/templates/{screenId}` is one TEMPLATE's page.\n *\n * A route for the same reasons a message's page is one: it is\n * linkable, and the listing above it is a cheaper surface a reader who\n * came for one template should not have to mount. The preview, the\n * aggregate figures and the recipients table all hang off this branch,\n * so none of them is constructed while the list is what is being read.\n */\n return detail[0] ? (\n <EmailTemplateDetail\n hostId={hostId}\n screenId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailScreensCard hostId={hostId} basePath={basePath} />\n )\n case 'audiences':\n /*\n * A list is a resource with its own pages, on the same terms a message\n * is: `/emails/audiences/{listId}` is one audience, and `…/edit` is its\n * settings.\n *\n * The membership used to unfold inside the audiences table. That made a\n * list unlinkable, put the back button one press from leaving the whole\n * surface, and asked the reader of a list of lists to hold the table\n * that lists them AND the table of one list's subscribers on the same\n * screen. The subscribers are also the expensive read here — one PII\n * document per person — so putting them behind a route is what stops\n * them being paid for by somebody who came to see which audiences exist.\n *\n * Ternaries rather than a lookup: only the branch taken is CONSTRUCTED,\n * which is the cost this whole function is shaped around.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard\n hostId={hostId}\n listId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <ListDetailCard\n hostId={hostId}\n consentGroup={consentGroup}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={hostId} basePath={basePath} />\n )\n case 'topics':\n // Create is a drawer on the list; EDIT is the topic's own route, which\n // is the section owning its own subtree exactly as `messages` does.\n return detail[0] ? (\n <EmailTopicDetail\n hostId={hostId}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={hostId} basePath={basePath} />\n )\n case 'sending':\n /*\n * `/emails/sending/{domain}` is one domain's page, and it is a route\n * for the reason every other detail here is: the DNS records, the DMARC\n * read and the verification button are what somebody came for, and a\n * link to them is what they paste to whoever actually edits the zone.\n *\n * The domain is the document id, so it is also the segment. It is\n * decoded because a URL carries it encoded and the record is keyed on\n * the bare name.\n */\n return detail[0] ? (\n <SendingDomainDetail\n hostId={hostId}\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <SendingDomainsCard hostId={hostId} basePath={basePath} />\n )\n case 'suppressions':\n return <SuppressionsCard hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section, at `/[orgSlug]/emails`.\n *\n * The same six sections as a site's, each answering over the organization.\n * Two of them are already the org's — an audience and a topic are shared by\n * every site — so their cards render unchanged with no site. The other four\n * are site facts: a message is sent as one site, a template is one site's\n * screen, a sending identity and a suppression list are one site's. Those\n * sections read each site in turn, a page of sites at a time, and link a row\n * into the site that holds it.\n *\n * Built only when that section is the one being read, for the reason\n * {@link sectionBody} is: every card opens its reads on mount.\n */\nfunction orgSectionBody(\n section: EmailsConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n orgMount: ConsolePluginOrgMount,\n /** The org document, from which an audience's consent group is resolved. */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n // Every site's messages, drawn by the plugin that sends; it is handed\n // the org mount so it can say which site each was sent as.\n return (\n <EmailMessagesSection\n hostId={null}\n orgMount={orgMount}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * One table over every site's templates. A template is a screen on one\n * site, so its own page stays that site's: each row links there, and\n * nothing beneath this section is an org route.\n */\n return <OrgEmailTemplatesCard />\n case 'audiences':\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard hostId={null} listId={detail[0]} basePath={basePath} />\n ) : (\n <ListDetailCard\n hostId={null}\n org={org}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={null} basePath={basePath} />\n )\n case 'topics':\n return detail[0] ? (\n <EmailTopicDetail\n hostId={null}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n /*\n * The catalog, whose topics each set their own confirmation, and under\n * it the organization's one switch over how a declared consent group\n * treats a confirmation one of its sites is waiting on. Only here: the\n * switch is the organization's, so a site's Topics page has none.\n */\n <Stack spacing={3}>\n <EmailTopicsCard hostId={null} basePath={basePath} />\n <ConsentGroupConfirmationCard org={org} />\n </Stack>\n )\n case 'sending':\n /*\n * A domain is proved by the organization, so its page is an org route\n * here as it is under a site. What each SITE sends as is the other half,\n * and the section's own page lists it per site.\n */\n return detail[0] ? (\n <OrgSendingDomainDetail\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <OrgSendingCard basePath={basePath} />\n )\n case 'suppressions':\n /*\n * `…/suppressions` totals every site's list; `…/suppressions/{hostId}`\n * is one site's list itself, the same card a site's own page draws.\n */\n return detail[0] ? (\n <OrgSiteSuppressions hostId={detail[0]} />\n ) : (\n <OrgSuppressionsCard />\n )\n default:\n return null\n }\n}\n\n/**\n * Emails page (AGL-395): the console surface owned by the email plugin,\n * rendered by the shell's generic plugin route.\n *\n * Two of its sections are two different things a merchant calls \"an email\":\n * an EMAIL is one message that was or will be sent, and a TEMPLATE is the\n * reusable besigner document a message is built from. Keeping them apart is\n * what lets each carry its own report — a message's own numbers, and a\n * template's summed across every message sent from it. Audience lists, the\n * topic catalog, the sending identities and the suppression list complete the\n * surface.\n *\n * The CAMPAIGN that groups messages is not here. It is a container with a\n * window of dates, a set of lists, a topic and revenue attribution — a\n * marketing object that reaches people by email — so it is a section of the\n * Marketing console, and a message's page links out to the campaign it\n * belongs to.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `emails-console-read-cost.spec.tsx` was written\n * BEFORE the conversion precisely to hold that line, and reports the same\n * counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag\n * somebody has to remember on the next surface.\n *\n * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.\n * Handed no site, it publishes the org mount to its cards and renders\n * {@link orgSectionBody} instead: the same six sections, over every site.\n */\nexport function EmailsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } =\n props\n\n /*==========================================\n * THE CONTROLLER THIS SURFACE IS BEING VIEWED AS.\n *\n * The declared group of sites that are one sender, or this site alone. Every\n * consent question asked below is asked FOR a controller and not for a site:\n * a grant is looked up under the asking site, a refusal is honored across\n * the whole group, and a grant held by a site outside it belongs to somebody\n * else. It is the same group `performCampaignSend` resolves, so the audience\n * table and the send agree about the same document by construction.\n *\n * Pure, from the org document the shell already passed, so it costs no read.\n * An absent org resolves to the group of one, which is the narrow answer.\n *\n * Only under a site. The organization's page has no site to resolve a group\n * FOR, and `consentGroupForHost` refuses to invent one — the audience page\n * there resolves it for the site the reader enrolls as, once one is chosen.\n *=========================================*/\n const consentGroup = useMemo(\n () =>\n hostId == null\n ? null\n : consentGroupForHost(org as Record<string, unknown>, hostId),\n [org, hostId],\n )\n\n /*\n * Nothing, deliberately, while the redirect is in flight. Rendering the\n * default section here would issue its listens on a URL about to be\n * replaced — on every arrival at `/emails`, which is every nav-tab click.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!orgMount) return null\n return (\n <EmailOrgMountProvider mount={orgMount} basePath={basePath}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as EmailsConsoleSectionId,\n detail,\n basePath,\n orgMount,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n </EmailOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as EmailsConsoleSectionId,\n hostId,\n detail,\n basePath,\n consentGroup as ConsentGroup,\n )}\n </HubSections>\n )\n}\nEmailsConsolePage.displayName = 'EmailsConsolePage'\n\nexport default EmailsConsolePage\n"],"names":["consentGroupForHost","useConsoleWidgetSlot","HubSections","Stack","useMemo","ConsentGroupConfirmationCard","EmailScreensCard","EmailTemplateDetail","EmailTopicDetail","EmailTopicsCard","ListDetailCard","ListEditCard","ListsCard","OrgEmailTemplatesCard","OrgSendingCard","OrgSendingDomainDetail","OrgSiteSuppressions","OrgSuppressionsCard","SendingDomainDetail","SendingDomainsCard","SuppressionsCard","EmailOrgMountProvider","EMAIL_MESSAGES_ZONE","EmailMessagesSection","props","Zone","slot","id","sectionBody","section","hostId","detail","basePath","consentGroup","screenId","listId","topicId","domain","decodeURIComponent","orgSectionBody","orgMount","org","spacing","EmailsConsolePage","sections","segments","length","slice","mount","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-page.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 consentGroupForHost,\n type ConsentGroup,\n type ConsolePluginOrgMount,\n type ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport { useConsentGroupAccess } from './consent-group-access'\nimport ConsentGroupConfirmationCard from './consent-group-confirmation-card'\nimport ConsentGroupsCard from './consent-groups-card'\nimport EmailScreensCard from './email-screens-card'\nimport EmailTemplateDetail from './email-template-detail'\nimport EmailTopicDetail from './email-topic-detail'\nimport EmailTopicsCard from './email-topics-card'\nimport ListDetailCard from './list-detail-card'\nimport ListEditCard from './list-edit-card'\nimport ListsCard from './lists-card'\nimport OrgEmailTemplatesCard from './org-email-templates-card'\nimport OrgSendingCard from './org-sending-card'\nimport OrgSendingDomainDetail from './org-sending-domain-detail'\nimport OrgSiteSuppressions from './org-site-suppressions'\nimport OrgSuppressionsCard from './org-suppressions-card'\nimport SendingDomainDetail from './sending-domain-detail'\nimport SendingDomainsCard from './sending-domains-card'\nimport SiteConsentGroupCard from './site-consent-group-card'\nimport SuppressionsCard from './suppressions-card'\nimport type { EmailsConsoleSectionId } from './emails-console-sections'\nimport { EmailOrgMountProvider, useEmailOrgMount } from './email-org-mount'\nimport { EMAIL_MESSAGES_ZONE, type EmailMessagesZoneProps } from './email-zones'\n\n/**\n * The Messages section: a zone, drawn by whichever plugin owns campaigns.\n *\n * Its own component because a zone's renderer comes from a hook and\n * `sectionBody` is a plain function — and so that, like every other branch\n * there, it is constructed only while Messages is the section being read.\n */\nfunction EmailMessagesSection(props: EmailMessagesZoneProps) {\n const Zone = useConsoleWidgetSlot()\n return Zone ? <Zone slot={EMAIL_MESSAGES_ZONE.id} {...props} /> : null\n}\n\n/**\n * The organization's Consent groups section: the editor, and under it the\n * switch over how a group's sites treat each other's confirmations.\n *\n * Both ask whether the reader may change consent groups, and the two routes\n * behind them ask the same two permissions, so the section reads the\n * reader's membership once and hands the answer to both.\n */\nfunction OrgConsentGroupsSection(props: { org?: Record<string, unknown> }) {\n const orgId = useEmailOrgMount()?.orgId\n const access = useConsentGroupAccess(orgId || undefined)\n return (\n <Stack spacing={3}>\n <ConsentGroupsCard org={props.org} access={access} />\n <ConsentGroupConfirmationCard org={props.org} access={access} />\n </Stack>\n )\n}\n\n/**\n * The body of one emails section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: EmailsConsoleSectionId,\n hostId: string,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n /**\n * The controller the surface is being viewed as, resolved once by the page.\n *\n * Only the audience detail needs it today — it is what its membership table\n * reads consent FOR — but it is resolved at the page because the answer is a\n * property of the org and this is the one place holding the org document.\n */\n consentGroup: ConsentGroup,\n /**\n * The org document, from which the Consent groups section reads the org's\n * slug and any change in flight.\n */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n /*\n * `/emails/messages/{emailId}` is ONE MESSAGE — the thing that was or\n * will be sent, as against the campaign that groups messages and the\n * template they are built from. A ROUTE rather than an expanded row: it\n * is linkable, which is what a merchant wants to paste into a message\n * about last week's send, and its preview, link table and recipient\n * list are reads the list above it must not pay for.\n *\n * The page links OUT to the campaign this message belongs to, which is\n * a section of the Marketing console.\n */\n /*\n * `…/{emailId}/edit` WRITES the email and `…/{emailId}` reports on\n * it — two jobs with two shapes, and creating stays a drawer on the\n * list. Which of the three is drawn is decided from `detail` by the\n * widget, which builds only the one it is asked for: the composer's\n * listens are not paid for by somebody reading a report.\n */\n return (\n <EmailMessagesSection\n hostId={hostId}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * `/emails/templates/{screenId}` is one TEMPLATE's page.\n *\n * A route for the same reasons a message's page is one: it is\n * linkable, and the listing above it is a cheaper surface a reader who\n * came for one template should not have to mount. The preview, the\n * aggregate figures and the recipients table all hang off this branch,\n * so none of them is constructed while the list is what is being read.\n */\n return detail[0] ? (\n <EmailTemplateDetail\n hostId={hostId}\n screenId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailScreensCard hostId={hostId} basePath={basePath} />\n )\n case 'audiences':\n /*\n * A list is a resource with its own pages, on the same terms a message\n * is: `/emails/audiences/{listId}` is one audience, and `…/edit` is its\n * settings.\n *\n * The membership used to unfold inside the audiences table. That made a\n * list unlinkable, put the back button one press from leaving the whole\n * surface, and asked the reader of a list of lists to hold the table\n * that lists them AND the table of one list's subscribers on the same\n * screen. The subscribers are also the expensive read here — one PII\n * document per person — so putting them behind a route is what stops\n * them being paid for by somebody who came to see which audiences exist.\n *\n * Ternaries rather than a lookup: only the branch taken is CONSTRUCTED,\n * which is the cost this whole function is shaped around.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard\n hostId={hostId}\n listId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <ListDetailCard\n hostId={hostId}\n consentGroup={consentGroup}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={hostId} basePath={basePath} />\n )\n case 'topics':\n // Create is a drawer on the list; EDIT is the topic's own route, which\n // is the section owning its own subtree exactly as `messages` does.\n return detail[0] ? (\n <EmailTopicDetail\n hostId={hostId}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={hostId} basePath={basePath} />\n )\n case 'sending':\n /*\n * `/emails/sending/{domain}` is one domain's page, and it is a route\n * for the reason every other detail here is: the DNS records, the DMARC\n * read and the verification button are what somebody came for, and a\n * link to them is what they paste to whoever actually edits the zone.\n *\n * The domain is the document id, so it is also the segment. It is\n * decoded because a URL carries it encoded and the record is keyed on\n * the bare name.\n */\n return detail[0] ? (\n <SendingDomainDetail\n hostId={hostId}\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <SendingDomainsCard hostId={hostId} basePath={basePath} />\n )\n case 'consent-groups':\n // Which sender this site is. Declared for the organization, so here it\n // is said and linked, never changed.\n return (\n <SiteConsentGroupCard\n hostId={hostId}\n consentGroup={consentGroup}\n org={org}\n />\n )\n case 'suppressions':\n return <SuppressionsCard hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section, at `/[orgSlug]/emails`.\n *\n * The same seven sections as a site's, each answering over the organization.\n * Three of them are already the org's — an audience and a topic are shared by\n * every site, and a consent group is declared for several — so their cards\n * render with no site. The other four are site facts: a message is sent as\n * one site, a template is one site's screen, a sending identity and a\n * suppression list are one site's. Those sections read each site in turn, a\n * page of sites at a time, and link a row into the site that holds it.\n *\n * Built only when that section is the one being read, for the reason\n * {@link sectionBody} is: every card opens its reads on mount.\n */\nfunction orgSectionBody(\n section: EmailsConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n orgMount: ConsolePluginOrgMount,\n /** The org document, from which an audience's consent group is resolved. */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n // Every site's messages, drawn by the plugin that sends; it is handed\n // the org mount so it can say which site each was sent as.\n return (\n <EmailMessagesSection\n hostId={null}\n orgMount={orgMount}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * One table over every site's templates. A template is a screen on one\n * site, so its own page stays that site's: each row links there, and\n * nothing beneath this section is an org route.\n */\n return <OrgEmailTemplatesCard />\n case 'audiences':\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard hostId={null} listId={detail[0]} basePath={basePath} />\n ) : (\n <ListDetailCard\n hostId={null}\n org={org}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={null} basePath={basePath} />\n )\n case 'topics':\n return detail[0] ? (\n <EmailTopicDetail\n hostId={null}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={null} basePath={basePath} />\n )\n case 'sending':\n /*\n * A domain is proved by the organization, so its page is an org route\n * here as it is under a site. What each SITE sends as is the other half,\n * and the section's own page lists it per site.\n */\n return detail[0] ? (\n <OrgSendingDomainDetail\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <OrgSendingCard basePath={basePath} />\n )\n case 'consent-groups':\n /*\n * The declaration itself, and under it the organization's one switch\n * over how a declared group treats a confirmation one of its sites is\n * waiting on — the switch belongs with the groups it governs rather\n * than with the topics that each set their own confirmation.\n */\n return <OrgConsentGroupsSection org={org} />\n case 'suppressions':\n /*\n * `…/suppressions` totals every site's list; `…/suppressions/{hostId}`\n * is one site's list itself, the same card a site's own page draws.\n */\n return detail[0] ? (\n <OrgSiteSuppressions hostId={detail[0]} />\n ) : (\n <OrgSuppressionsCard />\n )\n default:\n return null\n }\n}\n\n/**\n * Emails page (AGL-395): the console surface owned by the email plugin,\n * rendered by the shell's generic plugin route.\n *\n * Two of its sections are two different things a merchant calls \"an email\":\n * an EMAIL is one message that was or will be sent, and a TEMPLATE is the\n * reusable besigner document a message is built from. Keeping them apart is\n * what lets each carry its own report — a message's own numbers, and a\n * template's summed across every message sent from it. Audience lists, the\n * topic catalog, the sending identities and the suppression list complete the\n * surface.\n *\n * The CAMPAIGN that groups messages is not here. It is a container with a\n * window of dates, a set of lists, a topic and revenue attribution — a\n * marketing object that reaches people by email — so it is a section of the\n * Marketing console, and a message's page links out to the campaign it\n * belongs to.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `emails-console-read-cost.spec.tsx` was written\n * BEFORE the conversion precisely to hold that line, and reports the same\n * counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag\n * somebody has to remember on the next surface.\n *\n * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.\n * Handed no site, it publishes the org mount to its cards and renders\n * {@link orgSectionBody} instead: the same seven sections, over every site.\n */\nexport function EmailsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } =\n props\n\n /*==========================================\n * THE CONTROLLER THIS SURFACE IS BEING VIEWED AS.\n *\n * The declared group of sites that are one sender, or this site alone. Every\n * consent question asked below is asked FOR a controller and not for a site:\n * a grant is looked up under the asking site, a refusal is honored across\n * the whole group, and a grant held by a site outside it belongs to somebody\n * else. It is the same group `performCampaignSend` resolves, so the audience\n * table and the send agree about the same document by construction.\n *\n * Pure, from the org document the shell already passed, so it costs no read.\n * An absent org resolves to the group of one, which is the narrow answer.\n *\n * Only under a site. The organization's page has no site to resolve a group\n * FOR, and `consentGroupForHost` refuses to invent one — the audience page\n * there resolves it for the site the reader enrolls as, once one is chosen.\n *=========================================*/\n const consentGroup = useMemo(\n () =>\n hostId == null\n ? null\n : consentGroupForHost(org as Record<string, unknown>, hostId),\n [org, hostId],\n )\n\n /*\n * Nothing, deliberately, while the redirect is in flight. Rendering the\n * default section here would issue its listens on a URL about to be\n * replaced — on every arrival at `/emails`, which is every nav-tab click.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!orgMount) return null\n return (\n <EmailOrgMountProvider mount={orgMount} basePath={basePath}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as EmailsConsoleSectionId,\n detail,\n basePath,\n orgMount,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n </EmailOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as EmailsConsoleSectionId,\n hostId,\n detail,\n basePath,\n consentGroup as ConsentGroup,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n )\n}\nEmailsConsolePage.displayName = 'EmailsConsolePage'\n\nexport default EmailsConsolePage\n"],"names":["consentGroupForHost","useConsoleWidgetSlot","HubSections","Stack","useMemo","useConsentGroupAccess","ConsentGroupConfirmationCard","ConsentGroupsCard","EmailScreensCard","EmailTemplateDetail","EmailTopicDetail","EmailTopicsCard","ListDetailCard","ListEditCard","ListsCard","OrgEmailTemplatesCard","OrgSendingCard","OrgSendingDomainDetail","OrgSiteSuppressions","OrgSuppressionsCard","SendingDomainDetail","SendingDomainsCard","SiteConsentGroupCard","SuppressionsCard","EmailOrgMountProvider","useEmailOrgMount","EMAIL_MESSAGES_ZONE","EmailMessagesSection","props","Zone","slot","id","OrgConsentGroupsSection","orgId","access","undefined","spacing","org","sectionBody","section","hostId","detail","basePath","consentGroup","screenId","listId","topicId","domain","decodeURIComponent","orgSectionBody","orgMount","EmailsConsolePage","sections","segments","length","slice","mount","displayName"],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { ConsoleNavSection } from '@aglyn/aglyn';
18
- export type EmailsConsoleSectionId = 'messages' | 'templates' | 'audiences' | 'topics' | 'sending' | 'suppressions';
18
+ export type EmailsConsoleSectionId = 'messages' | 'templates' | 'audiences' | 'topics' | 'sending' | 'consent-groups' | 'suppressions';
19
19
  /**
20
20
  * The emails console's sections, in rail order (AGL-2501).
21
21
  *
@@ -38,7 +38,7 @@
38
38
  * the record rather than for the surface holding it. The shell prints the
39
39
  * surface and the section together — in the header, in the breadcrumb and
40
40
  * in the browser tab — so a section whose name repeats its surface's says
41
- * nothing about which of the six is open, which is the one job a section
41
+ * nothing about which section is open, which is the one job a section
42
42
  * name has. `messages` is also the word the rest of this surface already
43
43
  * uses for the record: a message's own page, a template's sends table and
44
44
  * the campaign detail all call it that.
@@ -91,6 +91,19 @@
91
91
  id: 'sending',
92
92
  label: 'Sending'
93
93
  },
94
+ /*
95
+ * WHICH SITES ARE ONE SENDER (AGL-3320).
96
+ *
97
+ * After Sending, because it answers the question beside it: Sending says
98
+ * what the `From:` line shows, this says which sites a signup, an
99
+ * unsubscribe and a CRM record are shared across. Before Suppressions,
100
+ * whose lists it decides the reach of — an address on one site's list is
101
+ * skipped by every site in that site's group. The organization declares
102
+ * the groups here; a site's own page says which one it is in.
103
+ */ {
104
+ id: 'consent-groups',
105
+ label: 'Consent groups'
106
+ },
94
107
  // Beside the audiences rather than inside them (AGL-2410): a suppression is
95
108
  // not a list you build, it is the reason a list you built did not all get
96
109
  // mailed.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-sections.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\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type EmailsConsoleSectionId =\n | 'messages'\n | 'templates'\n | 'audiences'\n | 'topics'\n | 'sending'\n | 'suppressions'\n\n/**\n * The emails console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids appear in links people keep — treat them as persisted, and prefer\n * renaming a LABEL over an id. Where the two have to move together, the old\n * path is kept alive by a redirect rather than by an id that no longer says\n * what the section is. `audiences` and `suppressions` are also the `?tab=`\n * ids this page deep-linked by before its sections became routes, so a\n * bookmark that named one of those tabs still names the same section.\n *\n * No `navTabId` on any of them: every section ships with the surface, so they\n * inherit the Emails nav item's gate.\n */\nexport const EMAILS_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n /*\n * The individual messages: one row per email that was or will be sent, each\n * with its own report.\n *\n * A message is what this surface is about, and this section is named for\n * the record rather than for the surface holding it. The shell prints the\n * surface and the section together — in the header, in the breadcrumb and\n * in the browser tab — so a section whose name repeats its surface's says\n * nothing about which of the six is open, which is the one job a section\n * name has. `messages` is also the word the rest of this surface already\n * uses for the record: a message's own page, a template's sends table and\n * the campaign detail all call it that.\n *\n * `/emails/emails/{id}` is answered by a permanent redirect in the console's\n * `next.config.js`, on the terms the Workflows → Automation move set: a URL\n * somebody pasted keeps resolving, and it resolves to the one address the\n * console generates.\n *\n * The CAMPAIGN that groups messages is a marketing object — a window of\n * dates, a set of lists, a topic and revenue attribution, which happens to\n * reach people by email — so it is a section of the Marketing console and a\n * message's page links out to it.\n */\n { id: 'messages', label: 'Messages' },\n /*\n * The reusable besigner documents a message is built from.\n *\n * Id and label say the same word on purpose: a template is the vocabulary\n * everywhere else in this surface — an email names the template it renders,\n * and the marketplace publishes them — so a URL saying anything else would\n * be the one place the reader has to translate.\n */\n { id: 'templates', label: 'Templates' },\n { id: 'audiences', label: 'Audiences' },\n // Between the audiences and the suppressions, which is where a topic sits\n // conceptually: an audience is who you may reach, a suppression is who you\n // may not, and a topic is the stream a recipient can leave without becoming\n // either.\n { id: 'topics', label: 'Topics' },\n /*\n * WHO THE MAIL COMES FROM, as against who it goes to.\n *\n * After the three audience sections and before Suppressions, which is where\n * the question sits: everything above decides who is reached, and this one\n * decides what they see in the `From:` line. It is also the section a\n * merchant is sent to from the composer when a send is refused for an\n * unverified identity, so it has to be a route of its own.\n */\n { id: 'sending', label: 'Sending' },\n // Beside the audiences rather than inside them (AGL-2410): a suppression is\n // not a list you build, it is the reason a list you built did not all get\n // mailed.\n { id: 'suppressions', label: 'Suppressions' },\n]\n\n/*\n * Rail ORDER decides where `/emails` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["EMAILS_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAYD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMA,0BAAwD;IACnE;;;;;;;;;;;;;;;;;;;;;;GAsBC,GACD;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;GAOC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC,0EAA0E;IAC1E,2EAA2E;IAC3E,4EAA4E;IAC5E,UAAU;IACV;QAAED,IAAI;QAAUC,OAAO;IAAS;IAChC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAWC,OAAO;IAAU;IAClC,4EAA4E;IAC5E,0EAA0E;IAC1E,UAAU;IACV;QAAED,IAAI;QAAgBC,OAAO;IAAe;CAC7C,CAED;;;;;CAKC,IAPA"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-sections.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\nimport type { ConsoleNavSection } from '@aglyn/aglyn'\n\nexport type EmailsConsoleSectionId =\n | 'messages'\n | 'templates'\n | 'audiences'\n | 'topics'\n | 'sending'\n | 'consent-groups'\n | 'suppressions'\n\n/**\n * The emails console's sections, in rail order (AGL-2501).\n *\n * One list, read twice and never copied: `plugin.ts` registers it on the nav\n * item so the shell can route and gate each section, and the page switches its\n * body on the id the shell resolves back.\n *\n * Ids appear in links people keep — treat them as persisted, and prefer\n * renaming a LABEL over an id. Where the two have to move together, the old\n * path is kept alive by a redirect rather than by an id that no longer says\n * what the section is. `audiences` and `suppressions` are also the `?tab=`\n * ids this page deep-linked by before its sections became routes, so a\n * bookmark that named one of those tabs still names the same section.\n *\n * No `navTabId` on any of them: every section ships with the surface, so they\n * inherit the Emails nav item's gate.\n */\nexport const EMAILS_CONSOLE_SECTIONS: readonly ConsoleNavSection[] = [\n /*\n * The individual messages: one row per email that was or will be sent, each\n * with its own report.\n *\n * A message is what this surface is about, and this section is named for\n * the record rather than for the surface holding it. The shell prints the\n * surface and the section together — in the header, in the breadcrumb and\n * in the browser tab — so a section whose name repeats its surface's says\n * nothing about which section is open, which is the one job a section\n * name has. `messages` is also the word the rest of this surface already\n * uses for the record: a message's own page, a template's sends table and\n * the campaign detail all call it that.\n *\n * `/emails/emails/{id}` is answered by a permanent redirect in the console's\n * `next.config.js`, on the terms the Workflows → Automation move set: a URL\n * somebody pasted keeps resolving, and it resolves to the one address the\n * console generates.\n *\n * The CAMPAIGN that groups messages is a marketing object — a window of\n * dates, a set of lists, a topic and revenue attribution, which happens to\n * reach people by email — so it is a section of the Marketing console and a\n * message's page links out to it.\n */\n { id: 'messages', label: 'Messages' },\n /*\n * The reusable besigner documents a message is built from.\n *\n * Id and label say the same word on purpose: a template is the vocabulary\n * everywhere else in this surface — an email names the template it renders,\n * and the marketplace publishes them — so a URL saying anything else would\n * be the one place the reader has to translate.\n */\n { id: 'templates', label: 'Templates' },\n { id: 'audiences', label: 'Audiences' },\n // Between the audiences and the suppressions, which is where a topic sits\n // conceptually: an audience is who you may reach, a suppression is who you\n // may not, and a topic is the stream a recipient can leave without becoming\n // either.\n { id: 'topics', label: 'Topics' },\n /*\n * WHO THE MAIL COMES FROM, as against who it goes to.\n *\n * After the three audience sections and before Suppressions, which is where\n * the question sits: everything above decides who is reached, and this one\n * decides what they see in the `From:` line. It is also the section a\n * merchant is sent to from the composer when a send is refused for an\n * unverified identity, so it has to be a route of its own.\n */\n { id: 'sending', label: 'Sending' },\n /*\n * WHICH SITES ARE ONE SENDER (AGL-3320).\n *\n * After Sending, because it answers the question beside it: Sending says\n * what the `From:` line shows, this says which sites a signup, an\n * unsubscribe and a CRM record are shared across. Before Suppressions,\n * whose lists it decides the reach of — an address on one site's list is\n * skipped by every site in that site's group. The organization declares\n * the groups here; a site's own page says which one it is in.\n */\n { id: 'consent-groups', label: 'Consent groups' },\n // Beside the audiences rather than inside them (AGL-2410): a suppression is\n // not a list you build, it is the reason a list you built did not all get\n // mailed.\n { id: 'suppressions', label: 'Suppressions' },\n]\n\n/*\n * Rail ORDER decides where `/emails` lands: the shell redirects a bare hub\n * URL to the first section in this list the reader may open (AGL-2501). There\n * is deliberately no separate default constant — a second place to say which\n * section is first is a second place for it to disagree with the rail.\n */\n"],"names":["EMAILS_CONSOLE_SECTIONS","id","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAaD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMA,0BAAwD;IACnE;;;;;;;;;;;;;;;;;;;;;;GAsBC,GACD;QAAEC,IAAI;QAAYC,OAAO;IAAW;IACpC;;;;;;;GAOC,GACD;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC;QAAED,IAAI;QAAaC,OAAO;IAAY;IACtC,0EAA0E;IAC1E,2EAA2E;IAC3E,4EAA4E;IAC5E,UAAU;IACV;QAAED,IAAI;QAAUC,OAAO;IAAS;IAChC;;;;;;;;GAQC,GACD;QAAED,IAAI;QAAWC,OAAO;IAAU;IAClC;;;;;;;;;GASC,GACD;QAAED,IAAI;QAAkBC,OAAO;IAAiB;IAChD,4EAA4E;IAC5E,0EAA0E;IAC1E,UAAU;IACV;QAAED,IAAI;QAAgBC,OAAO;IAAe;CAC7C,CAED;;;;;CAKC,IAPA"}
@@ -392,7 +392,7 @@ function OrgEmailTemplatesTable(props) {
392
392
  return /*#__PURE__*/ _jsxs(CardDisplay, {
393
393
  header: 'Templates',
394
394
  help: pluginDocsHelp('designedEmails', {
395
- anchor: '#create-a-template'
395
+ anchor: '#find-a-template'
396
396
  }),
397
397
  contentGutterX: true,
398
398
  contentGutterY: true,