@aglyn/plugins-email 1.0.0-beta.232 → 1.0.0-beta.233

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-email-templates-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 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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.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 type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_ROW_HEIGHT,\n} from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { listQueryConstraints } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\nimport {\n OrgSiteSelect,\n orgSiteEmailsPath,\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\n\n/**\n * How many of one site's templates the organization's list reads.\n *\n * Lower than the site's own list, because this one reads a page of sites at\n * once: a site with more than this is named under the table, with a link to\n * its own Templates page, which reads its whole set.\n */\nexport const ORG_TEMPLATE_CEILING = 50\n\n/*\n * What the organization's templates grid's Filters panel offers (AGL-3321).\n *\n * The template clauses and the search are the site list's own\n * (`EMAIL_TEMPLATE_QUERY`): ONE plan, put on EVERY site's query, so each\n * site's read is already the answer and nothing is matched in the browser.\n * Site is not a field of a screen but a choice of WHICH sites' queries run:\n * a Site clause reads exactly the sites it names, whichever page of sites\n * they are on. Origin (yours or installed) is not offered, for the reason the\n * declaration gives: a query cannot ask for a field to be absent.\n */\nconst SITE_FIELD: ListFilterField = {\n column: 'site',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\nconst ORG_TEMPLATE_FILTER_FIELDS: readonly ListFilterField[] = [\n ...EMAIL_TEMPLATE_QUERY.fields,\n SITE_FIELD,\n]\nconst ORG_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n ...EMAIL_TEMPLATE_FILTER_HEADERS,\n site: 'Site',\n}\n\n/** The sites a Site clause names, or null when none is in force. */\nexport function orgTemplateSiteScope(\n clauses: readonly ListFilterClause[],\n): string[] | null {\n const named = clauses\n .filter((clause) => clause.field === SITE_FIELD.column)\n .map((clause) =>\n clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n )\n if (!named.length) return null\n // Several Site clauses all hold: a site must be named by each.\n return named.reduce((kept, next) => kept.filter((id) => next.includes(id)))\n}\n\n/** One site's read, as the table assembles it. */\ninterface SiteRead {\n rows: Array<Record<string, any>>\n /** The site holds more templates than {@link ORG_TEMPLATE_CEILING}. */\n truncated: boolean\n /** The read has answered — rows or none. */\n settled: boolean\n}\n\n/**\n * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.\n *\n * A component rather than a hook in a loop, because the number of sites\n * changes and a hook's call count may not: each mounted reader is one\n * listener, and a page of sites mounts one per site on it.\n *\n * Every predicate is on the QUERY — the email scope, every clause and the\n * search word, one plan for all the sites (AGL-3321) — so a site's read is\n * its first templates THAT MATCH, by name, and a site with a thousand pages\n * reads its matching emails and nothing else. It reads one past the ceiling,\n * so the table can say when a site holds more matches than it listed.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n plan: ListQueryPlan\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, plan, onRead } = props\n const firestore = useFirestore()\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n ...listQueryConstraints(plan),\n limit(ORG_TEMPLATE_CEILING + 1),\n ),\n [firestore, hostId, planKey],\n { idField: '$id' },\n )\n // One window per snapshot, so a re-render that brings no new snapshot hands\n // the table the same rows it already holds.\n const window = useMemo(\n () => ceilingedWindow(data, ORG_TEMPLATE_CEILING),\n [data],\n )\n useEffect(() => {\n onRead(hostId, {\n rows: window.rows,\n truncated: window.truncated,\n settled: status !== 'loading',\n })\n }, [window, status, hostId, onRead])\n return null\n}\nSiteTemplatesReader.displayName = 'SiteTemplatesReader'\n\n/**\n * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one\n * table.\n *\n * A template is a screen on ONE site — its besigner, its assets and the\n * messages built from it are that site's — so this table reads each site in\n * turn and names the site on every row. A row opens the template's page on\n * its own site, where its report and its recipients are; nothing here is an\n * organization-level template page, because there is no such thing.\n *\n * The reads are one listener per site, over one page of sites at a time —\n * ordered by name — each its site's query for the clauses and search in\n * force, capped at {@link ORG_TEMPLATE_CEILING} matches. The shared footer\n * pages the matches in hand; an organization with more sites than one page\n * chooses which page of sites is read with the Sites picker above the\n * table, or names sites with the Site filter. Both caps say so on screen when\n * they bite, so a missing template is never mistaken for one that does not\n * exist.\n *\n * \"New template\" asks which site the design is made on — the besigner opens\n * on a site — and skips the question when the org has one site.\n */\nexport function OrgEmailTemplatesCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgEmailTemplatesTable mount={mount} /> : null\n}\nOrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard'\n\nfunction OrgEmailTemplatesTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const sitePage = useOrgSitePage(mount)\n const gridFilter = useListGridFilter({ selectFields: [SITE_FIELD.column] })\n /*\n * Which sites are read: the ones a Site clause names, else the page of\n * sites. The other clauses and the search are ONE plan every site's query\n * carries.\n */\n const siteScope = useMemo(\n () => orgTemplateSiteScope(gridFilter.clauses),\n [gridFilter.clauses],\n )\n const sites = useMemo(\n () =>\n siteScope\n ? mount.hosts.filter((host) => siteScope.includes(host.id))\n : sitePage.sites,\n [siteScope, mount.hosts, sitePage.sites],\n )\n const plan = useMemo(\n () =>\n planListQuery(\n EMAIL_TEMPLATE_QUERY,\n {\n clauses: gridFilter.clauses.filter(\n (clause) => clause.field !== SITE_FIELD.column,\n ),\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n nameSearchNormalizers,\n ),\n [gridFilter.clauses, gridFilter.searchWords],\n )\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: ORG_TEMPLATE_FILTER_FIELDS,\n headers: ORG_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const [reads, setReads] = useState<Record<string, SiteRead>>({})\n const onRead = useCallback(\n (hostId: string, read: SiteRead) =>\n setReads((current) => {\n const previous = current[hostId]\n // An answer the table already holds changes nothing to draw.\n if (\n previous &&\n previous.rows === read.rows &&\n previous.truncated === read.truncated &&\n previous.settled === read.settled\n ) {\n return current\n }\n return { ...current, [hostId]: read }\n }),\n [],\n )\n\n /*\n * One row per template, named by site. The row id joins the site to the\n * screen id: two sites can hold a screen with the same id, and a grid that\n * keyed on the screen alone would draw one of them twice. Every row is an\n * answer some site's query gave; the sort only interleaves the sites'\n * answers by the name each was already ordered by.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? []).map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n siteName: orgSiteName(mount, site.id),\n })),\n )\n .sort(\n (a, b) =>\n String(a['nameLower'] ?? '').localeCompare(String(b['nameLower'] ?? '')) ||\n orgSiteName(mount, a.hostId).localeCompare(\n orgSiteName(mount, b.hostId),\n ),\n ),\n [sites, reads, mount],\n )\n const settled = sites.every((site) => reads[site.id]?.settled)\n const crowded = sites.filter((site) => reads[site.id]?.truncated)\n\n /*\n * The matches in hand, a page at a time on the shared footer. Choosing\n * another page of sites, or another question, starts from the first page:\n * page four of one answer is not a position in another.\n */\n const filterOptions = useMemo(\n () => ({\n // Every site the organization has: a Site clause reads the sites it\n // names wherever they sit in the page of sites.\n site: mount.hosts.map((site) => ({ value: site.id, label: orgSiteName(mount, site.id) })),\n }),\n [mount],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const siteKey = sites.map((site) => site.id).join(',')\n // A new answer starts again at its first page.\n useEffect(() => setPage(0), [siteKey, planKey])\n const shown = useMemo(\n () => rows.slice(page * pageSize, page * pageSize + pageSize),\n [rows, page, pageSize],\n )\n const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize)\n\n const templateName = (row: Record<string, any>) =>\n String(row['displayName'] ?? 'Untitled template')\n /** The template's own page, on the site that holds it. */\n const templateHref = (row: Record<string, any>) =>\n orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`)\n const siteSubdomain = (hostId: string) =>\n mount.hosts.find((site) => site.id === hostId)?.subdomain ?? null\n const besignerHref = (\n hostId: string,\n screenId: string,\n versionId: unknown,\n ) => {\n const subdomain = siteSubdomain(hostId)\n return subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug: mount.orgSlug,\n host: subdomain,\n screenId,\n versionId: String(versionId),\n })\n : null\n }\n\n const handleDelete = async (row: Record<string, any>) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${templateName(row)}\" will be removed from ` +\n `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` +\n 'keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', row['hostId'], 'screens', row['screenId']),\n emailTemplateSoftDelete(),\n )\n }\n\n const rowActions = (row: Record<string, any>): RowActionsMenuItem[] => {\n const page = templateHref(row)\n const editor = besignerHref(\n row['hostId'],\n row['screenId'],\n row['versionId'],\n )\n const unlinkedReason =\n 'This site has no console address yet, so it cannot be linked to.'\n return [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: page ?? undefined,\n disabled: !page,\n disabledReason: unlinkedReason,\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: editor ?? undefined,\n disabled: !editor,\n disabledReason: row['versionId']\n ? unlinkedReason\n : 'This template has never been saved in the besigner. Open it from its page.',\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(row),\n },\n ]\n }\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 const href = templateHref(row)\n return href ? (\n <AppLink\n href={href}\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 value\n )\n },\n },\n {\n field: 'site',\n headerName: 'Site',\n width: 170,\n valueGetter: (_value, row) => row.hostId,\n renderCell: ({ row }) => row.siteName,\n },\n {\n // Whose template this is, as on a site's own list.\n field: 'origin',\n headerName: 'Origin',\n width: 130,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\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 /*==========================================\n * CREATE, ON A SITE.\n *\n * The same document a site's own list creates — `createEmailScreen`\n * through the quota-enforcing resources route — made as the chosen site,\n * then straight into that site's besigner. One site answers the question\n * itself; otherwise the dialog asks, defaulting to the session's pick.\n *=========================================*/\n const [choosing, setChoosing] = useState(false)\n const [createHostId, setCreateHostId] = useState('')\n const [creating, setCreating] = useState(false)\n\n const create = async (hostId: string) => {\n if (creating || !hostId) return\n setCreating(true)\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n setChoosing(false)\n const editor = besignerHref(hostId, screenId, versionId)\n if (editor) {\n void router.push(editor)\n } else {\n enqueueSnackbar(\n `Template created on ${orgSiteName(mount, hostId)}. Open it from ` +\n 'that site’s Emails page.',\n { variant: 'success' },\n )\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n } finally {\n setCreating(false)\n }\n }\n\n const handleNew = () => {\n if (mount.hosts.length === 1) return void create(mount.hosts[0].id)\n setCreateHostId(mount.pickedHostId ?? '')\n setChoosing(true)\n }\n\n const noSites = mount.hostsReady && mount.hosts.length === 0\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 disabled={noSites || creating}\n onClick={handleNew}\n >\n {creating ? 'Creating…' : 'New template'}\n </Button>\n ),\n }}\n >\n {sites.map((site) => (\n <SiteTemplatesReader\n key={site.id}\n hostId={site.id}\n plan={plan}\n onRead={onRead}\n />\n ))}\n <Stack spacing={1.5}>\n {/*\n WHICH SITES' TEMPLATES ARE LISTED, when there are more sites than\n one page holds. Only the chosen page's sites are read; a template on\n a site of another page is one choice away, not missing. It picks\n what is read, a scope, so it is not one of the grid's filters.\n */}\n {sitePageCount > 1 && !siteScope ? (\n <TextField\n select\n size=\"small\"\n label=\"Sites\"\n value={sitePage.page}\n onChange={(event) => sitePage.setPage(Number(event.target.value))}\n sx={{ maxWidth: 260 }}\n >\n {Array.from({ length: sitePageCount }, (_, index) => (\n <MenuItem key={index} value={index}>\n {orgSitePageLabel({\n from: index * sitePage.pageSize + 1,\n to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),\n count: sitePage.count,\n })}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A template is designed on ' +\n 'a site, in its besigner, so it appears here once there is one.'}\n </Typography>\n ) : rows.length === 0 && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'None of these sites has an email template yet. Design one ' +\n 'here, then send it from a campaign — a new template opens in ' +\n 'the besigner with email-safe components only.'\n : 'Loading templates…'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={ORG_TEMPLATE_FILTER_FIELDS}\n headers={ORG_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={listFilterGridColumns(\n columns,\n ORG_TEMPLATE_FILTER_FIELDS,\n filterOptions,\n ORG_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to every site's query; the grid\n // neither filters nor sorts the rows it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No templates match these filters\"\n onOpen={(_id, row) => {\n const href = templateHref(row)\n if (href) router.push(href)\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {crowded.map((site) => {\n const href = orgSiteEmailsPath(mount, site.id, 'templates')\n return (\n <Alert key={site.id} severity=\"info\">\n {`${orgSiteName(mount, site.id)} has more than ` +\n `${ORG_TEMPLATE_CEILING} ${filtering ? 'matching templates' : 'templates'}, ` +\n 'and only the first of them are listed here. '}\n {href ? (\n <AppLink href={href}>{'See all of its templates'}</AppLink>\n ) : null}\n </Alert>\n )\n })}\n </Stack>\n\n <Dialog\n open={choosing}\n onClose={creating ? undefined : () => setChoosing(false)}\n fullWidth\n maxWidth=\"xs\"\n >\n <DialogTitle>{'New template'}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'A template is designed on one site, in its besigner, and is ' +\n 'sent from that site’s campaigns.'}\n </DialogContentText>\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={createHostId}\n onChange={setCreateHostId}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setChoosing(false)} disabled={creating}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!createHostId || creating}\n onClick={() => void create(createHostId)}\n >\n {creating ? 'Creating…' : 'Create and open'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable'\n\nexport default OrgEmailTemplatesCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","planListQuery","useListGridFilter","nameSearchNormalizers","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","listQueryConstraints","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","collection","doc","limit","query","updateDoc","useRouter","useCallback","useEffect","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","emailTemplateSoftDelete","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","SITE_FIELD","column","kind","path","operators","ORG_TEMPLATE_FILTER_FIELDS","fields","ORG_TEMPLATE_FILTER_HEADERS","site","orgTemplateSiteScope","clauses","named","filter","clause","field","map","value","split","entry","trim","Boolean","length","reduce","kept","next","id","includes","SiteTemplatesReader","props","hostId","plan","onRead","firestore","planKey","JSON","stringify","filters","orderBy","data","status","idField","window","rows","truncated","settled","displayName","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","gridFilter","selectFields","siteScope","sites","hosts","host","search","searchWords","base","filtering","some","word","refusals","refused","headers","reads","setReads","read","current","previous","flatMap","screen","$id","screenId","String","siteName","sort","a","b","localeCompare","every","crowded","filterOptions","label","page","setPage","pageSize","setPageSize","siteKey","join","shown","slice","sitePageCount","Math","ceil","count","templateName","row","templateHref","siteSubdomain","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","rowActions","editor","unlinkedReason","key","icon","size","href","undefined","disabled","disabledReason","destructive","onClick","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","event","stopPropagation","width","origin","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","setClauses","options","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","onOpen","_id","rowCount","onPageChange","onPageSizeChange","severity","open","onClose","fullWidth","mb"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-email-templates-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 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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.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 type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_ROW_HEIGHT,\n} from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { listQueryConstraints } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef, GridSortModel } from '@mui/x-data-grid'\nimport {\n type ListPageSort,\n sortListRows,\n} from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\nimport {\n OrgSiteSelect,\n orgSiteEmailsPath,\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\n\n/**\n * How many of one site's templates the organization's list reads.\n *\n * Lower than the site's own list, because this one reads a page of sites at\n * once: a site with more than this is named under the table, with a link to\n * its own Templates page, which reads its whole set.\n */\nexport const ORG_TEMPLATE_CEILING = 50\n\n/*\n * What the organization's templates grid's Filters panel offers (AGL-3321).\n *\n * The template clauses and the search are the site list's own\n * (`EMAIL_TEMPLATE_QUERY`): ONE plan, put on EVERY site's query, so each\n * site's read is already the answer and nothing is matched in the browser.\n * Site is not a field of a screen but a choice of WHICH sites' queries run:\n * a Site clause reads exactly the sites it names, whichever page of sites\n * they are on. Origin (yours or installed) is not offered, for the reason the\n * declaration gives: a query cannot ask for a field to be absent.\n */\nconst SITE_FIELD: ListFilterField = {\n column: 'site',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\nconst ORG_TEMPLATE_FILTER_FIELDS: readonly ListFilterField[] = [\n ...EMAIL_TEMPLATE_QUERY.fields,\n SITE_FIELD,\n]\nconst ORG_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n ...EMAIL_TEMPLATE_FILTER_HEADERS,\n site: 'Site',\n}\n\n/** The sites a Site clause names, or null when none is in force. */\nexport function orgTemplateSiteScope(\n clauses: readonly ListFilterClause[],\n): string[] | null {\n const named = clauses\n .filter((clause) => clause.field === SITE_FIELD.column)\n .map((clause) =>\n clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n )\n if (!named.length) return null\n // Several Site clauses all hold: a site must be named by each.\n return named.reduce((kept, next) => kept.filter((id) => next.includes(id)))\n}\n\n/** How each header orders the matches in hand (AGL-3680). */\nconst ORG_TEMPLATE_SORTS: Readonly<Record<string, ListPageSort<Record<string, any>>>> = {\n displayName: (row) => String(row['displayName'] ?? 'Untitled template'),\n site: (row) => row['siteName'],\n origin: (row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n}\n\n/** One site's read, as the table assembles it. */\ninterface SiteRead {\n rows: Array<Record<string, any>>\n /** The site holds more templates than {@link ORG_TEMPLATE_CEILING}. */\n truncated: boolean\n /** The read has answered — rows or none. */\n settled: boolean\n}\n\n/**\n * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.\n *\n * A component rather than a hook in a loop, because the number of sites\n * changes and a hook's call count may not: each mounted reader is one\n * listener, and a page of sites mounts one per site on it.\n *\n * Every predicate is on the QUERY — the email scope, every clause and the\n * search word, one plan for all the sites (AGL-3321) — so a site's read is\n * its first templates THAT MATCH, by name, and a site with a thousand pages\n * reads its matching emails and nothing else. It reads one past the ceiling,\n * so the table can say when a site holds more matches than it listed.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n plan: ListQueryPlan\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, plan, onRead } = props\n const firestore = useFirestore()\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n ...listQueryConstraints(plan),\n limit(ORG_TEMPLATE_CEILING + 1),\n ),\n [firestore, hostId, planKey],\n { idField: '$id' },\n )\n // One window per snapshot, so a re-render that brings no new snapshot hands\n // the table the same rows it already holds.\n const window = useMemo(\n () => ceilingedWindow(data, ORG_TEMPLATE_CEILING),\n [data],\n )\n useEffect(() => {\n onRead(hostId, {\n rows: window.rows,\n truncated: window.truncated,\n settled: status !== 'loading',\n })\n }, [window, status, hostId, onRead])\n return null\n}\nSiteTemplatesReader.displayName = 'SiteTemplatesReader'\n\n/**\n * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one\n * table.\n *\n * A template is a screen on ONE site — its besigner, its assets and the\n * messages built from it are that site's — so this table reads each site in\n * turn and names the site on every row. A row opens the template's page on\n * its own site, where its report and its recipients are; nothing here is an\n * organization-level template page, because there is no such thing.\n *\n * The reads are one listener per site, over one page of sites at a time —\n * ordered by name — each its site's query for the clauses and search in\n * force, capped at {@link ORG_TEMPLATE_CEILING} matches. The shared footer\n * pages the matches in hand; an organization with more sites than one page\n * chooses which page of sites is read with the Sites picker above the\n * table, or names sites with the Site filter. Both caps say so on screen when\n * they bite, so a missing template is never mistaken for one that does not\n * exist.\n *\n * \"New template\" asks which site the design is made on — the besigner opens\n * on a site — and skips the question when the org has one site.\n */\nexport function OrgEmailTemplatesCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgEmailTemplatesTable mount={mount} /> : null\n}\nOrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard'\n\nfunction OrgEmailTemplatesTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const sitePage = useOrgSitePage(mount)\n const gridFilter = useListGridFilter({ selectFields: [SITE_FIELD.column] })\n /*\n * Which sites are read: the ones a Site clause names, else the page of\n * sites. The other clauses and the search are ONE plan every site's query\n * carries.\n */\n const siteScope = useMemo(\n () => orgTemplateSiteScope(gridFilter.clauses),\n [gridFilter.clauses],\n )\n const sites = useMemo(\n () =>\n siteScope\n ? mount.hosts.filter((host) => siteScope.includes(host.id))\n : sitePage.sites,\n [siteScope, mount.hosts, sitePage.sites],\n )\n const plan = useMemo(\n () =>\n planListQuery(\n EMAIL_TEMPLATE_QUERY,\n {\n clauses: gridFilter.clauses.filter(\n (clause) => clause.field !== SITE_FIELD.column,\n ),\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n nameSearchNormalizers,\n ),\n [gridFilter.clauses, gridFilter.searchWords],\n )\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: ORG_TEMPLATE_FILTER_FIELDS,\n headers: ORG_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const [reads, setReads] = useState<Record<string, SiteRead>>({})\n const onRead = useCallback(\n (hostId: string, read: SiteRead) =>\n setReads((current) => {\n const previous = current[hostId]\n // An answer the table already holds changes nothing to draw.\n if (\n previous &&\n previous.rows === read.rows &&\n previous.truncated === read.truncated &&\n previous.settled === read.settled\n ) {\n return current\n }\n return { ...current, [hostId]: read }\n }),\n [],\n )\n\n /*\n * One row per template, named by site. The row id joins the site to the\n * screen id: two sites can hold a screen with the same id, and a grid that\n * keyed on the screen alone would draw one of them twice. Every row is an\n * answer some site's query gave; the sort only interleaves the sites'\n * answers by the name each was already ordered by.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? []).map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n siteName: orgSiteName(mount, site.id),\n })),\n )\n .sort(\n (a, b) =>\n String(a['nameLower'] ?? '').localeCompare(String(b['nameLower'] ?? '')) ||\n orgSiteName(mount, a.hostId).localeCompare(\n orgSiteName(mount, b.hostId),\n ),\n ),\n [sites, reads, mount],\n )\n const settled = sites.every((site) => reads[site.id]?.settled)\n const crowded = sites.filter((site) => reads[site.id]?.truncated)\n\n /*\n * The matches in hand, a page at a time on the shared footer. Choosing\n * another page of sites, or another question, starts from the first page:\n * page four of one answer is not a position in another.\n */\n const filterOptions = useMemo(\n () => ({\n // Every site the organization has: a Site clause reads the sites it\n // names wherever they sit in the page of sites.\n site: mount.hosts.map((site) => ({ value: site.id, label: orgSiteName(mount, site.id) })),\n }),\n [mount],\n )\n /*\n * EVERY HEADER SORTS (AGL-3680), over EVERY match in hand rather than the\n * page on screen: the rows are all loaded (each site capped, and the cap\n * says so above), so the sort is exact and the footer pages the sorted\n * whole. Site sorts by the name the column shows, not the site id.\n */\n const [sortModel, setSortModel] = useState<GridSortModel>([])\n const sorted = useMemo(() => {\n const [first] = sortModel\n const value = first?.sort ? ORG_TEMPLATE_SORTS[first.field] : undefined\n return value && first?.sort ? sortListRows(rows, value, first.sort) : rows\n }, [rows, sortModel])\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const siteKey = sites.map((site) => site.id).join(',')\n const sortKey = JSON.stringify(sortModel)\n // A new answer, or a new order, starts again at its first page.\n useEffect(() => setPage(0), [siteKey, planKey, sortKey])\n const shown = useMemo(\n () => sorted.slice(page * pageSize, page * pageSize + pageSize),\n [sorted, page, pageSize],\n )\n const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize)\n\n const templateName = (row: Record<string, any>) =>\n String(row['displayName'] ?? 'Untitled template')\n /** The template's own page, on the site that holds it. */\n const templateHref = (row: Record<string, any>) =>\n orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`)\n const siteSubdomain = (hostId: string) =>\n mount.hosts.find((site) => site.id === hostId)?.subdomain ?? null\n const besignerHref = (\n hostId: string,\n screenId: string,\n versionId: unknown,\n ) => {\n const subdomain = siteSubdomain(hostId)\n return subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug: mount.orgSlug,\n host: subdomain,\n screenId,\n versionId: String(versionId),\n })\n : null\n }\n\n const handleDelete = async (row: Record<string, any>) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${templateName(row)}\" will be removed from ` +\n `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` +\n 'keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', row['hostId'], 'screens', row['screenId']),\n emailTemplateSoftDelete(),\n )\n }\n\n const rowActions = (row: Record<string, any>): RowActionsMenuItem[] => {\n const page = templateHref(row)\n const editor = besignerHref(\n row['hostId'],\n row['screenId'],\n row['versionId'],\n )\n const unlinkedReason =\n 'This site has no console address yet, so it cannot be linked to.'\n return [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: page ?? undefined,\n disabled: !page,\n disabledReason: unlinkedReason,\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: editor ?? undefined,\n disabled: !editor,\n disabledReason: row['versionId']\n ? unlinkedReason\n : 'This template has never been saved in the besigner. Open it from its page.',\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(row),\n },\n ]\n }\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 const href = templateHref(row)\n return href ? (\n <AppLink\n href={href}\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 value\n )\n },\n },\n {\n field: 'site',\n headerName: 'Site',\n width: 170,\n valueGetter: (_value, row) => row.hostId,\n renderCell: ({ row }) => row.siteName,\n },\n {\n // Whose template this is, as on a site's own list.\n field: 'origin',\n headerName: 'Origin',\n width: 130,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\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 /*==========================================\n * CREATE, ON A SITE.\n *\n * The same document a site's own list creates — `createEmailScreen`\n * through the quota-enforcing resources route — made as the chosen site,\n * then straight into that site's besigner. One site answers the question\n * itself; otherwise the dialog asks, defaulting to the session's pick.\n *=========================================*/\n const [choosing, setChoosing] = useState(false)\n const [createHostId, setCreateHostId] = useState('')\n const [creating, setCreating] = useState(false)\n\n const create = async (hostId: string) => {\n if (creating || !hostId) return\n setCreating(true)\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n setChoosing(false)\n const editor = besignerHref(hostId, screenId, versionId)\n if (editor) {\n void router.push(editor)\n } else {\n enqueueSnackbar(\n `Template created on ${orgSiteName(mount, hostId)}. Open it from ` +\n 'that site’s Emails page.',\n { variant: 'success' },\n )\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n } finally {\n setCreating(false)\n }\n }\n\n const handleNew = () => {\n if (mount.hosts.length === 1) return void create(mount.hosts[0].id)\n setCreateHostId(mount.pickedHostId ?? '')\n setChoosing(true)\n }\n\n const noSites = mount.hostsReady && mount.hosts.length === 0\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 disabled={noSites || creating}\n onClick={handleNew}\n >\n {creating ? 'Creating…' : 'New template'}\n </Button>\n ),\n }}\n >\n {sites.map((site) => (\n <SiteTemplatesReader\n key={site.id}\n hostId={site.id}\n plan={plan}\n onRead={onRead}\n />\n ))}\n <Stack spacing={1.5}>\n {/*\n WHICH SITES' TEMPLATES ARE LISTED, when there are more sites than\n one page holds. Only the chosen page's sites are read; a template on\n a site of another page is one choice away, not missing. It picks\n what is read, a scope, so it is not one of the grid's filters.\n */}\n {sitePageCount > 1 && !siteScope ? (\n <TextField\n select\n size=\"small\"\n label=\"Sites\"\n value={sitePage.page}\n onChange={(event) => sitePage.setPage(Number(event.target.value))}\n sx={{ maxWidth: 260 }}\n >\n {Array.from({ length: sitePageCount }, (_, index) => (\n <MenuItem key={index} value={index}>\n {orgSitePageLabel({\n from: index * sitePage.pageSize + 1,\n to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),\n count: sitePage.count,\n })}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A template is designed on ' +\n 'a site, in its besigner, so it appears here once there is one.'}\n </Typography>\n ) : rows.length === 0 && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'None of these sites has an email template yet. Design one ' +\n 'here, then send it from a campaign — a new template opens in ' +\n 'the besigner with email-safe components only.'\n : 'Loading templates…'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={ORG_TEMPLATE_FILTER_FIELDS}\n headers={ORG_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={listFilterGridColumns(\n columns,\n ORG_TEMPLATE_FILTER_FIELDS,\n filterOptions,\n ORG_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to every site's query; a header\n // sorts every match in hand before the footer pages it.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n sortingMode=\"server\"\n sortModel={sortModel}\n onSortModelChange={setSortModel}\n noRowsLabel=\"No templates match these filters\"\n onOpen={(_id, row) => {\n const href = templateHref(row)\n if (href) router.push(href)\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {crowded.map((site) => {\n const href = orgSiteEmailsPath(mount, site.id, 'templates')\n return (\n <Alert key={site.id} severity=\"info\">\n {`${orgSiteName(mount, site.id)} has more than ` +\n `${ORG_TEMPLATE_CEILING} ${filtering ? 'matching templates' : 'templates'}, ` +\n 'and only the first of them are listed here. '}\n {href ? (\n <AppLink href={href}>{'See all of its templates'}</AppLink>\n ) : null}\n </Alert>\n )\n })}\n </Stack>\n\n <Dialog\n open={choosing}\n onClose={creating ? undefined : () => setChoosing(false)}\n fullWidth\n maxWidth=\"xs\"\n >\n <DialogTitle>{'New template'}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'A template is designed on one site, in its besigner, and is ' +\n 'sent from that site’s campaigns.'}\n </DialogContentText>\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={createHostId}\n onChange={setCreateHostId}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setChoosing(false)} disabled={creating}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!createHostId || creating}\n onClick={() => void create(createHostId)}\n >\n {creating ? 'Creating…' : 'Create and open'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable'\n\nexport default OrgEmailTemplatesCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","planListQuery","useListGridFilter","nameSearchNormalizers","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","listQueryConstraints","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","sortListRows","collection","doc","limit","query","updateDoc","useRouter","useCallback","useEffect","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","emailTemplateSoftDelete","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","SITE_FIELD","column","kind","path","operators","ORG_TEMPLATE_FILTER_FIELDS","fields","ORG_TEMPLATE_FILTER_HEADERS","site","orgTemplateSiteScope","clauses","named","filter","clause","field","map","value","split","entry","trim","Boolean","length","reduce","kept","next","id","includes","ORG_TEMPLATE_SORTS","displayName","row","String","origin","SiteTemplatesReader","props","hostId","plan","onRead","firestore","planKey","JSON","stringify","filters","orderBy","data","status","idField","window","rows","truncated","settled","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","gridFilter","selectFields","siteScope","sites","hosts","host","search","searchWords","base","filtering","some","word","refusals","refused","headers","reads","setReads","read","current","previous","flatMap","screen","$id","screenId","siteName","sort","a","b","localeCompare","every","crowded","filterOptions","label","sortModel","setSortModel","sorted","first","undefined","page","setPage","pageSize","setPageSize","siteKey","join","sortKey","shown","slice","sitePageCount","Math","ceil","count","templateName","templateHref","siteSubdomain","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","rowActions","editor","unlinkedReason","key","icon","size","href","disabled","disabledReason","destructive","onClick","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","event","stopPropagation","width","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","setClauses","options","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","onSortModelChange","noRowsLabel","onOpen","_id","rowCount","onPageChange","onPageSizeChange","severity","open","onClose","fullWidth","mb"],"mappings":"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"}
@@ -47,6 +47,7 @@ import { collection, deleteDoc, doc } from "firebase/firestore";
47
47
  import { useCallback, useEffect, useMemo, useState } from "react";
48
48
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
49
49
  import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
50
+ import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
50
51
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
51
52
  import { SUPPRESSION_FILTER_HEADERS, SUPPRESSION_LIST_QUERY } from "../constants/list-queries.js";
52
53
  import { describeSuppressionReason as describeReason, readSuppressionTotals, SUPPRESSION_REASONS } from "./suppression-totals.js";
@@ -157,12 +158,18 @@ const SUPPRESSION_SELECT_FIELDS = [
157
158
  */ const gridFilter = useListGridFilter({
158
159
  selectFields: SUPPRESSION_SELECT_FIELDS
159
160
  });
161
+ /*
162
+ * EVERY HEADER SORTS ON THE QUERY (AGL-3680): Since, Address and Reason
163
+ * are stored on every entry (see `SUPPRESSION_LIST_QUERY`). Newest first
164
+ * until a header is clicked.
165
+ */ const [askedSort, setAskedSort] = useState(SUPPRESSION_LIST_QUERY.sorts[0]);
160
166
  const { rows: entries, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
161
167
  collection: collection(firestore, 'hosts', hostId, 'suppressions'),
162
168
  declaration: SUPPRESSION_LIST_QUERY,
163
169
  request: {
164
170
  clauses: gridFilter.clauses,
165
- search: gridFilter.searchWords
171
+ search: gridFilter.searchWords,
172
+ sort: askedSort
166
173
  },
167
174
  deps: [
168
175
  firestore,
@@ -170,6 +177,14 @@ const SUPPRESSION_SELECT_FIELDS = [
170
177
  ],
171
178
  idField: '$id'
172
179
  });
180
+ const columnSort = useListColumnSort({
181
+ sorts: SUPPRESSION_LIST_QUERY.sorts,
182
+ defaultSort: SUPPRESSION_LIST_QUERY.sorts[0],
183
+ sort: askedSort,
184
+ onSortChange: setAskedSort,
185
+ orderBy: plan.orderBy,
186
+ rows: entries
187
+ });
173
188
  const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
174
189
  /*
175
190
  * IMPORT AND EXPORT (AGL-3529) open the console's transfer wizard and
@@ -582,22 +597,25 @@ const SUPPRESSION_SELECT_FIELDS = [
582
597
  }),
583
598
  /*#__PURE__*/ _jsx(ListQueryNotices, {
584
599
  refused: refusals,
585
- notices: plan.notices
600
+ notices: [
601
+ ...plan.notices,
602
+ ...columnSort.notices
603
+ ]
586
604
  }),
587
605
  /*#__PURE__*/ _jsx(ListTable, {
588
606
  "aria-label": "Suppressed addresses",
589
- rows: entries,
607
+ rows: columnSort.rows,
608
+ columnSort: columnSort,
590
609
  columns: listFilterGridColumns(columns, SUPPRESSION_LIST_QUERY.fields, SUPPRESSION_FILTER_OPTIONS, SUPPRESSION_FILTER_HEADERS),
591
610
  rowHeight: TABLE_ROW_HEIGHT,
592
611
  // Paged by the footer below, so the grid must not also slice.
593
612
  hideFooter: true,
594
- // The panel and the search go to the query; the grid neither
595
- // filters nor sorts the page it holds.
613
+ // The panel, the search and the headers go to the query; the
614
+ // grid neither filters nor sorts the page it holds.
596
615
  filterMode: "server",
597
616
  filterModel: gridFilter.filterModel,
598
617
  onFilterModelChange: gridFilter.onFilterModelChange,
599
618
  quickFilter: true,
600
- disableColumnSorting: true,
601
619
  noRowsLabel: "No suppressions match these filters"
602
620
  }),
603
621
  /*#__PURE__*/ _jsx(ListPagination, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport {\n mdiEmailCheckOutline,\n mdiTrayArrowDown,\n mdiTrayArrowUp,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n Container,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\n/*\n * The shared drawer, reached by its own path.\n *\n * `@aglyn/shared-ui-jsx`'s barrel deliberately does not re-export this one, so\n * a deep import is the supported way in rather than an escape hatch. The\n * console's `CreateArtifactDrawer` — what Screens, Components, Layouts and\n * Templates create through — is this same component with a form inside it,\n * and it lives in `apps/console`, which a plugin library may not import. So\n * the chrome is composed from the same primitive rather than duplicated from\n * the wrapper.\n */\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Chip,\n IconButton,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport Button from '@mui/material/Button'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, deleteDoc, doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n SUPPRESSION_FILTER_HEADERS,\n SUPPRESSION_LIST_QUERY,\n} from '../constants/list-queries'\nimport {\n describeSuppressionReason as describeReason,\n readSuppressionTotals,\n SUPPRESSION_REASONS,\n type SuppressionTotals,\n} from './suppression-totals'\nimport { orgSiteName, useEmailOrgMount } from './email-org-mount'\nimport { SUPPRESSIONS_RESOURCE } from '../transfer/email-transfer-catalog'\n\nexport interface SuppressionsCardProps {\n hostId: string\n}\n\n/** A stored entry. `reason` is absent on anything written before AGL-2408. */\ninterface SuppressionRow {\n $id: string\n email?: string\n reason?: string\n suppressedAt?: { seconds?: number } | null\n createdAt?: { seconds?: number } | null\n /**\n * The site this entry was copied from when the two stopped being one\n * sender (AGL-3320). The copy keeps the original's reason and dates.\n */\n carriedFromHostId?: string\n}\n\n/*\n * What the suppressions grid's Filters panel offers, every clause and the\n * search on the list's query (AGL-3321): see `SUPPRESSION_LIST_QUERY`. The\n * Reason choices are the stored reasons, by the words the column uses.\n */\nconst SUPPRESSION_FILTER_OPTIONS = {\n reason: Object.entries(SUPPRESSION_REASONS).map(([value, described]) => ({\n value,\n label: described.label,\n })),\n}\nconst SUPPRESSION_SELECT_FIELDS = ['reason']\n\n/**\n * `YYYY-MM-DD` from a Firestore timestamp shape, or an em dash.\n *\n * `createdAt` first, and that ordering is the column's meaning rather than a\n * preference. Both writers restamp `suppressedAt` on every touch and write\n * `createdAt` only when the document is new, precisely so that a bounce\n * arriving after an unsubscribe does not move the date the person actually\n * unsubscribed. Reading `suppressedAt` first put the restamp on screen under a\n * heading that says \"Since\", and it is also the field the list is ordered by,\n * so a re-touched row would have sorted by one date and displayed another.\n */\nfunction onDate(row: SuppressionRow): string {\n const seconds = row.createdAt?.seconds ?? row.suppressedAt?.seconds\n if (!seconds) return '—'\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n\n/**\n * Suppressions (AGL-2410): who is not being emailed, and why.\n *\n * ## What was missing\n *\n * `hosts/{hostId}/suppressions` was written by two paths — the unsubscribe\n * handler and, since AGL-1918, the Resend webhook on a permanent bounce or a\n * complaint — and read by exactly one: `campaign-send.ts`, to filter an\n * audience. Nothing in the console displayed it. So a merchant could not\n * answer any of:\n *\n * - *\"My campaign says 500 recipients and 480 sent — who were the other\n * 20?\"* The send returns `{recipients, sent}` and the difference was\n * unexplained.\n * - *\"Is my list going stale?\"* A bounce rate is the single most useful\n * number about a list and there was nowhere to see it.\n * - *\"This address was suppressed by mistake.\"* There was no way to remove\n * an entry — and a link prescanner unsubscribing someone (AGL-2408 §2) was\n * therefore unrecoverable from inside the product.\n *\n * ## Why a surface and not another counter\n *\n * AGL-1918 deliberately did NOT write a `stats.bounces` counter alongside its\n * fix, because a number with no screen to show it is the written-but-never-\n * read shape this issue is about, one level up. So the fix is the READER, and\n * the breakdown here is derived from the rows on screen rather than from a\n * second stored figure that could disagree with them.\n *\n * ## Removing an entry\n *\n * A plain client `deleteDoc`, and that is a decision. The list belongs to the\n * merchant, host admins already read it through the same rules, and removing\n * a row does nothing except make an address targetable again — there is no\n * counter to launder and no money attached, which is the AGL-1367 test for\n * whether a write has to move server-side.\n *\n * The confirmation is not decoration either: for a `bounce` the address very\n * likely does not exist, and mailing it again is what a provider scores the\n * sending domain on. So the dialog says which reason is being overridden\n * rather than asking a generic \"are you sure\".\n */\nexport function SuppressionsCard(props: SuppressionsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // Present on the organization's page, which can name the other site.\n const orgMount = useEmailOrgMount()\n const [adding, setAdding] = useState(false)\n\n const [addInput, setAddInput] = useState('')\n const [addNote, setAddNote] = useState('')\n const [busy, setBusy] = useState(false)\n\n /*\n * The page IS the query, ordered by the server (AGL-2501, AGL-2292), and\n * every filter and search word is a predicate on it (AGL-3321).\n *\n * This was `limit(500)` with no `orderBy`, sorted by date in the browser.\n * Firestore answers an unordered limit in DOCUMENT-ID order, and an entry\n * here is keyed by `sha256(email)` — so the window was five hundred\n * addresses chosen by the hash of the address, and the client sort dressed\n * that sample up as the newest five hundred. A list past the ceiling\n * therefore hid whoever bounced this morning behind whoever happened to\n * hash low, with no gap on screen to notice and no control asking for more.\n *\n * `createdAt` is the safe field to order on, and that is checked rather\n * than assumed: both writers — the unsubscribe handler and the Resend\n * bounce/complaint webhook — stamp it when the document is created, the\n * pre-AGL-2408 handler wrote `{ email, createdAt }`, and `suppressions` is\n * not in `IMPORTABLE_FIELDS`, so no restore path can produce a row without\n * one. `suppressedAt` would NOT be safe: it is absent on every entry\n * written before AGL-1918, and `orderBy` drops documents that lack the\n * field rather than mis-sorting them.\n */\n const gridFilter = useListGridFilter({ selectFields: SUPPRESSION_SELECT_FIELDS })\n const {\n rows: entries,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<SuppressionRow>({\n collection: collection(firestore, 'hosts', hostId, 'suppressions'),\n declaration: SUPPRESSION_LIST_QUERY,\n request: { clauses: gridFilter.clauses, search: gridFilter.searchWords },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * IMPORT AND EXPORT (AGL-3529) open the console's transfer wizard and\n * export dialog on this site's list. An import only ever adds: an address\n * already here keeps its entry, and nothing removes one. The export's\n * \"current filter\" is this table's own query.\n */\n const transfer = useTransferLauncher()\n const exportFilter = filtering\n ? {\n label: 'Entries matching the table’s filters',\n value: { filters: plan.filters },\n }\n : null\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: SUPPRESSION_LIST_QUERY.fields,\n headers: SUPPRESSION_FILTER_HEADERS,\n options: SUPPRESSION_FILTER_OPTIONS,\n }),\n [plan.refused],\n )\n\n /*==========================================\n * THE BREAKDOWN IS A SERVER AGGREGATE, not a tally of the page.\n *\n * These chips answer \"is my list going stale?\", and they were a `reduce`\n * over whatever the listener had fetched — so on a site past the old\n * ceiling \"Bounced: 140\" meant 140 of an arbitrary five hundred, and under\n * a ten-row page it would have meant 140 of ten. A bounce rate computed\n * from a sample is not a bounce rate, and nothing on screen said it was one.\n *\n * `readSuppressionTotals` holds the four reads and the remainder rule for\n * unsubscribes, and the organization's summary of every site's list asks\n * the same function, so a site's figures read the same on both pages.\n *=========================================*/\n const [totalsEpoch, setTotalsEpoch] = useState(0)\n const [totals, setTotals] = useState<SuppressionTotals | null>(null)\n useEffect(() => {\n let active = true\n void readSuppressionTotals(firestore, hostId)\n .then((read) => {\n if (active) setTotals(read)\n })\n .catch(() => {\n // Held at null rather than zeroed. \"Bounced: 0\" is a confident wrong\n // number in the reassuring direction, and this card exists to warn.\n if (active) setTotals(null)\n })\n return () => {\n active = false\n }\n // The list is a live listener and refreshes itself; an aggregate is a\n // one-shot read and would otherwise keep reporting the breakdown from\n // before the address was put back.\n }, [firestore, hostId, totalsEpoch])\n\n /*\n * The ADD, through a route rather than a client write.\n *\n * The Remove button below writes straight from the browser, and this does\n * not, which looks inconsistent until the document id is considered: an\n * entry is keyed by `sha256` of the normalized address, and a browser\n * computing that itself would be a second derivation of the key every\n * reader shares. Getting it wrong is silent and one-directional — the\n * merchant is told the person is suppressed and the mail keeps going. A\n * removal has no such hazard: it names a row that is already on screen.\n */\n const handleAdd = useCallback(async () => {\n const typed = addInput.trim()\n if (!typed || busy) return\n setBusy(true)\n try {\n const response = await authorizedFetch(\n user,\n '/api/email/suppression-add',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, emails: typed, note: addNote.trim() }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The address could not be suppressed.',\n { variant: 'error' },\n )\n }\n const results = (payload?.results ?? []) as Array<{\n input: string\n added: boolean\n refusal?: string\n }>\n const added = Number(payload?.added ?? 0)\n const rejected = results.filter(\n (result) => result.refusal === 'not-an-address',\n )\n // The refusals are NAMED, because \"3 of 5 added\" leaves an operator to\n // work out which two, and the two that failed are the ones somebody\n // asked to stop being emailed.\n if (rejected.length) {\n enqueueSnackbar(\n `Not an email address: ${rejected\n .map((result) => result.input)\n .join(', ')}`,\n { variant: 'warning' },\n )\n }\n if (added) {\n enqueueSnackbar(\n added === 1\n ? 'Added to the suppression list'\n : `${added} addresses added to the suppression list`,\n { variant: 'success', persist: false },\n )\n } else if (!rejected.length) {\n enqueueSnackbar('Already on the suppression list', { variant: 'info' })\n }\n if (added) {\n setAddInput('')\n setAddNote('')\n setAdding(false)\n setTotalsEpoch((epoch) => epoch + 1)\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }, [addInput, addNote, busy, user, hostId, enqueueSnackbar])\n\n /**\n * Whether one address is ALSO on the platform-wide list.\n *\n * The two lists are consulted together at send time and were visible\n * separately, so a merchant who removed their own entry could still find\n * the address was never mailed, with nothing anywhere saying why. The\n * platform entry is invisible to them and cannot be lifted by them, which\n * is precisely why it has to be said before the click rather than\n * discovered from a recipient count that stays short.\n */\n const isBlockedPlatformWide = useCallback(\n async (email: string): Promise<boolean> => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/email/suppression-status',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, emails: email }),\n },\n )\n if (!response.ok) return false\n const payload = await response.json().catch(() => ({}))\n return ((payload?.platform ?? []) as string[]).length > 0\n } catch (error) {\n console.error(error)\n return false\n }\n },\n [user, hostId],\n )\n\n const handleRemove = async (row: SuppressionRow) => {\n const reason = describeReason(row.reason).label.toLowerCase()\n // The platform entry is invisible to a merchant and cannot be lifted by\n // one, so removing the site's row here changes nothing about whether the\n // address is mailed. Saying so BEFORE the click is the whole point: the\n // alternative is a merchant who removes the row, sends again, and sees a\n // recipient count that is still short with nothing explaining it.\n /*\n * ASKED, NOT MOUNTED.\n *\n * One keyed read for the one address the merchant is acting on, at the\n * moment they act. Fetching this for every visible row on mount would be\n * a request per page render for an answer that is only ever needed on a\n * click, and this card is one tab of a page a merchant opens to read.\n *\n * A failed check answers \"not blocked\", which is the ordinary case: the\n * dialog then reads exactly as it did before, and the removal still\n * works. Refusing to open the dialog because a supplementary lookup\n * failed would make an outage on an explanation into an outage on the\n * control it explains.\n */\n const alsoPlatform = row.email\n ? await isBlockedPlatformWide(row.email)\n : false\n const accepted = await confirm({\n title: alsoPlatform\n ? 'This address will still be skipped'\n : 'Put this address back on your list?',\n description: alsoPlatform\n ? `${row.email ?? 'This address'} bounced permanently or reported ` +\n 'spam somewhere else in Aglyn, so it is on the platform-wide list ' +\n 'as well as yours. Removing your entry will not start mail ' +\n 'reaching it — contact support to have the platform entry lifted.'\n : `${row.email ?? 'This address'} is suppressed because it ` +\n `${reason === 'bounced' ? 'bounced permanently' : reason === 'marked as spam' ? 'was marked as spam' : reason === 'added by hand' ? 'was added by hand' : 'unsubscribed'}. ` +\n 'Removing the entry means your next campaign will email it again.',\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating on\n // the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n try {\n await deleteDoc(\n doc(firestore, 'hosts', hostId, 'suppressions', row.$id),\n )\n enqueueSnackbar('Removed from the suppression list', {\n variant: 'success',\n persist: false,\n })\n setTotalsEpoch((epoch) => epoch + 1)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n }\n }\n\n const columns: GridColDef<SuppressionRow>[] = [\n {\n field: 'email',\n headerName: 'Address',\n flex: 1,\n minWidth: 240,\n /*\n Entries are keyed by `sha256(email)` because addresses are PII, and the\n address itself is stored in the document. An older row written before\n the address was stored has only its hash — which tells a merchant\n nothing, so it says so rather than displaying 64 hex characters.\n */\n renderCell: ({ row }) =>\n row.email || (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'(address not recorded)'}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Reason',\n width: 230,\n // An absent reason is an unsubscribe, as the chip says.\n valueGetter: (_value, row) => String(row.reason ?? 'unsubscribe'),\n renderCell: ({ row }) => {\n const described = describeReason(row.reason)\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n <Chip\n size=\"small\"\n color={described.color}\n variant=\"outlined\"\n label={described.label}\n />\n {/*\n A copy made when sites stopped being one sender. It is an\n entry like any other; the marker says why an address nobody\n on this site acted on is on its list.\n */}\n {row.carriedFromHostId ? (\n <Tooltip\n title={`Copied from ${\n orgMount\n ? orgSiteName(orgMount, row.carriedFromHostId)\n : 'another site'\n } when the two stopped being one sender`}\n >\n <Chip size=\"small\" variant=\"outlined\" label=\"Copied\" />\n </Tooltip>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'since',\n headerName: 'Since',\n width: 140,\n valueGetter: (_value, row) => onDate(row),\n },\n /*\n A SUPPRESSION HAS NO PAGE, so the row does not open one — this is the\n surface's one list whose rows are not a way in to anything. What it does\n have is the one act a merchant performs on an entry, and the trailing\n cluster is where every other list on this surface keeps that: a bare\n `Remove` in the row is a click that puts an address back into mailing\n range, sitting where the reader's eye is running along the row.\n */\n listActionsColumn(\n (row: SuppressionRow) => (\n <ListRowActions\n label={row.email || 'this entry'}\n items={[\n {\n key: 'remove',\n label: 'Remove from the list',\n icon: <MdiIcon path={mdiEmailCheckOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleRemove(row),\n },\n ]}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Suppressions\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#compliance' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {/* Each only for whom the route takes it (AGL-3554). */}\n {transfer?.can('export', { resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowDown.path} size={0.8} />}\n onClick={() =>\n transfer.openExport({\n resource: SUPPRESSIONS_RESOURCE,\n scope: 'host',\n hostId,\n ...(exportFilter ? { filter: exportFilter } : {}),\n })\n }\n >\n {'Export'}\n </Button>\n ) : null}\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport\n target={{ resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }}\n onFinished={() => setTotalsEpoch((epoch) => epoch + 1)}\n />\n {transfer?.can('import', { resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowUp.path} size={0.8} />}\n onClick={() =>\n transfer.openImport({\n resource: SUPPRESSIONS_RESOURCE,\n scope: 'host',\n hostId,\n onFinished: () => setTotalsEpoch((epoch) => epoch + 1),\n })\n }\n >\n {'Import'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => setAdding(true)}\n >\n {'Add'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Addresses this site’s marketing email skips. Someone lands here ' +\n 'by clicking unsubscribe, by bouncing permanently, by marking a ' +\n 'message as spam, or because you added them — this is where the ' +\n 'gap between a campaign’s recipient count and what it actually ' +\n 'sent comes from.'}\n </Typography>\n {entries.length === 0 && !hasMore && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nobody is suppressed. Every address in your audiences is ' +\n 'currently mailable.'}\n </Typography>\n ) : (\n <>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap' }}>\n {totals === null ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Could not read the breakdown. This is not the same as ' +\n 'nobody having bounced.'}\n </Typography>\n ) : (\n Object.entries(totals)\n // A reason nobody has hit is not news, and three chips\n // reading zero make the two that matter harder to find.\n .filter(([, total]) => total > 0)\n .map(([reason, total]) => {\n const described = describeReason(reason)\n return (\n <Chip\n key={reason}\n size=\"small\"\n color={described.color}\n variant=\"outlined\"\n label={`${described.label}: ${total}`}\n />\n )\n })\n )}\n </Stack>\n <ListFilterChips\n fields={SUPPRESSION_LIST_QUERY.fields}\n headers={SUPPRESSION_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SUPPRESSION_FILTER_OPTIONS}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Suppressed addresses\"\n rows={entries}\n columns={listFilterGridColumns(\n columns as GridColDef[],\n SUPPRESSION_LIST_QUERY.fields,\n SUPPRESSION_FILTER_OPTIONS,\n SUPPRESSION_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No suppressions match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={entries.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n // The collection's real size, so the footer's count line says\n // \"1–10 of 812\" rather than \"of more than 10\" — the aggregate\n // above already knows it, and it is the same number the chips\n // are a breakdown of. A filtered list's size is unknown.\n count={\n totals && !filtering\n ? Object.values(totals).reduce((a, b) => a + b, 0)\n : undefined\n }\n />\n </>\n )}\n </Stack>\n {/*\n A DRAWER, not a form stacked above the table.\n Creating is a drawer and picking is a dialog, which is how Screens,\n Components, Layouts and Templates all take a name before they create\n one; the chrome here is the same `NavigationDrawerComponent` those go\n through, composed directly because the console's wrapper around it\n lives in an application a plugin library may not import.\n */}\n <NavigationDrawerComponent\n open={adding}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={() => setAdding(false)}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={() => setAdding(false)}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'Stop emailing an address'}\n </Typography>\n </>\n }\n appBarRight={\n <Button\n variant=\"outlined\"\n color=\"inherit\"\n onClick={() => setAdding(false)}\n >\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Use this when somebody asks you to stop emailing them by ' +\n 'reply, by phone, or in person. They stay on your audiences ' +\n 'and keep every record you hold about them — this only stops ' +\n 'this site’s marketing email reaching them.'}\n </Typography>\n <TextField\n label=\"Email addresses\"\n value={addInput}\n onChange={(event) => setAddInput(event.target.value)}\n multiline\n minRows={3}\n fullWidth\n autoFocus\n helperText={\n 'One per line, or separated by commas. Up to 50 at a time.'\n }\n />\n <TextField\n label=\"Note (optional)\"\n value={addNote}\n onChange={(event) => setAddNote(event.target.value)}\n fullWidth\n helperText={\n 'How the request reached you. Kept with the entry as the ' +\n 'record that it was honored.'\n }\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Alert severity=\"info\">\n {'Order confirmations, booking reminders and password resets ' +\n 'are unaffected. Somebody who asked to stop hearing from ' +\n 'your marketing still gets their receipt.'}\n </Alert>\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n disabled={busy || !addInput.trim()}\n onClick={() => void handleAdd()}\n >\n {'Add to suppression list'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n </CardDisplay>\n )\n}\nSuppressionsCard.displayName = 'SuppressionsCard'\n\nexport default SuppressionsCard\n"],"names":["TransferResumeImport","pluginDocsHelp","useTransferLauncher","ICON_VARIANT_CLOSE","mdiEmailCheckOutline","mdiTrayArrowDown","mdiTrayArrowUp","CardDisplay","Container","MdiIcon","SrOnly","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","NavigationDrawerComponent","useSnackbar","Alert","Chip","IconButton","Stack","TextField","Tooltip","Typography","Button","collection","deleteDoc","doc","useCallback","useEffect","useMemo","useState","useFirestore","useUser","useListQuery","authorizedFetch","SUPPRESSION_FILTER_HEADERS","SUPPRESSION_LIST_QUERY","describeSuppressionReason","describeReason","readSuppressionTotals","SUPPRESSION_REASONS","orgSiteName","useEmailOrgMount","SUPPRESSIONS_RESOURCE","SUPPRESSION_FILTER_OPTIONS","reason","Object","entries","map","value","described","label","SUPPRESSION_SELECT_FIELDS","onDate","row","seconds","createdAt","suppressedAt","Date","toISOString","slice","SuppressionsCard","props","hostId","firestore","data","user","enqueueSnackbar","confirm","orgMount","adding","setAdding","addInput","setAddInput","addNote","setAddNote","busy","setBusy","gridFilter","selectFields","rows","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","deps","idField","filtering","length","some","word","trim","transfer","exportFilter","filters","refusals","refused","fields","headers","options","totalsEpoch","setTotalsEpoch","totals","setTotals","active","then","read","catch","handleAdd","typed","response","method","body","JSON","stringify","emails","note","payload","json","ok","error","variant","results","added","Number","rejected","filter","result","refusal","input","join","persist","epoch","console","isBlockedPlatformWide","email","platform","handleRemove","toLowerCase","alsoPlatform","accepted","title","description","confirmationText","confirmationButtonProps","color","$id","columns","field","headerName","flex","minWidth","renderCell","width","valueGetter","_value","String","direction","spacing","sx","alignItems","height","size","carriedFromHostId","items","key","icon","path","destructive","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","can","resource","scope","startIcon","openExport","target","onFinished","openImport","useFlexGap","flexWrap","total","onChange","setClauses","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","rowCount","onPageChange","onPageSizeChange","count","values","reduce","a","b","undefined","open","onClose","AppBarProps","appBarLeft","edge","mr","component","appBarRight","gutterY","event","multiline","minRows","fullWidth","autoFocus","helperText","slotProps","htmlInput","maxLength","severity","justifyContent","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport {\n mdiEmailCheckOutline,\n mdiTrayArrowDown,\n mdiTrayArrowUp,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n Container,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\n/*\n * The shared drawer, reached by its own path.\n *\n * `@aglyn/shared-ui-jsx`'s barrel deliberately does not re-export this one, so\n * a deep import is the supported way in rather than an escape hatch. The\n * console's `CreateArtifactDrawer` — what Screens, Components, Layouts and\n * Templates create through — is this same component with a form inside it,\n * and it lives in `apps/console`, which a plugin library may not import. So\n * the chrome is composed from the same primitive rather than duplicated from\n * the wrapper.\n */\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Chip,\n IconButton,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport Button from '@mui/material/Button'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, deleteDoc, doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n SUPPRESSION_FILTER_HEADERS,\n SUPPRESSION_LIST_QUERY,\n} from '../constants/list-queries'\nimport {\n describeSuppressionReason as describeReason,\n readSuppressionTotals,\n SUPPRESSION_REASONS,\n type SuppressionTotals,\n} from './suppression-totals'\nimport { orgSiteName, useEmailOrgMount } from './email-org-mount'\nimport { SUPPRESSIONS_RESOURCE } from '../transfer/email-transfer-catalog'\n\nexport interface SuppressionsCardProps {\n hostId: string\n}\n\n/** A stored entry. `reason` is absent on anything written before AGL-2408. */\ninterface SuppressionRow {\n $id: string\n email?: string\n reason?: string\n suppressedAt?: { seconds?: number } | null\n createdAt?: { seconds?: number } | null\n /**\n * The site this entry was copied from when the two stopped being one\n * sender (AGL-3320). The copy keeps the original's reason and dates.\n */\n carriedFromHostId?: string\n}\n\n/*\n * What the suppressions grid's Filters panel offers, every clause and the\n * search on the list's query (AGL-3321): see `SUPPRESSION_LIST_QUERY`. The\n * Reason choices are the stored reasons, by the words the column uses.\n */\nconst SUPPRESSION_FILTER_OPTIONS = {\n reason: Object.entries(SUPPRESSION_REASONS).map(([value, described]) => ({\n value,\n label: described.label,\n })),\n}\nconst SUPPRESSION_SELECT_FIELDS = ['reason']\n\n/**\n * `YYYY-MM-DD` from a Firestore timestamp shape, or an em dash.\n *\n * `createdAt` first, and that ordering is the column's meaning rather than a\n * preference. Both writers restamp `suppressedAt` on every touch and write\n * `createdAt` only when the document is new, precisely so that a bounce\n * arriving after an unsubscribe does not move the date the person actually\n * unsubscribed. Reading `suppressedAt` first put the restamp on screen under a\n * heading that says \"Since\", and it is also the field the list is ordered by,\n * so a re-touched row would have sorted by one date and displayed another.\n */\nfunction onDate(row: SuppressionRow): string {\n const seconds = row.createdAt?.seconds ?? row.suppressedAt?.seconds\n if (!seconds) return '—'\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n\n/**\n * Suppressions (AGL-2410): who is not being emailed, and why.\n *\n * ## What was missing\n *\n * `hosts/{hostId}/suppressions` was written by two paths — the unsubscribe\n * handler and, since AGL-1918, the Resend webhook on a permanent bounce or a\n * complaint — and read by exactly one: `campaign-send.ts`, to filter an\n * audience. Nothing in the console displayed it. So a merchant could not\n * answer any of:\n *\n * - *\"My campaign says 500 recipients and 480 sent — who were the other\n * 20?\"* The send returns `{recipients, sent}` and the difference was\n * unexplained.\n * - *\"Is my list going stale?\"* A bounce rate is the single most useful\n * number about a list and there was nowhere to see it.\n * - *\"This address was suppressed by mistake.\"* There was no way to remove\n * an entry — and a link prescanner unsubscribing someone (AGL-2408 §2) was\n * therefore unrecoverable from inside the product.\n *\n * ## Why a surface and not another counter\n *\n * AGL-1918 deliberately did NOT write a `stats.bounces` counter alongside its\n * fix, because a number with no screen to show it is the written-but-never-\n * read shape this issue is about, one level up. So the fix is the READER, and\n * the breakdown here is derived from the rows on screen rather than from a\n * second stored figure that could disagree with them.\n *\n * ## Removing an entry\n *\n * A plain client `deleteDoc`, and that is a decision. The list belongs to the\n * merchant, host admins already read it through the same rules, and removing\n * a row does nothing except make an address targetable again — there is no\n * counter to launder and no money attached, which is the AGL-1367 test for\n * whether a write has to move server-side.\n *\n * The confirmation is not decoration either: for a `bounce` the address very\n * likely does not exist, and mailing it again is what a provider scores the\n * sending domain on. So the dialog says which reason is being overridden\n * rather than asking a generic \"are you sure\".\n */\nexport function SuppressionsCard(props: SuppressionsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // Present on the organization's page, which can name the other site.\n const orgMount = useEmailOrgMount()\n const [adding, setAdding] = useState(false)\n\n const [addInput, setAddInput] = useState('')\n const [addNote, setAddNote] = useState('')\n const [busy, setBusy] = useState(false)\n\n /*\n * The page IS the query, ordered by the server (AGL-2501, AGL-2292), and\n * every filter and search word is a predicate on it (AGL-3321).\n *\n * This was `limit(500)` with no `orderBy`, sorted by date in the browser.\n * Firestore answers an unordered limit in DOCUMENT-ID order, and an entry\n * here is keyed by `sha256(email)` — so the window was five hundred\n * addresses chosen by the hash of the address, and the client sort dressed\n * that sample up as the newest five hundred. A list past the ceiling\n * therefore hid whoever bounced this morning behind whoever happened to\n * hash low, with no gap on screen to notice and no control asking for more.\n *\n * `createdAt` is the safe field to order on, and that is checked rather\n * than assumed: both writers — the unsubscribe handler and the Resend\n * bounce/complaint webhook — stamp it when the document is created, the\n * pre-AGL-2408 handler wrote `{ email, createdAt }`, and `suppressions` is\n * not in `IMPORTABLE_FIELDS`, so no restore path can produce a row without\n * one. `suppressedAt` would NOT be safe: it is absent on every entry\n * written before AGL-1918, and `orderBy` drops documents that lack the\n * field rather than mis-sorting them.\n */\n const gridFilter = useListGridFilter({ selectFields: SUPPRESSION_SELECT_FIELDS })\n /*\n * EVERY HEADER SORTS ON THE QUERY (AGL-3680): Since, Address and Reason\n * are stored on every entry (see `SUPPRESSION_LIST_QUERY`). Newest first\n * until a header is clicked.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(\n SUPPRESSION_LIST_QUERY.sorts[0],\n )\n const {\n rows: entries,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<SuppressionRow>({\n collection: collection(firestore, 'hosts', hostId, 'suppressions'),\n declaration: SUPPRESSION_LIST_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const columnSort = useListColumnSort<SuppressionRow>({\n sorts: SUPPRESSION_LIST_QUERY.sorts,\n defaultSort: SUPPRESSION_LIST_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: entries,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * IMPORT AND EXPORT (AGL-3529) open the console's transfer wizard and\n * export dialog on this site's list. An import only ever adds: an address\n * already here keeps its entry, and nothing removes one. The export's\n * \"current filter\" is this table's own query.\n */\n const transfer = useTransferLauncher()\n const exportFilter = filtering\n ? {\n label: 'Entries matching the table’s filters',\n value: { filters: plan.filters },\n }\n : null\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: SUPPRESSION_LIST_QUERY.fields,\n headers: SUPPRESSION_FILTER_HEADERS,\n options: SUPPRESSION_FILTER_OPTIONS,\n }),\n [plan.refused],\n )\n\n /*==========================================\n * THE BREAKDOWN IS A SERVER AGGREGATE, not a tally of the page.\n *\n * These chips answer \"is my list going stale?\", and they were a `reduce`\n * over whatever the listener had fetched — so on a site past the old\n * ceiling \"Bounced: 140\" meant 140 of an arbitrary five hundred, and under\n * a ten-row page it would have meant 140 of ten. A bounce rate computed\n * from a sample is not a bounce rate, and nothing on screen said it was one.\n *\n * `readSuppressionTotals` holds the four reads and the remainder rule for\n * unsubscribes, and the organization's summary of every site's list asks\n * the same function, so a site's figures read the same on both pages.\n *=========================================*/\n const [totalsEpoch, setTotalsEpoch] = useState(0)\n const [totals, setTotals] = useState<SuppressionTotals | null>(null)\n useEffect(() => {\n let active = true\n void readSuppressionTotals(firestore, hostId)\n .then((read) => {\n if (active) setTotals(read)\n })\n .catch(() => {\n // Held at null rather than zeroed. \"Bounced: 0\" is a confident wrong\n // number in the reassuring direction, and this card exists to warn.\n if (active) setTotals(null)\n })\n return () => {\n active = false\n }\n // The list is a live listener and refreshes itself; an aggregate is a\n // one-shot read and would otherwise keep reporting the breakdown from\n // before the address was put back.\n }, [firestore, hostId, totalsEpoch])\n\n /*\n * The ADD, through a route rather than a client write.\n *\n * The Remove button below writes straight from the browser, and this does\n * not, which looks inconsistent until the document id is considered: an\n * entry is keyed by `sha256` of the normalized address, and a browser\n * computing that itself would be a second derivation of the key every\n * reader shares. Getting it wrong is silent and one-directional — the\n * merchant is told the person is suppressed and the mail keeps going. A\n * removal has no such hazard: it names a row that is already on screen.\n */\n const handleAdd = useCallback(async () => {\n const typed = addInput.trim()\n if (!typed || busy) return\n setBusy(true)\n try {\n const response = await authorizedFetch(\n user,\n '/api/email/suppression-add',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, emails: typed, note: addNote.trim() }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The address could not be suppressed.',\n { variant: 'error' },\n )\n }\n const results = (payload?.results ?? []) as Array<{\n input: string\n added: boolean\n refusal?: string\n }>\n const added = Number(payload?.added ?? 0)\n const rejected = results.filter(\n (result) => result.refusal === 'not-an-address',\n )\n // The refusals are NAMED, because \"3 of 5 added\" leaves an operator to\n // work out which two, and the two that failed are the ones somebody\n // asked to stop being emailed.\n if (rejected.length) {\n enqueueSnackbar(\n `Not an email address: ${rejected\n .map((result) => result.input)\n .join(', ')}`,\n { variant: 'warning' },\n )\n }\n if (added) {\n enqueueSnackbar(\n added === 1\n ? 'Added to the suppression list'\n : `${added} addresses added to the suppression list`,\n { variant: 'success', persist: false },\n )\n } else if (!rejected.length) {\n enqueueSnackbar('Already on the suppression list', { variant: 'info' })\n }\n if (added) {\n setAddInput('')\n setAddNote('')\n setAdding(false)\n setTotalsEpoch((epoch) => epoch + 1)\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }, [addInput, addNote, busy, user, hostId, enqueueSnackbar])\n\n /**\n * Whether one address is ALSO on the platform-wide list.\n *\n * The two lists are consulted together at send time and were visible\n * separately, so a merchant who removed their own entry could still find\n * the address was never mailed, with nothing anywhere saying why. The\n * platform entry is invisible to them and cannot be lifted by them, which\n * is precisely why it has to be said before the click rather than\n * discovered from a recipient count that stays short.\n */\n const isBlockedPlatformWide = useCallback(\n async (email: string): Promise<boolean> => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/email/suppression-status',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, emails: email }),\n },\n )\n if (!response.ok) return false\n const payload = await response.json().catch(() => ({}))\n return ((payload?.platform ?? []) as string[]).length > 0\n } catch (error) {\n console.error(error)\n return false\n }\n },\n [user, hostId],\n )\n\n const handleRemove = async (row: SuppressionRow) => {\n const reason = describeReason(row.reason).label.toLowerCase()\n // The platform entry is invisible to a merchant and cannot be lifted by\n // one, so removing the site's row here changes nothing about whether the\n // address is mailed. Saying so BEFORE the click is the whole point: the\n // alternative is a merchant who removes the row, sends again, and sees a\n // recipient count that is still short with nothing explaining it.\n /*\n * ASKED, NOT MOUNTED.\n *\n * One keyed read for the one address the merchant is acting on, at the\n * moment they act. Fetching this for every visible row on mount would be\n * a request per page render for an answer that is only ever needed on a\n * click, and this card is one tab of a page a merchant opens to read.\n *\n * A failed check answers \"not blocked\", which is the ordinary case: the\n * dialog then reads exactly as it did before, and the removal still\n * works. Refusing to open the dialog because a supplementary lookup\n * failed would make an outage on an explanation into an outage on the\n * control it explains.\n */\n const alsoPlatform = row.email\n ? await isBlockedPlatformWide(row.email)\n : false\n const accepted = await confirm({\n title: alsoPlatform\n ? 'This address will still be skipped'\n : 'Put this address back on your list?',\n description: alsoPlatform\n ? `${row.email ?? 'This address'} bounced permanently or reported ` +\n 'spam somewhere else in Aglyn, so it is on the platform-wide list ' +\n 'as well as yours. Removing your entry will not start mail ' +\n 'reaching it — contact support to have the platform entry lifted.'\n : `${row.email ?? 'This address'} is suppressed because it ` +\n `${reason === 'bounced' ? 'bounced permanently' : reason === 'marked as spam' ? 'was marked as spam' : reason === 'added by hand' ? 'was added by hand' : 'unsubscribed'}. ` +\n 'Removing the entry means your next campaign will email it again.',\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating on\n // the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n try {\n await deleteDoc(\n doc(firestore, 'hosts', hostId, 'suppressions', row.$id),\n )\n enqueueSnackbar('Removed from the suppression list', {\n variant: 'success',\n persist: false,\n })\n setTotalsEpoch((epoch) => epoch + 1)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n }\n }\n\n const columns: GridColDef<SuppressionRow>[] = [\n {\n field: 'email',\n headerName: 'Address',\n flex: 1,\n minWidth: 240,\n /*\n Entries are keyed by `sha256(email)` because addresses are PII, and the\n address itself is stored in the document. An older row written before\n the address was stored has only its hash — which tells a merchant\n nothing, so it says so rather than displaying 64 hex characters.\n */\n renderCell: ({ row }) =>\n row.email || (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'(address not recorded)'}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Reason',\n width: 230,\n // An absent reason is an unsubscribe, as the chip says.\n valueGetter: (_value, row) => String(row.reason ?? 'unsubscribe'),\n renderCell: ({ row }) => {\n const described = describeReason(row.reason)\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center', height: '100%' }}\n >\n <Chip\n size=\"small\"\n color={described.color}\n variant=\"outlined\"\n label={described.label}\n />\n {/*\n A copy made when sites stopped being one sender. It is an\n entry like any other; the marker says why an address nobody\n on this site acted on is on its list.\n */}\n {row.carriedFromHostId ? (\n <Tooltip\n title={`Copied from ${\n orgMount\n ? orgSiteName(orgMount, row.carriedFromHostId)\n : 'another site'\n } when the two stopped being one sender`}\n >\n <Chip size=\"small\" variant=\"outlined\" label=\"Copied\" />\n </Tooltip>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'since',\n headerName: 'Since',\n width: 140,\n valueGetter: (_value, row) => onDate(row),\n },\n /*\n A SUPPRESSION HAS NO PAGE, so the row does not open one — this is the\n surface's one list whose rows are not a way in to anything. What it does\n have is the one act a merchant performs on an entry, and the trailing\n cluster is where every other list on this surface keeps that: a bare\n `Remove` in the row is a click that puts an address back into mailing\n range, sitting where the reader's eye is running along the row.\n */\n listActionsColumn(\n (row: SuppressionRow) => (\n <ListRowActions\n label={row.email || 'this entry'}\n items={[\n {\n key: 'remove',\n label: 'Remove from the list',\n icon: <MdiIcon path={mdiEmailCheckOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleRemove(row),\n },\n ]}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Suppressions\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#compliance' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {/* Each only for whom the route takes it (AGL-3554). */}\n {transfer?.can('export', { resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowDown.path} size={0.8} />}\n onClick={() =>\n transfer.openExport({\n resource: SUPPRESSIONS_RESOURCE,\n scope: 'host',\n hostId,\n ...(exportFilter ? { filter: exportFilter } : {}),\n })\n }\n >\n {'Export'}\n </Button>\n ) : null}\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport\n target={{ resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }}\n onFinished={() => setTotalsEpoch((epoch) => epoch + 1)}\n />\n {transfer?.can('import', { resource: SUPPRESSIONS_RESOURCE, scope: 'host', hostId }) ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowUp.path} size={0.8} />}\n onClick={() =>\n transfer.openImport({\n resource: SUPPRESSIONS_RESOURCE,\n scope: 'host',\n hostId,\n onFinished: () => setTotalsEpoch((epoch) => epoch + 1),\n })\n }\n >\n {'Import'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => setAdding(true)}\n >\n {'Add'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Addresses this site’s marketing email skips. Someone lands here ' +\n 'by clicking unsubscribe, by bouncing permanently, by marking a ' +\n 'message as spam, or because you added them — this is where the ' +\n 'gap between a campaign’s recipient count and what it actually ' +\n 'sent comes from.'}\n </Typography>\n {entries.length === 0 && !hasMore && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nobody is suppressed. Every address in your audiences is ' +\n 'currently mailable.'}\n </Typography>\n ) : (\n <>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap' }}>\n {totals === null ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Could not read the breakdown. This is not the same as ' +\n 'nobody having bounced.'}\n </Typography>\n ) : (\n Object.entries(totals)\n // A reason nobody has hit is not news, and three chips\n // reading zero make the two that matter harder to find.\n .filter(([, total]) => total > 0)\n .map(([reason, total]) => {\n const described = describeReason(reason)\n return (\n <Chip\n key={reason}\n size=\"small\"\n color={described.color}\n variant=\"outlined\"\n label={`${described.label}: ${total}`}\n />\n )\n })\n )}\n </Stack>\n <ListFilterChips\n fields={SUPPRESSION_LIST_QUERY.fields}\n headers={SUPPRESSION_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SUPPRESSION_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Suppressed addresses\"\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(\n columns as GridColDef[],\n SUPPRESSION_LIST_QUERY.fields,\n SUPPRESSION_FILTER_OPTIONS,\n SUPPRESSION_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the headers go to the query; the\n // grid neither filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No suppressions match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={entries.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n // The collection's real size, so the footer's count line says\n // \"1–10 of 812\" rather than \"of more than 10\" — the aggregate\n // above already knows it, and it is the same number the chips\n // are a breakdown of. A filtered list's size is unknown.\n count={\n totals && !filtering\n ? Object.values(totals).reduce((a, b) => a + b, 0)\n : undefined\n }\n />\n </>\n )}\n </Stack>\n {/*\n A DRAWER, not a form stacked above the table.\n Creating is a drawer and picking is a dialog, which is how Screens,\n Components, Layouts and Templates all take a name before they create\n one; the chrome here is the same `NavigationDrawerComponent` those go\n through, composed directly because the console's wrapper around it\n lives in an application a plugin library may not import.\n */}\n <NavigationDrawerComponent\n open={adding}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={() => setAdding(false)}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={() => setAdding(false)}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'Stop emailing an address'}\n </Typography>\n </>\n }\n appBarRight={\n <Button\n variant=\"outlined\"\n color=\"inherit\"\n onClick={() => setAdding(false)}\n >\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Use this when somebody asks you to stop emailing them by ' +\n 'reply, by phone, or in person. They stay on your audiences ' +\n 'and keep every record you hold about them — this only stops ' +\n 'this site’s marketing email reaching them.'}\n </Typography>\n <TextField\n label=\"Email addresses\"\n value={addInput}\n onChange={(event) => setAddInput(event.target.value)}\n multiline\n minRows={3}\n fullWidth\n autoFocus\n helperText={\n 'One per line, or separated by commas. Up to 50 at a time.'\n }\n />\n <TextField\n label=\"Note (optional)\"\n value={addNote}\n onChange={(event) => setAddNote(event.target.value)}\n fullWidth\n helperText={\n 'How the request reached you. Kept with the entry as the ' +\n 'record that it was honored.'\n }\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Alert severity=\"info\">\n {'Order confirmations, booking reminders and password resets ' +\n 'are unaffected. Somebody who asked to stop hearing from ' +\n 'your marketing still gets their receipt.'}\n </Alert>\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n disabled={busy || !addInput.trim()}\n onClick={() => void handleAdd()}\n >\n {'Add to suppression list'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n </CardDisplay>\n )\n}\nSuppressionsCard.displayName = 'SuppressionsCard'\n\nexport default SuppressionsCard\n"],"names":["TransferResumeImport","pluginDocsHelp","useTransferLauncher","ICON_VARIANT_CLOSE","mdiEmailCheckOutline","mdiTrayArrowDown","mdiTrayArrowUp","CardDisplay","Container","MdiIcon","SrOnly","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","NavigationDrawerComponent","useSnackbar","Alert","Chip","IconButton","Stack","TextField","Tooltip","Typography","Button","collection","deleteDoc","doc","useCallback","useEffect","useMemo","useState","useFirestore","useUser","useListQuery","useListColumnSort","authorizedFetch","SUPPRESSION_FILTER_HEADERS","SUPPRESSION_LIST_QUERY","describeSuppressionReason","describeReason","readSuppressionTotals","SUPPRESSION_REASONS","orgSiteName","useEmailOrgMount","SUPPRESSIONS_RESOURCE","SUPPRESSION_FILTER_OPTIONS","reason","Object","entries","map","value","described","label","SUPPRESSION_SELECT_FIELDS","onDate","row","seconds","createdAt","suppressedAt","Date","toISOString","slice","SuppressionsCard","props","hostId","firestore","data","user","enqueueSnackbar","confirm","orgMount","adding","setAdding","addInput","setAddInput","addNote","setAddNote","busy","setBusy","gridFilter","selectFields","askedSort","setAskedSort","sorts","rows","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","sort","deps","idField","columnSort","defaultSort","onSortChange","orderBy","filtering","length","some","word","trim","transfer","exportFilter","filters","refusals","refused","fields","headers","options","totalsEpoch","setTotalsEpoch","totals","setTotals","active","then","read","catch","handleAdd","typed","response","method","body","JSON","stringify","emails","note","payload","json","ok","error","variant","results","added","Number","rejected","filter","result","refusal","input","join","persist","epoch","console","isBlockedPlatformWide","email","platform","handleRemove","toLowerCase","alsoPlatform","accepted","title","description","confirmationText","confirmationButtonProps","color","$id","columns","field","headerName","flex","minWidth","renderCell","width","valueGetter","_value","String","direction","spacing","sx","alignItems","height","size","carriedFromHostId","items","key","icon","path","destructive","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","can","resource","scope","startIcon","openExport","target","onFinished","openImport","useFlexGap","flexWrap","total","onChange","setClauses","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","rowCount","onPageChange","onPageSizeChange","count","values","reduce","a","b","undefined","open","onClose","AppBarProps","appBarLeft","edge","mr","component","appBarRight","gutterY","event","multiline","minRows","fullWidth","autoFocus","helperText","slotProps","htmlInput","maxLength","severity","justifyContent","disabled","displayName"],"mappings":"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"}