@aglyn/plugins-email 1.0.0-beta.179 → 1.0.0-beta.181

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 (65) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/components/email-org-mount.d.ts +116 -0
  4. package/src/lib/components/email-org-mount.js +211 -0
  5. package/src/lib/components/email-org-mount.js.map +1 -0
  6. package/src/lib/components/email-topic-detail.d.ts +2 -1
  7. package/src/lib/components/email-topic-detail.js +7 -1
  8. package/src/lib/components/email-topic-detail.js.map +1 -1
  9. package/src/lib/components/email-topics-card.d.ts +2 -1
  10. package/src/lib/components/email-topics-card.js +7 -1
  11. package/src/lib/components/email-topics-card.js.map +1 -1
  12. package/src/lib/components/email-zones.d.ts +28 -1
  13. package/src/lib/components/email-zones.js.map +1 -1
  14. package/src/lib/components/emails-console-page.d.ts +4 -0
  15. package/src/lib/components/emails-console-page.js +107 -5
  16. package/src/lib/components/emails-console-page.js.map +1 -1
  17. package/src/lib/components/list-detail-card.d.ts +19 -3
  18. package/src/lib/components/list-detail-card.js +52 -13
  19. package/src/lib/components/list-detail-card.js.map +1 -1
  20. package/src/lib/components/list-edit-card.d.ts +2 -1
  21. package/src/lib/components/list-edit-card.js +56 -17
  22. package/src/lib/components/list-edit-card.js.map +1 -1
  23. package/src/lib/components/lists-card.d.ts +2 -1
  24. package/src/lib/components/lists-card.js +5 -5
  25. package/src/lib/components/lists-card.js.map +1 -1
  26. package/src/lib/components/org-email-templates-card.d.ts +34 -0
  27. package/src/lib/components/org-email-templates-card.js +470 -0
  28. package/src/lib/components/org-email-templates-card.js.map +1 -0
  29. package/src/lib/components/org-sending-card.d.ts +30 -0
  30. package/src/lib/components/org-sending-card.js +680 -0
  31. package/src/lib/components/org-sending-card.js.map +1 -0
  32. package/src/lib/components/org-sending-domain-detail.d.ts +28 -0
  33. package/src/lib/components/org-sending-domain-detail.js +497 -0
  34. package/src/lib/components/org-sending-domain-detail.js.map +1 -0
  35. package/src/lib/components/org-site-suppressions.d.ts +20 -0
  36. package/src/lib/components/org-site-suppressions.js +99 -0
  37. package/src/lib/components/org-site-suppressions.js.map +1 -0
  38. package/src/lib/components/org-suppressions-card.d.ts +22 -0
  39. package/src/lib/components/org-suppressions-card.js +196 -0
  40. package/src/lib/components/org-suppressions-card.js.map +1 -0
  41. package/src/lib/components/sending-domain-detail.d.ts +12 -0
  42. package/src/lib/components/sending-domain-detail.js +4 -1
  43. package/src/lib/components/sending-domain-detail.js.map +1 -1
  44. package/src/lib/components/suppression-totals.d.ts +63 -0
  45. package/src/lib/components/suppression-totals.js +105 -0
  46. package/src/lib/components/suppression-totals.js.map +1 -0
  47. package/src/lib/components/suppressions-card.js +7 -80
  48. package/src/lib/components/suppressions-card.js.map +1 -1
  49. package/src/lib/components/use-org-email-topics.d.ts +9 -1
  50. package/src/lib/components/use-org-email-topics.js +4 -3
  51. package/src/lib/components/use-org-email-topics.js.map +1 -1
  52. package/src/lib/components/use-org-sending-views.d.ts +39 -0
  53. package/src/lib/components/use-org-sending-views.js +124 -0
  54. package/src/lib/components/use-org-sending-views.js.map +1 -0
  55. package/src/lib/declarations.console-server.d.ts +31 -0
  56. package/src/lib/declarations.console-server.js +43 -0
  57. package/src/lib/declarations.console-server.js.map +1 -0
  58. package/src/lib/model/sending-domain-status.d.ts +36 -0
  59. package/src/lib/model/sending-domain-status.js +46 -0
  60. package/src/lib/model/sending-domain-status.js.map +1 -1
  61. package/src/lib/plugin.js +29 -1
  62. package/src/lib/plugin.js.map +1 -1
  63. package/src/lib/server.d.ts +27 -0
  64. package/src/lib/server.js +130 -5
  65. package/src/lib/server.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-sending-domain-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 { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiEmailCheckOutline } from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useSendingApi,\n type SendingDomainView,\n} from '@aglyn/tenant-feature-instance/hooks/use-sending-identity-api'\nimport { Alert, Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n describeOrgSendingDomainRemoval,\n describeSendingDomain,\n INCONCLUSIVE_CHECK,\n} from '../model/sending-domain-status'\nimport {\n OrgSiteSelect,\n orgSiteName,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\nimport { RecordRow } from './sending-domain-detail'\nimport { useOrgSendingViews } from './use-org-sending-views'\n\nexport interface OrgSendingDomainDetailProps {\n domain: string\n /** The organization's Emails page, for the way back to Sending. */\n basePath: string\n}\n\n/**\n * ONE SENDING DOMAIN, from the organization's page —\n * `/[orgSlug]/emails/sending/{domain}`.\n *\n * The domain is the organization's, so its page reads the same as it does\n * from a site's: what to publish, whether a lookup can see it, and the checks\n * that move it on. What changes is the half that is a site's choice. Under a\n * site this page asks whether THIS site sends as the domain; over the org it\n * lists the sites that do, and offers to move another site onto it.\n *\n * The records come from the domains route, which is owner-or-admin. An editor\n * still sees the domain's state and the sites sending as it — both come back\n * on each site's own identity read, which an editor may make — and is told\n * why the records are not shown rather than handed an error.\n *\n * `inconclusive` is held apart from the record exactly as on the site's page:\n * a lookup nobody answered changed nothing.\n */\nexport function OrgSendingDomainDetail(props: OrgSendingDomainDetailProps) {\n const mount = useEmailOrgMount()\n return mount ? <OrgSendingDomainPage mount={mount} {...props} /> : null\n}\nOrgSendingDomainDetail.displayName = 'OrgSendingDomainDetail'\n\nfunction OrgSendingDomainPage(\n props: OrgSendingDomainDetailProps & { mount: EmailOrgMount },\n) {\n const { mount, domain, basePath } = props\n const call = useSendingApi()\n const router = useRouter()\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // Held in a ref: `load` depends on it and the hook hands a fresh function\n // back per render, which would re-run the fetch on every render.\n const notifyRef = useRef(enqueueSnackbar)\n notifyRef.current = enqueueSnackbar\n\n // The first page of sites by name: enough to say which of them send as\n // this domain, and bounded however many sites the organization runs.\n const { sites } = useOrgSitePage(mount)\n const {\n views,\n reload,\n first,\n loading: viewsLoading,\n } = useOrgSendingViews(sites)\n const canManage = first?.canManage === true\n\n const [record, setRecord] = useState<SendingDomainView | null>(null)\n const [refusal, setRefusal] = useState('')\n const [loading, setLoading] = useState(true)\n const [busy, setBusy] = useState(false)\n const [unreachable, setUnreachable] = useState(false)\n const [assignHostId, setAssignHostId] = useState('')\n\n const load = useCallback(async () => {\n setLoading(true)\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'GET',\n query: { orgId: mount.orgId },\n })\n setLoading(false)\n if (!response.ok) {\n setRecord(null)\n setRefusal(payload?.error ?? 'This domain’s records could not be read.')\n return\n }\n setRefusal('')\n setRecord(\n (payload?.domains ?? []).find(\n (one: SendingDomainView) => one.domain === domain,\n ) ?? null,\n )\n }, [call, mount.orgId, domain])\n\n useEffect(() => {\n void load()\n }, [load])\n\n /** The domain as the list knows it, for a reader the records route refused. */\n const listed = first?.domains.find((one) => one.domain === domain) ?? null\n const status = record?.status ?? listed?.status ?? null\n const using = mount.hosts.filter(\n (site) => views[site.id]?.selected === domain,\n )\n /** Sites whose identity this page has not read, so cannot speak for. */\n const unchecked = mount.hosts.filter((site) => !views[site.id]).length\n const offered = mount.hosts.filter(\n (site) => views[site.id]?.selected !== domain,\n )\n\n const handleVerify = useCallback(async () => {\n if (busy) return\n setBusy(true)\n setUnreachable(false)\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'POST',\n body: { orgId: mount.orgId, domain, action: 'verify' },\n })\n setBusy(false)\n if (response.status === 503) {\n setUnreachable(true)\n return\n }\n if (!response.ok) {\n return void notifyRef.current(\n payload?.error ?? 'The check could not run',\n {\n variant: 'warning',\n },\n )\n }\n await Promise.all([load(), reload()])\n notifyRef.current(\n payload?.verified\n ? 'Verified — this domain can send'\n : 'Checked. Some records are still missing.',\n { variant: payload?.verified ? 'success' : 'info' },\n )\n }, [busy, call, domain, load, mount.orgId, reload])\n\n const handleRequestRecords = useCallback(async () => {\n if (busy) return\n setBusy(true)\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'POST',\n body: { orgId: mount.orgId, domain, action: 'request' },\n })\n setBusy(false)\n if (!response.ok) {\n return void notifyRef.current(\n payload?.error ?? 'Could not request records',\n { variant: 'warning' },\n )\n }\n await load()\n }, [busy, call, domain, load, mount.orgId])\n\n /** Moves one site onto this domain, or off it with `'platform'`. */\n const handleUse = useCallback(\n async (hostId: string, next: string) => {\n if (busy || !hostId) return\n setBusy(true)\n const { response, payload } = await call({\n path: 'sending-identity',\n method: 'POST',\n body: { hostId, domain: next },\n })\n setBusy(false)\n if (!response.ok) {\n return void notifyRef.current(\n payload?.error ?? 'Could not change the identity',\n { variant: 'warning' },\n )\n }\n setAssignHostId('')\n await reload(hostId)\n // The address the ROUTE settled on — see the site's page.\n const site = orgSiteName(mount, hostId)\n notifyRef.current(\n payload?.from\n ? `${site} now sends as ${payload.from}`\n : `${site}’s sending address was changed`,\n { variant: 'success' },\n )\n },\n [busy, call, mount, reload],\n )\n\n const handleRelease = useCallback(async () => {\n const ok = await confirm({\n ...describeOrgSendingDomainRemoval({\n domain,\n sites: using.map((site) => ({\n name: orgSiteName(mount, site.id),\n issued: views[site.id]?.platformDomain === domain,\n })),\n unchecked,\n }),\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!ok) return\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'DELETE',\n query: { orgId: mount.orgId, domain },\n })\n if (!response.ok) {\n return void notifyRef.current(\n payload?.error ?? 'Could not remove the domain',\n { variant: 'warning' },\n )\n }\n router.push(`${basePath}/sending`)\n }, [basePath, call, confirm, domain, mount, router, unchecked, using, views])\n\n const state = status\n ? describeSendingDomain({\n status,\n pendingProvider: record?.pendingProvider ?? status === 'requested',\n issueError: record?.lastIssueError ?? listed?.lastIssueError ?? null,\n missing: record?.lastMissing ?? null,\n })\n : null\n const missing = new Set(record?.lastMissing ?? [])\n const actions: RowActionsMenuItem[] =\n canManage && record\n ? [\n {\n key: 'release',\n label: 'Remove domain',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleRelease(),\n },\n ]\n : []\n\n const card = (\n <CardDisplay\n header={'Sending domain'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#sending-domains' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\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={`${basePath}/sending`}\n size=\"small\"\n color=\"primary\"\n >\n {'All of Sending'}\n </Button>\n {actions.length ? <RowActionsMenu items={actions} /> : null}\n </Stack>\n ),\n }}\n >\n {(loading || viewsLoading) && !state ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : !state ? (\n <Alert severity=\"warning\">\n {refusal ||\n `${domain} is not claimed by this organization. It may have been ` +\n 'removed — go back and add it again if you still want it.'}\n </Alert>\n ) : (\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Chip\n label={state.label}\n color={state.color}\n variant={state.sending ? 'filled' : 'outlined'}\n />\n </Stack>\n\n <Alert severity={state.severity}>{state.text}</Alert>\n\n {unreachable ? (\n <Alert severity={INCONCLUSIVE_CHECK.severity} icon={false}>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {INCONCLUSIVE_CHECK.label}\n </Typography>\n <Typography variant=\"body2\">{INCONCLUSIVE_CHECK.text}</Typography>\n </Alert>\n ) : null}\n\n {/*\n THE SITES SENDING AS IT, of those this page read. Each is the\n site's own choice, and moving one off it is the org twin of the\n \"stop sending\" control on a site's page.\n */}\n <Divider />\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {'Sites sending as this domain'}\n </Typography>\n {using.length ? (\n <Stack spacing={1}>\n {using.map((site) => (\n <Stack\n key={site.id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\">\n {orgSiteName(mount, site.id)}\n </Typography>\n {canManage ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => void handleUse(site.id, 'platform')}\n >\n {'Stop sending as this domain'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unchecked\n ? 'None of the sites checked sends as it.'\n : 'No site sends as it.'}\n </Typography>\n )}\n {unchecked && !viewsLoading ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${unchecked} more ${unchecked === 1 ? 'site was' : 'sites were'} ` +\n 'not checked; the Sending page reads them a page at a time.'}\n </Typography>\n ) : null}\n {canManage && status === 'verified' && offered.length ? (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <OrgSiteSelect\n mount={mount}\n label=\"Send a site’s email as this domain\"\n value={assignHostId}\n onChange={setAssignHostId}\n sites={offered}\n />\n <Button\n variant=\"outlined\"\n startIcon={\n <MdiIcon path={mdiEmailCheckOutline.path} size={0.8} />\n }\n disabled={busy || !assignHostId}\n onClick={() => void handleUse(assignHostId, domain)}\n >\n {'Send as this domain'}\n </Button>\n </Stack>\n ) : null}\n\n {/*\n THE RECORDS, which only the domains route returns. A reader it\n refused is told why, in place of the records, rather than shown an\n empty section that reads as \"nothing to publish\".\n */}\n <Divider />\n {refusal ? (\n <Alert severity=\"info\">\n {canManage\n ? refusal\n : 'The records to publish, and the checks that verify them, ' +\n 'are shown to the organization’s owners and admins.'}\n </Alert>\n ) : record ? (\n <>\n {record.dmarc ? (\n <Alert\n severity={\n record.dmarc.policy === 'reject' &&\n record.status !== 'verified'\n ? 'error'\n : 'info'\n }\n >\n {record.dmarc.consequence}\n </Alert>\n ) : null}\n {record.records?.some((entry) => entry.value) ? (\n <>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {'Publish these records'}\n </Typography>\n <Stack spacing={2}>\n {record.records\n .filter((entry) => entry.value)\n .map((entry) => (\n <RecordRow\n key={`${entry.type}:${entry.name}`}\n record={entry}\n missing={missing.has(`${entry.type}:${entry.name}`)}\n />\n ))}\n {record.dmarcSuggestion &&\n record.dmarc?.policy === 'absent' ? (\n <RecordRow\n record={record.dmarcSuggestion}\n missing={false}\n />\n ) : null}\n </Stack>\n </>\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {record.status === 'requested' && canManage ? (\n <Button\n variant=\"contained\"\n disabled={busy}\n onClick={() => void handleRequestRecords()}\n >\n {busy ? 'Working…' : 'Request records'}\n </Button>\n ) : null}\n {record.status !== 'requested' ? (\n <Button\n variant=\"contained\"\n disabled={busy}\n onClick={() => void handleVerify()}\n >\n {busy ? 'Checking…' : 'Check DNS'}\n </Button>\n ) : null}\n </Stack>\n </>\n ) : null}\n </Stack>\n )}\n </CardDisplay>\n )\n\n return (\n <>\n <PageHeaderRecord title={domain} />\n {card}\n </>\n )\n}\nOrgSendingDomainPage.displayName = 'OrgSendingDomainPage'\n\nexport default OrgSendingDomainDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","mdiDeleteOutline","mdiEmailCheckOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","useSnackbar","useSendingApi","Alert","Button","Chip","Divider","Stack","Typography","useRouter","useCallback","useEffect","useRef","useState","describeOrgSendingDomainRemoval","describeSendingDomain","INCONCLUSIVE_CHECK","OrgSiteSelect","orgSiteName","useEmailOrgMount","useOrgSitePage","RecordRow","useOrgSendingViews","OrgSendingDomainDetail","props","mount","OrgSendingDomainPage","displayName","record","domain","basePath","call","router","confirm","enqueueSnackbar","notifyRef","current","sites","views","reload","first","loading","viewsLoading","canManage","setRecord","refusal","setRefusal","setLoading","busy","setBusy","unreachable","setUnreachable","assignHostId","setAssignHostId","load","response","payload","path","method","query","orgId","ok","error","domains","find","one","listed","status","using","hosts","filter","site","id","selected","unchecked","length","offered","handleVerify","body","action","variant","Promise","all","verified","handleRequestRecords","handleUse","hostId","next","from","handleRelease","map","name","issued","platformDomain","confirmationButtonProps","color","then","catch","push","state","pendingProvider","issueError","lastIssueError","missing","lastMissing","Set","actions","key","label","icon","size","destructive","onClick","card","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","href","items","severity","useFlexGap","flexWrap","sending","text","fontWeight","disabled","value","onChange","startIcon","dmarc","policy","consequence","records","some","entry","has","type","dmarcSuggestion","gap","title"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,EAAEC,cAAc,QAAQ,eAAc;AAC/D,SAASC,gBAAgB,EAAEC,oBAAoB,QAAQ,yBAAwB;AAC/E,SACEC,OAAO,EACPC,WAAW,EACXC,OAAO,EACPC,sBAAsB,QACjB,uBAAsB;AAC7B,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,aAAa,QAER,gEAA+D;AACtE,SAASC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC/E,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,SACEC,+BAA+B,EAC/BC,qBAAqB,EACrBC,kBAAkB,QACb,oCAAgC;AACvC,SACEC,aAAa,EACbC,WAAW,EACXC,gBAAgB,EAChBC,cAAc,QAET,uBAAmB;AAC1B,SAASC,SAAS,QAAQ,6BAAyB;AACnD,SAASC,kBAAkB,QAAQ,6BAAyB;AAQ5D;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,uBAAuBC,KAAkC;IACvE,MAAMC,QAAQN;IACd,OAAOM,sBAAQ,KAACC;QAAqBD,OAAOA;OAAWD,UAAY;AACrE;AACAD,uBAAuBI,WAAW,GAAG;AAErC,SAASD,qBACPF,KAA6D;cAyD9CI,qBA4HGA;QAkLLA,iBAgBKA;IArXlB,MAAM,EAAEH,KAAK,EAAEI,MAAM,EAAEC,QAAQ,EAAE,GAAGN;IACpC,MAAMO,OAAO7B;IACb,MAAM8B,SAASvB;IACf,MAAM,EAAEwB,OAAO,EAAE,GAAGlC;IACpB,MAAM,EAAEmC,eAAe,EAAE,GAAGjC;IAC5B,0EAA0E;IAC1E,iEAAiE;IACjE,MAAMkC,YAAYvB,OAAOsB;IACzBC,UAAUC,OAAO,GAAGF;IAEpB,uEAAuE;IACvE,qEAAqE;IACrE,MAAM,EAAEG,KAAK,EAAE,GAAGjB,eAAeK;IACjC,MAAM,EACJa,KAAK,EACLC,MAAM,EACNC,KAAK,EACLC,SAASC,YAAY,EACtB,GAAGpB,mBAAmBe;IACvB,MAAMM,YAAYH,CAAAA,yBAAAA,MAAOG,SAAS,MAAK;IAEvC,MAAM,CAACf,QAAQgB,UAAU,GAAG/B,SAAmC;IAC/D,MAAM,CAACgC,SAASC,WAAW,GAAGjC,SAAS;IACvC,MAAM,CAAC4B,SAASM,WAAW,GAAGlC,SAAS;IACvC,MAAM,CAACmC,MAAMC,QAAQ,GAAGpC,SAAS;IACjC,MAAM,CAACqC,aAAaC,eAAe,GAAGtC,SAAS;IAC/C,MAAM,CAACuC,cAAcC,gBAAgB,GAAGxC,SAAS;IAEjD,MAAMyC,OAAO5C,YAAY;YAerB;QAdFqC,WAAW;QACX,MAAM,EAAEQ,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMzB,KAAK;YACvC0B,MAAM;YACNC,QAAQ;YACRC,OAAO;gBAAEC,OAAOnC,MAAMmC,KAAK;YAAC;QAC9B;QACAb,WAAW;QACX,IAAI,CAACQ,SAASM,EAAE,EAAE;;YAChBjB,UAAU;YACVE,oBAAWU,2BAAAA,QAASM,KAAK,oBAAI;YAC7B;QACF;QACAhB,WAAW;QACXF,WACE,QAAA,SAACY,2BAAAA,QAASO,OAAO,mBAAI,EAAE,EAAEC,IAAI,CAC3B,CAACC,MAA2BA,IAAIpC,MAAM,KAAKA,mBAD7C,QAEK;IAET,GAAG;QAACE;QAAMN,MAAMmC,KAAK;QAAE/B;KAAO;IAE9BlB,UAAU;QACR,KAAK2C;IACP,GAAG;QAACA;KAAK;IAET,6EAA6E,GAC7E,MAAMY,iBAAS1B,yBAAAA,MAAOuB,OAAO,CAACC,IAAI,CAAC,CAACC,MAAQA,IAAIpC,MAAM,KAAKA,0BAAW;IACtE,MAAMsC,UAASvC,iBAAAA,0BAAAA,OAAQuC,MAAM,oBAAID,0BAAAA,OAAQC,MAAM,YAAhCvC,QAAoC;IACnD,MAAMwC,QAAQ3C,MAAM4C,KAAK,CAACC,MAAM,CAC9B,CAACC;YAASjC;eAAAA,EAAAA,iBAAAA,KAAK,CAACiC,KAAKC,EAAE,CAAC,qBAAdlC,eAAgBmC,QAAQ,MAAK5C;;IAEzC,sEAAsE,GACtE,MAAM6C,YAAYjD,MAAM4C,KAAK,CAACC,MAAM,CAAC,CAACC,OAAS,CAACjC,KAAK,CAACiC,KAAKC,EAAE,CAAC,EAAEG,MAAM;IACtE,MAAMC,UAAUnD,MAAM4C,KAAK,CAACC,MAAM,CAChC,CAACC;YAASjC;eAAAA,EAAAA,iBAAAA,KAAK,CAACiC,KAAKC,EAAE,CAAC,qBAAdlC,eAAgBmC,QAAQ,MAAK5C;;IAGzC,MAAMgD,eAAenE,YAAY;QAC/B,IAAIsC,MAAM;QACVC,QAAQ;QACRE,eAAe;QACf,MAAM,EAAEI,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMzB,KAAK;YACvC0B,MAAM;YACNC,QAAQ;YACRoB,MAAM;gBAAElB,OAAOnC,MAAMmC,KAAK;gBAAE/B;gBAAQkD,QAAQ;YAAS;QACvD;QACA9B,QAAQ;QACR,IAAIM,SAASY,MAAM,KAAK,KAAK;YAC3BhB,eAAe;YACf;QACF;QACA,IAAI,CAACI,SAASM,EAAE,EAAE;;YAChB,OAAO,KAAK1B,UAAUC,OAAO,SAC3BoB,2BAAAA,QAASM,KAAK,mBAAI,2BAClB;gBACEkB,SAAS;YACX;QAEJ;QACA,MAAMC,QAAQC,GAAG,CAAC;YAAC5B;YAAQf;SAAS;QACpCJ,UAAUC,OAAO,CACfoB,CAAAA,2BAAAA,QAAS2B,QAAQ,IACb,oCACA,4CACJ;YAAEH,SAASxB,CAAAA,2BAAAA,QAAS2B,QAAQ,IAAG,YAAY;QAAO;IAEtD,GAAG;QAACnC;QAAMjB;QAAMF;QAAQyB;QAAM7B,MAAMmC,KAAK;QAAErB;KAAO;IAElD,MAAM6C,uBAAuB1E,YAAY;QACvC,IAAIsC,MAAM;QACVC,QAAQ;QACR,MAAM,EAAEM,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMzB,KAAK;YACvC0B,MAAM;YACNC,QAAQ;YACRoB,MAAM;gBAAElB,OAAOnC,MAAMmC,KAAK;gBAAE/B;gBAAQkD,QAAQ;YAAU;QACxD;QACA9B,QAAQ;QACR,IAAI,CAACM,SAASM,EAAE,EAAE;;YAChB,OAAO,KAAK1B,UAAUC,OAAO,SAC3BoB,2BAAAA,QAASM,KAAK,mBAAI,6BAClB;gBAAEkB,SAAS;YAAU;QAEzB;QACA,MAAM1B;IACR,GAAG;QAACN;QAAMjB;QAAMF;QAAQyB;QAAM7B,MAAMmC,KAAK;KAAC;IAE1C,kEAAkE,GAClE,MAAMyB,YAAY3E,YAChB,OAAO4E,QAAgBC;QACrB,IAAIvC,QAAQ,CAACsC,QAAQ;QACrBrC,QAAQ;QACR,MAAM,EAAEM,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMzB,KAAK;YACvC0B,MAAM;YACNC,QAAQ;YACRoB,MAAM;gBAAEQ;gBAAQzD,QAAQ0D;YAAK;QAC/B;QACAtC,QAAQ;QACR,IAAI,CAACM,SAASM,EAAE,EAAE;;YAChB,OAAO,KAAK1B,UAAUC,OAAO,SAC3BoB,2BAAAA,QAASM,KAAK,mBAAI,iCAClB;gBAAEkB,SAAS;YAAU;QAEzB;QACA3B,gBAAgB;QAChB,MAAMd,OAAO+C;QACb,0DAA0D;QAC1D,MAAMf,OAAOrD,YAAYO,OAAO6D;QAChCnD,UAAUC,OAAO,CACfoB,CAAAA,2BAAAA,QAASgC,IAAI,IACT,GAAGjB,KAAK,cAAc,EAAEf,QAAQgC,IAAI,EAAE,GACtC,GAAGjB,KAAK,8BAA8B,CAAC,EAC3C;YAAES,SAAS;QAAU;IAEzB,GACA;QAAChC;QAAMjB;QAAMN;QAAOc;KAAO;IAG7B,MAAMkD,gBAAgB/E,YAAY;QAChC,MAAMmD,KAAK,MAAM5B,QAAQ,aACpBnB,gCAAgC;YACjCe;YACAQ,OAAO+B,MAAMsB,GAAG,CAAC,CAACnB;oBAERjC;uBAFkB;oBAC1BqD,MAAMzE,YAAYO,OAAO8C,KAAKC,EAAE;oBAChCoB,QAAQtD,EAAAA,iBAAAA,KAAK,CAACiC,KAAKC,EAAE,CAAC,qBAAdlC,eAAgBuD,cAAc,MAAKhE;gBAC7C;;YACA6C;QACF;YACAoB,yBAAyB;gBAAEC,OAAO;YAAQ;WAE1C,0DAA0D;SACzDC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACpC,IAAI;QACT,MAAM,EAAEN,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMzB,KAAK;YACvC0B,MAAM;YACNC,QAAQ;YACRC,OAAO;gBAAEC,OAAOnC,MAAMmC,KAAK;gBAAE/B;YAAO;QACtC;QACA,IAAI,CAAC0B,SAASM,EAAE,EAAE;;YAChB,OAAO,KAAK1B,UAAUC,OAAO,SAC3BoB,2BAAAA,QAASM,KAAK,mBAAI,+BAClB;gBAAEkB,SAAS;YAAU;QAEzB;QACAhD,OAAOkE,IAAI,CAAC,GAAGpE,SAAS,QAAQ,CAAC;IACnC,GAAG;QAACA;QAAUC;QAAME;QAASJ;QAAQJ;QAAOO;QAAQ0C;QAAWN;QAAO9B;KAAM;IAE5E,MAAM6D,QAAQhC,SACVpD,sBAAsB;QACpBoD;QACAiC,eAAe,WAAExE,0BAAAA,OAAQwE,eAAe,oBAAIjC,WAAW;QACvDkC,UAAU,GAAEzE,iBAAAA,0BAAAA,OAAQ0E,cAAc,oBAAIpC,0BAAAA,OAAQoC,cAAc,YAAhD1E,QAAoD;QAChE2E,OAAO,WAAE3E,0BAAAA,OAAQ4E,WAAW,oBAAI;IAClC,KACA;IACJ,MAAMD,UAAU,IAAIE,aAAI7E,0BAAAA,OAAQ4E,WAAW,oBAAI,EAAE;IACjD,MAAME,UACJ/D,aAAaf,SACT;QACE;YACE+E,KAAK;YACLC,OAAO;YACPC,oBAAM,KAAC/G;gBAAQ2D,MAAM/D,iBAAiB+D,IAAI;gBAAEqD,MAAM;;YAClDC,aAAa;YACbC,SAAS,IAAM,KAAKvB;QACtB;KACD,GACD,EAAE;IAER,MAAMwB,qBACJ,KAACpH;QACCqH,QAAQ;QACRC,MAAM1H,eAAe,kBAAkB;YAAE2H,QAAQ;QAAmB;QACpEC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXzC,sBACE,MAACxE;gBAAMkH,WAAU;gBAAMC,SAAS;gBAAGC,IAAI;oBAAEC,YAAY;gBAAS;;kCAC5D,KAACxH;wBACCyH,WAAWjI;uBACN;wBAAEkI,kBAAkB;wBAASC,cAAc;oBAAM;wBACtDC,MAAM,GAAGlG,SAAS,QAAQ,CAAC;wBAC3BgF,MAAK;wBACLf,OAAM;kCAEL;;oBAEFW,QAAQ/B,MAAM,iBAAG,KAAC3E;wBAAeiI,OAAOvB;yBAAc;;;QAG7D;kBAEC,AAACjE,CAAAA,WAAWC,YAAW,KAAM,CAACyD,sBAC7B,KAAC3F;YAAWwE,SAAQ;YAAQe,OAAM;sBAC/B;aAED,CAACI,sBACH,KAAChG;YAAM+H,UAAS;sBACbrF,WACC,GAAGhB,OAAO,uDAAuD,CAAC,GAChE;2BAGN,MAACtB;YAAMmH,SAAS;;8BACd,KAACnH;oBACCkH,WAAU;oBACVC,SAAS;oBACTS,UAAU;oBACVR,IAAI;wBAAEC,YAAY;wBAAUQ,UAAU;oBAAO;8BAE7C,cAAA,KAAC/H;wBACCuG,OAAOT,MAAMS,KAAK;wBAClBb,OAAOI,MAAMJ,KAAK;wBAClBf,SAASmB,MAAMkC,OAAO,GAAG,WAAW;;;8BAIxC,KAAClI;oBAAM+H,UAAU/B,MAAM+B,QAAQ;8BAAG/B,MAAMmC,IAAI;;gBAE3CpF,4BACC,MAAC/C;oBAAM+H,UAAUlH,mBAAmBkH,QAAQ;oBAAErB,MAAM;;sCAClD,KAACrG;4BAAWwE,SAAQ;4BAAQ2C,IAAI;gCAAEY,YAAY;4BAAO;sCAClDvH,mBAAmB4F,KAAK;;sCAE3B,KAACpG;4BAAWwE,SAAQ;sCAAShE,mBAAmBsH,IAAI;;;qBAEpD;8BAOJ,KAAChI;8BACD,KAACE;oBAAWwE,SAAQ;oBAAQ2C,IAAI;wBAAEY,YAAY;oBAAO;8BAClD;;gBAEFnE,MAAMO,MAAM,iBACX,KAACpE;oBAAMmH,SAAS;8BACbtD,MAAMsB,GAAG,CAAC,CAACnB,qBACV,MAAChE;4BAECkH,WAAU;4BACVC,SAAS;4BACTC,IAAI;gCAAEC,YAAY;4BAAS;;8CAE3B,KAACpH;oCAAWwE,SAAQ;8CACjB9D,YAAYO,OAAO8C,KAAKC,EAAE;;gCAE5B7B,0BACC,KAACvC;oCACC0G,MAAK;oCACL0B,UAAUxF;oCACVgE,SAAS,IAAM,KAAK3B,UAAUd,KAAKC,EAAE,EAAE;8CAEtC;qCAED;;2BAhBCD,KAAKC,EAAE;mCAqBlB,KAAChE;oBAAWwE,SAAQ;oBAAQe,OAAM;8BAC/BrB,YACG,2CACA;;gBAGPA,aAAa,CAAChC,6BACb,KAAClC;oBAAWwE,SAAQ;oBAAUe,OAAM;8BACjC,GAAGrB,UAAU,MAAM,EAAEA,cAAc,IAAI,aAAa,aAAa,CAAC,CAAC,GAClE;qBAEF;gBACH/B,aAAawB,WAAW,cAAcS,QAAQD,MAAM,iBACnD,MAACpE;oBACCkH,WAAU;oBACVC,SAAS;oBACTS,UAAU;oBACVR,IAAI;wBAAEC,YAAY;wBAAUQ,UAAU;oBAAO;;sCAE7C,KAACnH;4BACCQ,OAAOA;4BACPmF,OAAM;4BACN6B,OAAOrF;4BACPsF,UAAUrF;4BACVhB,OAAOuC;;sCAET,KAACxE;4BACC4E,SAAQ;4BACR2D,yBACE,KAAC7I;gCAAQ2D,MAAM9D,qBAAqB8D,IAAI;gCAAEqD,MAAM;;4BAElD0B,UAAUxF,QAAQ,CAACI;4BACnB4D,SAAS,IAAM,KAAK3B,UAAUjC,cAAcvB;sCAE3C;;;qBAGH;8BAOJ,KAACvB;gBACAuC,wBACC,KAAC1C;oBAAM+H,UAAS;8BACbvF,YACGE,UACA,8DACA;qBAEJjB,uBACF;;wBACGA,OAAOgH,KAAK,iBACX,KAACzI;4BACC+H,UACEtG,OAAOgH,KAAK,CAACC,MAAM,KAAK,YACxBjH,OAAOuC,MAAM,KAAK,aACd,UACA;sCAGLvC,OAAOgH,KAAK,CAACE,WAAW;6BAEzB;wBACHlH,EAAAA,kBAAAA,OAAOmH,OAAO,qBAAdnH,gBAAgBoH,IAAI,CAAC,CAACC,QAAUA,MAAMR,KAAK,mBAC1C;;8CACE,KAACjI;oCAAWwE,SAAQ;oCAAQ2C,IAAI;wCAAEY,YAAY;oCAAO;8CAClD;;8CAEH,MAAChI;oCAAMmH,SAAS;;wCACb9F,OAAOmH,OAAO,CACZzE,MAAM,CAAC,CAAC2E,QAAUA,MAAMR,KAAK,EAC7B/C,GAAG,CAAC,CAACuD,sBACJ,KAAC5H;gDAECO,QAAQqH;gDACR1C,SAASA,QAAQ2C,GAAG,CAAC,GAAGD,MAAME,IAAI,CAAC,CAAC,EAAEF,MAAMtD,IAAI,EAAE;+CAF7C,GAAGsD,MAAME,IAAI,CAAC,CAAC,EAAEF,MAAMtD,IAAI,EAAE;wCAKvC/D,OAAOwH,eAAe,IACvBxH,EAAAA,gBAAAA,OAAOgH,KAAK,qBAAZhH,cAAciH,MAAM,MAAK,yBACvB,KAACxH;4CACCO,QAAQA,OAAOwH,eAAe;4CAC9B7C,SAAS;6CAET;;;;6BAGN;sCACJ,MAAChG;4BACCkH,WAAU;4BACVC,SAAS;4BACTC,IAAI;gCAAES,UAAU;gCAAQiB,KAAK;4BAAE;;gCAE9BzH,OAAOuC,MAAM,KAAK,eAAexB,0BAChC,KAACvC;oCACC4E,SAAQ;oCACRwD,UAAUxF;oCACVgE,SAAS,IAAM,KAAK5B;8CAEnBpC,OAAO,aAAa;qCAErB;gCACHpB,OAAOuC,MAAM,KAAK,4BACjB,KAAC/D;oCACC4E,SAAQ;oCACRwD,UAAUxF;oCACVgE,SAAS,IAAM,KAAKnC;8CAEnB7B,OAAO,cAAc;qCAEtB;;;;qBAGN;;;;IAMZ,qBACE;;0BACE,KAACxD;gBAAiB8J,OAAOzH;;YACxBoF;;;AAGP;AACAvF,qBAAqBC,WAAW,GAAG;AAEnC,eAAeJ,uBAAsB"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ONE SITE'S SUPPRESSION LIST, opened from the organization's summary —
3
+ * `/[orgSlug]/emails/suppressions/{hostId}`.
4
+ *
5
+ * The same card a site's own Emails page draws, with a picker to move to
6
+ * another site's list and the way back to the summary. The site is named by
7
+ * its document id rather than its subdomain because the id is what the list
8
+ * is stored under and what does not change when the site is renamed.
9
+ *
10
+ * Only a site the mount lists is opened. The rules would refuse any other
11
+ * site's list anyway; saying so here is what stops a stale link rendering an
12
+ * empty list that reads as "nobody is suppressed".
13
+ */
14
+ export declare function OrgSiteSuppressions(props: {
15
+ hostId: string;
16
+ }): import("react").JSX.Element;
17
+ export declare namespace OrgSiteSuppressions {
18
+ var displayName: string;
19
+ }
20
+ export default OrgSiteSuppressions;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { PageHeaderRecord } from "@aglyn/aglyn";
20
+ import { AppLink } from "@aglyn/shared-ui-jsx";
21
+ import { Alert, Button, Stack, Typography } from "@mui/material";
22
+ import { useRouter } from "next/navigation";
23
+ import { OrgSiteSelect, orgSiteName, useEmailOrgMount } from "./email-org-mount.js";
24
+ import SuppressionsCard from "./suppressions-card.js";
25
+ /**
26
+ * ONE SITE'S SUPPRESSION LIST, opened from the organization's summary —
27
+ * `/[orgSlug]/emails/suppressions/{hostId}`.
28
+ *
29
+ * The same card a site's own Emails page draws, with a picker to move to
30
+ * another site's list and the way back to the summary. The site is named by
31
+ * its document id rather than its subdomain because the id is what the list
32
+ * is stored under and what does not change when the site is renamed.
33
+ *
34
+ * Only a site the mount lists is opened. The rules would refuse any other
35
+ * site's list anyway; saying so here is what stops a stale link rendering an
36
+ * empty list that reads as "nobody is suppressed".
37
+ */ export function OrgSiteSuppressions(props) {
38
+ const { hostId } = props;
39
+ const mount = useEmailOrgMount();
40
+ const router = useRouter();
41
+ if (!mount) return null;
42
+ const summaryHref = `${mount.basePath}/suppressions`;
43
+ const known = mount.hosts.some((site)=>site.id === hostId);
44
+ const back = /*#__PURE__*/ _jsx(Button, _extends({
45
+ component: AppLink
46
+ }, {
47
+ componentVariant: 'naked',
48
+ nativeButton: false
49
+ }, {
50
+ href: summaryHref,
51
+ size: "small",
52
+ color: "primary",
53
+ children: 'All sites'
54
+ }));
55
+ if (!known) {
56
+ return mount.hostsReady ? /*#__PURE__*/ _jsx(Alert, {
57
+ severity: "warning",
58
+ action: back,
59
+ children: 'That site is not one of this organization’s, or it has been ' + 'removed. Choose a site from the list of every site’s suppressions.'
60
+ }) : /*#__PURE__*/ _jsx(Typography, {
61
+ variant: "body2",
62
+ color: "text.secondary",
63
+ children: 'Loading…'
64
+ });
65
+ }
66
+ return /*#__PURE__*/ _jsxs(Stack, {
67
+ spacing: 2,
68
+ children: [
69
+ /*#__PURE__*/ _jsx(PageHeaderRecord, {
70
+ title: orgSiteName(mount, hostId)
71
+ }),
72
+ /*#__PURE__*/ _jsxs(Stack, {
73
+ direction: "row",
74
+ spacing: 1,
75
+ useFlexGap: true,
76
+ sx: {
77
+ alignItems: 'center',
78
+ flexWrap: 'wrap'
79
+ },
80
+ children: [
81
+ /*#__PURE__*/ _jsx(OrgSiteSelect, {
82
+ mount: mount,
83
+ label: "Site",
84
+ value: hostId,
85
+ onChange: (next)=>router.push(`${summaryHref}/${encodeURIComponent(next)}`)
86
+ }),
87
+ back
88
+ ]
89
+ }),
90
+ /*#__PURE__*/ _jsx(SuppressionsCard, {
91
+ hostId: hostId
92
+ }, hostId)
93
+ ]
94
+ });
95
+ }
96
+ OrgSiteSuppressions.displayName = 'OrgSiteSuppressions';
97
+ export default OrgSiteSuppressions;
98
+
99
+ //# sourceMappingURL=org-site-suppressions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-site-suppressions.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 { PageHeaderRecord } from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport { useRouter } from 'next/navigation'\nimport { OrgSiteSelect, orgSiteName, useEmailOrgMount } from './email-org-mount'\nimport SuppressionsCard from './suppressions-card'\n\n/**\n * ONE SITE'S SUPPRESSION LIST, opened from the organization's summary —\n * `/[orgSlug]/emails/suppressions/{hostId}`.\n *\n * The same card a site's own Emails page draws, with a picker to move to\n * another site's list and the way back to the summary. The site is named by\n * its document id rather than its subdomain because the id is what the list\n * is stored under and what does not change when the site is renamed.\n *\n * Only a site the mount lists is opened. The rules would refuse any other\n * site's list anyway; saying so here is what stops a stale link rendering an\n * empty list that reads as \"nobody is suppressed\".\n */\nexport function OrgSiteSuppressions(props: { hostId: string }) {\n const { hostId } = props\n const mount = useEmailOrgMount()\n const router = useRouter()\n if (!mount) return null\n\n const summaryHref = `${mount.basePath}/suppressions`\n const known = mount.hosts.some((site) => site.id === hostId)\n const back = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={summaryHref}\n size=\"small\"\n color=\"primary\"\n >\n {'All sites'}\n </Button>\n )\n\n if (!known) {\n return mount.hostsReady ? (\n <Alert severity=\"warning\" action={back}>\n {'That site is not one of this organization’s, or it has been ' +\n 'removed. Choose a site from the list of every site’s suppressions.'}\n </Alert>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n )\n }\n\n return (\n <Stack spacing={2}>\n {/* The heading and the trail name the site whose list this is. */}\n <PageHeaderRecord title={orgSiteName(mount, hostId)} />\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={hostId}\n onChange={(next) =>\n router.push(`${summaryHref}/${encodeURIComponent(next)}`)\n }\n />\n {back}\n </Stack>\n {/* Keyed, so moving to another site starts its list from the top. */}\n <SuppressionsCard key={hostId} hostId={hostId} />\n </Stack>\n )\n}\nOrgSiteSuppressions.displayName = 'OrgSiteSuppressions'\n\nexport default OrgSiteSuppressions\n"],"names":["PageHeaderRecord","AppLink","Alert","Button","Stack","Typography","useRouter","OrgSiteSelect","orgSiteName","useEmailOrgMount","SuppressionsCard","OrgSiteSuppressions","props","hostId","mount","router","summaryHref","basePath","known","hosts","some","site","id","back","component","componentVariant","nativeButton","href","size","color","hostsReady","severity","action","variant","spacing","title","direction","useFlexGap","sx","alignItems","flexWrap","label","value","onChange","next","push","encodeURIComponent","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,QAAQ,eAAc;AAC/C,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,KAAK,EAAEC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAChE,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,aAAa,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,uBAAmB;AAChF,OAAOC,sBAAsB,yBAAqB;AAElD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,oBAAoBC,KAAyB;IAC3D,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAME,QAAQL;IACd,MAAMM,SAAST;IACf,IAAI,CAACQ,OAAO,OAAO;IAEnB,MAAME,cAAc,GAAGF,MAAMG,QAAQ,CAAC,aAAa,CAAC;IACpD,MAAMC,QAAQJ,MAAMK,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKT;IACrD,MAAMU,qBACJ,KAACpB;QACCqB,WAAWvB;OACN;QAAEwB,kBAAkB;QAASC,cAAc;IAAM;QACtDC,MAAMX;QACNY,MAAK;QACLC,OAAM;kBAEL;;IAIL,IAAI,CAACX,OAAO;QACV,OAAOJ,MAAMgB,UAAU,iBACrB,KAAC5B;YAAM6B,UAAS;YAAUC,QAAQT;sBAC/B,iEACC;2BAGJ,KAAClB;YAAW4B,SAAQ;YAAQJ,OAAM;sBAC/B;;IAGP;IAEA,qBACE,MAACzB;QAAM8B,SAAS;;0BAEd,KAAClC;gBAAiBmC,OAAO3B,YAAYM,OAAOD;;0BAC5C,MAACT;gBACCgC,WAAU;gBACVF,SAAS;gBACTG,UAAU;gBACVC,IAAI;oBAAEC,YAAY;oBAAUC,UAAU;gBAAO;;kCAE7C,KAACjC;wBACCO,OAAOA;wBACP2B,OAAM;wBACNC,OAAO7B;wBACP8B,UAAU,CAACC,OACT7B,OAAO8B,IAAI,CAAC,GAAG7B,YAAY,CAAC,EAAE8B,mBAAmBF,OAAO;;oBAG3DrB;;;0BAGH,KAACb;gBAA8BG,QAAQA;eAAhBA;;;AAG7B;AACAF,oBAAoBoC,WAAW,GAAG;AAElC,eAAepC,oBAAmB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * EVERY SITE'S SUPPRESSION LIST, SUMMED BY REASON.
3
+ *
4
+ * A suppression is one site's: the unsubscribe was from that site's mail, the
5
+ * bounce was an address that site sent to, and the send path filters each
6
+ * site's audience by its own list. So the organization's page does not merge
7
+ * the lists into one — an address suppressed on one site is still mailable by
8
+ * another — it answers the question a person managing several sites actually
9
+ * has, "is any site's list going stale?", with one row per site, and opens a
10
+ * site's own list from its row.
11
+ *
12
+ * The figures are the same four server aggregates a site's own card reads,
13
+ * through the same function, so a row here and that site's chips agree. They
14
+ * are read for one page of sites at a time and never as documents: a
15
+ * suppression list can hold tens of thousands of addresses, and an aggregate
16
+ * costs the same whatever it counts.
17
+ */
18
+ export declare function OrgSuppressionsCard(): import("react").JSX.Element;
19
+ export declare namespace OrgSuppressionsCard {
20
+ var displayName: string;
21
+ }
22
+ export default OrgSuppressionsCard;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { pluginDocsHelp } from "@aglyn/aglyn";
20
+ import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
21
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
22
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
23
+ import { useFirestore } from "@aglyn/tenant-feature-instance";
24
+ import { Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
25
+ import { useRouter } from "next/navigation";
26
+ import { useEffect, useRef, useState } from "react";
27
+ import { orgSiteName, orgSitePageLabel, useEmailOrgMount, useOrgSitePage } from "./email-org-mount.js";
28
+ import { readSuppressionTotals, SUPPRESSION_REASONS } from "./suppression-totals.js";
29
+ /** The breakdown's columns, in the order a site's own card lists its chips. */ const REASON_COLUMNS = [
30
+ 'unsubscribe',
31
+ 'bounce',
32
+ 'complaint',
33
+ 'manual'
34
+ ];
35
+ /**
36
+ * EVERY SITE'S SUPPRESSION LIST, SUMMED BY REASON.
37
+ *
38
+ * A suppression is one site's: the unsubscribe was from that site's mail, the
39
+ * bounce was an address that site sent to, and the send path filters each
40
+ * site's audience by its own list. So the organization's page does not merge
41
+ * the lists into one — an address suppressed on one site is still mailable by
42
+ * another — it answers the question a person managing several sites actually
43
+ * has, "is any site's list going stale?", with one row per site, and opens a
44
+ * site's own list from its row.
45
+ *
46
+ * The figures are the same four server aggregates a site's own card reads,
47
+ * through the same function, so a row here and that site's chips agree. They
48
+ * are read for one page of sites at a time and never as documents: a
49
+ * suppression list can hold tens of thousands of addresses, and an aggregate
50
+ * costs the same whatever it counts.
51
+ */ export function OrgSuppressionsCard() {
52
+ const mount = useEmailOrgMount();
53
+ return mount ? /*#__PURE__*/ _jsx(OrgSuppressionsTable, {
54
+ mount: mount
55
+ }) : null;
56
+ }
57
+ OrgSuppressionsCard.displayName = 'OrgSuppressionsCard';
58
+ function OrgSuppressionsTable(props) {
59
+ const { mount } = props;
60
+ const firestore = useFirestore();
61
+ const router = useRouter();
62
+ const sitePage = useOrgSitePage(mount);
63
+ const { sites } = sitePage;
64
+ const [totals, setTotals] = useState({});
65
+ /*
66
+ * Each site is counted ONCE per visit. Paging asks only the sites not yet
67
+ * counted, so paging back re-reads nothing. An answer is kept whenever it
68
+ * lands, so moving on mid-flight does not strand a site's count — only
69
+ * leaving the section discards one.
70
+ */ const requested = useRef(new Set());
71
+ const mounted = useRef(true);
72
+ useEffect(()=>{
73
+ mounted.current = true;
74
+ return ()=>{
75
+ mounted.current = false;
76
+ };
77
+ }, []);
78
+ useEffect(()=>{
79
+ for (const site of sites){
80
+ if (requested.current.has(site.id)) continue;
81
+ requested.current.add(site.id);
82
+ const settle = (read)=>{
83
+ if (mounted.current) {
84
+ setTotals((current)=>_extends({}, current, {
85
+ [site.id]: read
86
+ }));
87
+ }
88
+ };
89
+ void readSuppressionTotals(firestore, site.id).then(settle).catch(()=>settle('failed'));
90
+ }
91
+ }, [
92
+ firestore,
93
+ sites
94
+ ]);
95
+ const siteHref = (hostId)=>`${mount.basePath}/suppressions/${encodeURIComponent(hostId)}`;
96
+ const noSites = mount.hostsReady && mount.hosts.length === 0;
97
+ const anyFailed = sites.some((site)=>totals[site.id] === 'failed');
98
+ return /*#__PURE__*/ _jsx(CardDisplay, {
99
+ header: "Suppressions",
100
+ help: pluginDocsHelp('emailCampaigns', {
101
+ anchor: '#suppressions'
102
+ }),
103
+ contentGutterX: true,
104
+ contentGutterY: true,
105
+ contentBordered: "all",
106
+ children: /*#__PURE__*/ _jsxs(Stack, {
107
+ spacing: 1.5,
108
+ children: [
109
+ /*#__PURE__*/ _jsx(Typography, {
110
+ variant: "body2",
111
+ color: "text.secondary",
112
+ children: 'Addresses each site’s marketing email skips, and why. Every site ' + 'keeps its own list — someone who unsubscribed from one site can ' + 'still be emailed by another — so open a site to see who is on ' + 'its list, or to add or remove someone.'
113
+ }),
114
+ noSites ? /*#__PURE__*/ _jsx(Typography, {
115
+ variant: "body2",
116
+ color: "text.secondary",
117
+ children: 'This organization has no sites yet.'
118
+ }) : /*#__PURE__*/ _jsxs(ScrollTable, {
119
+ size: "small",
120
+ children: [
121
+ /*#__PURE__*/ _jsx(TableHead, {
122
+ children: /*#__PURE__*/ _jsxs(TableRow, {
123
+ children: [
124
+ /*#__PURE__*/ _jsx(TableCell, {
125
+ children: 'Site'
126
+ }),
127
+ REASON_COLUMNS.map((reason)=>/*#__PURE__*/ _jsx(TableCell, {
128
+ align: "right",
129
+ children: SUPPRESSION_REASONS[reason].label
130
+ }, reason)),
131
+ /*#__PURE__*/ _jsx(TableCell, {
132
+ align: "right",
133
+ children: 'Total'
134
+ })
135
+ ]
136
+ })
137
+ }),
138
+ /*#__PURE__*/ _jsx(TableBody, {
139
+ children: sites.map((site)=>{
140
+ const read = totals[site.id];
141
+ const figures = read && read !== 'failed' ? read : null;
142
+ /*
143
+ * Loading and failed are different answers, and neither is a
144
+ * zero. A dash says the count could not be read; the ellipsis
145
+ * says it has not arrived yet.
146
+ */ const cell = (value)=>figures ? Number(value != null ? value : 0).toLocaleString() : read === 'failed' ? '—' : '…';
147
+ const total = figures ? REASON_COLUMNS.reduce((sum, reason)=>sum + figures[reason], 0) : undefined;
148
+ return /*#__PURE__*/ _jsxs(TableRow, {
149
+ hover: true,
150
+ sx: {
151
+ cursor: 'pointer'
152
+ },
153
+ onClick: ()=>router.push(siteHref(site.id)),
154
+ children: [
155
+ /*#__PURE__*/ _jsx(TableCell, {
156
+ children: /*#__PURE__*/ _jsx(AppLink, {
157
+ href: siteHref(site.id),
158
+ onClick: (event)=>event.stopPropagation(),
159
+ children: orgSiteName(mount, site.id)
160
+ })
161
+ }),
162
+ REASON_COLUMNS.map((reason)=>/*#__PURE__*/ _jsx(TableCell, {
163
+ align: "right",
164
+ children: cell(figures == null ? void 0 : figures[reason])
165
+ }, reason)),
166
+ /*#__PURE__*/ _jsx(TableCell, {
167
+ align: "right",
168
+ children: cell(total)
169
+ })
170
+ ]
171
+ }, site.id);
172
+ })
173
+ })
174
+ ]
175
+ }),
176
+ anyFailed ? /*#__PURE__*/ _jsx(Typography, {
177
+ variant: "caption",
178
+ color: "text.secondary",
179
+ children: 'A dash is a count that could not be read. It is not the same ' + 'as nobody being suppressed.'
180
+ }) : null,
181
+ noSites ? null : /*#__PURE__*/ _jsx(ListPagination, {
182
+ page: sitePage.page,
183
+ pageSize: sitePage.pageSize,
184
+ rowCount: sites.length,
185
+ count: sitePage.count,
186
+ onPageChange: sitePage.setPage,
187
+ labelDisplayedRows: orgSitePageLabel
188
+ })
189
+ ]
190
+ })
191
+ });
192
+ }
193
+ OrgSuppressionsTable.displayName = 'OrgSuppressionsTable';
194
+ export default OrgSuppressionsCard;
195
+
196
+ //# sourceMappingURL=org-suppressions-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-suppressions-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\nimport {\n readSuppressionTotals,\n SUPPRESSION_REASONS,\n type SuppressionTotals,\n} from './suppression-totals'\n\n/** The breakdown's columns, in the order a site's own card lists its chips. */\nconst REASON_COLUMNS: ReadonlyArray<keyof SuppressionTotals> = [\n 'unsubscribe',\n 'bounce',\n 'complaint',\n 'manual',\n]\n\n/** A site's figures, or why there are none. */\ntype SiteTotals = SuppressionTotals | 'failed'\n\n/**\n * EVERY SITE'S SUPPRESSION LIST, SUMMED BY REASON.\n *\n * A suppression is one site's: the unsubscribe was from that site's mail, the\n * bounce was an address that site sent to, and the send path filters each\n * site's audience by its own list. So the organization's page does not merge\n * the lists into one — an address suppressed on one site is still mailable by\n * another — it answers the question a person managing several sites actually\n * has, \"is any site's list going stale?\", with one row per site, and opens a\n * site's own list from its row.\n *\n * The figures are the same four server aggregates a site's own card reads,\n * through the same function, so a row here and that site's chips agree. They\n * are read for one page of sites at a time and never as documents: a\n * suppression list can hold tens of thousands of addresses, and an aggregate\n * costs the same whatever it counts.\n */\nexport function OrgSuppressionsCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgSuppressionsTable mount={mount} /> : null\n}\nOrgSuppressionsCard.displayName = 'OrgSuppressionsCard'\n\nfunction OrgSuppressionsTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const sitePage = useOrgSitePage(mount)\n const { sites } = sitePage\n const [totals, setTotals] = useState<Record<string, SiteTotals>>({})\n\n /*\n * Each site is counted ONCE per visit. Paging asks only the sites not yet\n * counted, so paging back re-reads nothing. An answer is kept whenever it\n * lands, so moving on mid-flight does not strand a site's count — only\n * leaving the section discards one.\n */\n const requested = useRef(new Set<string>())\n const mounted = useRef(true)\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n useEffect(() => {\n for (const site of sites) {\n if (requested.current.has(site.id)) continue\n requested.current.add(site.id)\n const settle = (read: SiteTotals) => {\n if (mounted.current) {\n setTotals((current) => ({ ...current, [site.id]: read }))\n }\n }\n void readSuppressionTotals(firestore, site.id)\n .then(settle)\n .catch(() => settle('failed'))\n }\n }, [firestore, sites])\n\n const siteHref = (hostId: string) =>\n `${mount.basePath}/suppressions/${encodeURIComponent(hostId)}`\n const noSites = mount.hostsReady && mount.hosts.length === 0\n const anyFailed = sites.some((site) => totals[site.id] === 'failed')\n\n return (\n <CardDisplay\n header=\"Suppressions\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#suppressions' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Addresses each site’s marketing email skips, and why. Every site ' +\n 'keeps its own list — someone who unsubscribed from one site can ' +\n 'still be emailed by another — so open a site to see who is on ' +\n 'its list, or to add or remove someone.'}\n </Typography>\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Site'}</TableCell>\n {REASON_COLUMNS.map((reason) => (\n <TableCell key={reason} align=\"right\">\n {SUPPRESSION_REASONS[reason].label}\n </TableCell>\n ))}\n <TableCell align=\"right\">{'Total'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {sites.map((site) => {\n const read = totals[site.id]\n const figures = read && read !== 'failed' ? read : null\n /*\n * Loading and failed are different answers, and neither is a\n * zero. A dash says the count could not be read; the ellipsis\n * says it has not arrived yet.\n */\n const cell = (value: number | undefined) =>\n figures\n ? Number(value ?? 0).toLocaleString()\n : read === 'failed'\n ? '—'\n : '…'\n const total = figures\n ? REASON_COLUMNS.reduce(\n (sum, reason) => sum + figures[reason],\n 0,\n )\n : undefined\n return (\n <TableRow\n key={site.id}\n hover\n sx={{ cursor: 'pointer' }}\n onClick={() => router.push(siteHref(site.id))}\n >\n <TableCell>\n {/*\n The row's own handler would fire too and push the same\n route twice — one history entry per back press.\n */}\n <AppLink\n href={siteHref(site.id)}\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {orgSiteName(mount, site.id)}\n </AppLink>\n </TableCell>\n {REASON_COLUMNS.map((reason) => (\n <TableCell key={reason} align=\"right\">\n {cell(figures?.[reason])}\n </TableCell>\n ))}\n <TableCell align=\"right\">{cell(total)}</TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n {anyFailed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A dash is a count that could not be read. It is not the same ' +\n 'as nobody being suppressed.'}\n </Typography>\n ) : null}\n {noSites ? null : (\n <ListPagination\n page={sitePage.page}\n pageSize={sitePage.pageSize}\n rowCount={sites.length}\n count={sitePage.count}\n onPageChange={sitePage.setPage}\n labelDisplayedRows={orgSitePageLabel}\n />\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOrgSuppressionsTable.displayName = 'OrgSuppressionsTable'\n\nexport default OrgSuppressionsCard\n"],"names":["pluginDocsHelp","AppLink","CardDisplay","ListPagination","ScrollTable","useFirestore","Stack","TableBody","TableCell","TableHead","TableRow","Typography","useRouter","useEffect","useRef","useState","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","readSuppressionTotals","SUPPRESSION_REASONS","REASON_COLUMNS","OrgSuppressionsCard","mount","OrgSuppressionsTable","displayName","props","firestore","router","sitePage","sites","totals","setTotals","requested","Set","mounted","current","site","has","id","add","settle","read","then","catch","siteHref","hostId","basePath","encodeURIComponent","noSites","hostsReady","hosts","length","anyFailed","some","header","help","anchor","contentGutterX","contentGutterY","contentBordered","spacing","variant","color","size","map","reason","align","label","figures","cell","value","Number","toLocaleString","total","reduce","sum","undefined","hover","sx","cursor","onClick","push","href","event","stopPropagation","page","pageSize","rowCount","count","onPageChange","setPage","labelDisplayedRows"],"mappings":"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"}
@@ -1,8 +1,20 @@
1
+ import { type SendingDnsRecordView } from '@aglyn/tenant-feature-instance/hooks/use-sending-identity-api';
1
2
  export interface SendingDomainDetailProps {
2
3
  hostId: string;
3
4
  domain: string;
4
5
  basePath: string;
5
6
  }
7
+ /**
8
+ * The record block, in the shape a registrar's own form asks for. Shared with
9
+ * the organization's page for a domain, which prints the same records.
10
+ */
11
+ export declare function RecordRow(props: {
12
+ record: SendingDnsRecordView;
13
+ missing: boolean;
14
+ }): import("react").JSX.Element;
15
+ export declare namespace RecordRow {
16
+ var displayName: string;
17
+ }
6
18
  /**
7
19
  * ONE SENDING DOMAIN: what to publish, whether we can see it, and whether
8
20
  * this site sends as it.
@@ -26,7 +26,10 @@ import { useRouter } from "next/navigation";
26
26
  import { useCallback, useEffect, useRef, useState } from "react";
27
27
  import { describeSendingDomain, describeSendingDomainRemoval, INCONCLUSIVE_CHECK } from "../model/sending-domain-status.js";
28
28
  import { useSendingApi } from "@aglyn/tenant-feature-instance/hooks/use-sending-identity-api";
29
- /** The record block, in the shape a registrar's own form asks for. */ function RecordRow(props) {
29
+ /**
30
+ * The record block, in the shape a registrar's own form asks for. Shared with
31
+ * the organization's page for a domain, which prints the same records.
32
+ */ export function RecordRow(props) {
30
33
  const { record, missing } = props;
31
34
  const target = record.priority ? `${record.priority} ${record.value}` : record.value;
32
35
  return /*#__PURE__*/ _jsxs(Stack, {