@aglyn/plugins-email 1.0.0-beta.198 → 1.0.0-beta.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/email-screens-card.d.ts +8 -9
- package/src/lib/components/email-screens-card.js +83 -100
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/email-template-detail.js +3 -2
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/list-edit-card.js +2 -2
- package/src/lib/components/list-edit-card.js.map +1 -1
- package/src/lib/components/list-members-panel.js +72 -93
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/lists-card.js +70 -77
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.d.ts +10 -6
- package/src/lib/components/org-email-templates-card.js +145 -90
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/suppressions-card.js +73 -69
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/constants/list-queries.d.ts +34 -0
- package/src/lib/constants/list-queries.js +256 -0
- package/src/lib/constants/list-queries.js.map +1 -0
- package/src/lib/server-email-drafts.js +3 -4
- package/src/lib/server-email-drafts.js.map +1 -1
- package/src/lib/server-suppressions.js +3 -1
- package/src/lib/server-suppressions.js.map +1 -1
- package/src/lib/server.js +3 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/email-template-soft-delete.d.ts +39 -0
- package/src/lib/utils/email-template-soft-delete.js +41 -0
- package/src/lib/utils/email-template-soft-delete.js.map +1 -0
|
@@ -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 {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\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 {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\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 {\n collection,\n doc,\n query,\n Timestamp,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\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-3317).\n * The table holds every template its page of sites read, so it answers the\n * panel over all of them before the footer's slice. Site offers the sites on\n * that page; Origin reads `originKey`, the provenance the column draws.\n */\nconst ORG_TEMPLATE_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text', 'templateName'),\n inMemoryListField('site', 'select', 'hostId'),\n inMemoryListField('origin', 'select', 'originKey'),\n]\nconst ORG_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n site: 'Site',\n origin: 'Origin',\n}\nconst ORIGIN_OPTIONS = [\n { value: 'local', label: 'Yours' },\n { value: 'installed', label: 'Installed' },\n]\nconst ORG_TEMPLATE_SEARCH_FIELDS = ['templateName', 'siteName'] as const\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 * The equality filter is on the QUERY, so a site with a thousand pages reads\n * its emails and nothing else. `collectionCeiling` orders on the document\n * name, which an equality predicate composes onto without a composite index.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, onRead } = props\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n collectionCeiling(\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n where('kind', '==', 'email'),\n ),\n ORG_TEMPLATE_CEILING,\n ),\n [firestore, hostId],\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 capped at {@link ORG_TEMPLATE_CEILING} templates.\n * The shared footer pages the templates in hand, as a site's own list does;\n * an organization with more sites than one page chooses which page of sites\n * is read with the Sites filter above the table, and only that page's sites\n * are read. Both caps say so on screen when they bite, so a missing template\n * is never mistaken for one that does not 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 { sites } = sitePage\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.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? [])\n .filter((screen) => !screen['deletedAt'])\n .map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n templateName: String(screen['displayName'] ?? 'Untitled template'),\n siteName: orgSiteName(mount, site.id),\n originKey:\n templateProvenance(screen).origin === 'installed' ? 'installed' : 'local',\n })),\n )\n .sort(\n (a, b) =>\n String(a['displayName'] ?? '').localeCompare(\n String(b['displayName'] ?? ''),\n ) ||\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 templates in hand, a page at a time on the shared footer. Choosing\n * another page of sites starts the templates from their first page: page\n * four of one set of sites is not a position in another.\n */\n const filterOptions = useMemo(\n () => ({\n site: sites.map((site) => ({ value: site.id, label: orgSiteName(mount, site.id) })),\n origin: ORIGIN_OPTIONS,\n }),\n [sites, mount],\n )\n const templateFilter = useListRowsFilter({\n rows,\n fields: ORG_TEMPLATE_FILTER_FIELDS,\n options: filterOptions,\n headers: ORG_TEMPLATE_FILTER_HEADERS,\n search: ORG_TEMPLATE_SEARCH_FIELDS,\n })\n const matched = templateFilter.rows\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const searchKey = templateFilter.gridFilter.searchWords.join(' ')\n // A narrowed list starts again at its first page.\n useEffect(\n () => setPage(0),\n [sitePage.page, templateFilter.gridFilter.clauses, searchKey],\n )\n const shown = useMemo(\n () => matched.slice(page * pageSize, page * pageSize + pageSize),\n [matched, 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 { deletedAt: Timestamp.now() },\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) => row.originKey,\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n /*==========================================\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 key={site.id} hostId={site.id} onRead={onRead} />\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 ? (\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 ? (\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 {...templateFilter.chipsProps} />\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={templateFilter.filterColumns(columns)}\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 are the grid's; the table answers\n // them over every template it read, before the footer's slice.\n {...templateFilter.gridProps}\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={matched.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} templates, and only the first of them ` +\n '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","ListRowActions","ListTable","listActionsColumn","inMemoryListField","useListRowsFilter","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","collection","doc","query","Timestamp","updateDoc","where","useRouter","useCallback","useEffect","useMemo","useState","templateProvenance","createEmailScreen","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","ORG_TEMPLATE_FILTER_FIELDS","ORG_TEMPLATE_FILTER_HEADERS","displayName","site","origin","ORIGIN_OPTIONS","value","label","ORG_TEMPLATE_SEARCH_FIELDS","SiteTemplatesReader","props","hostId","onRead","firestore","data","status","idField","window","rows","truncated","settled","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","sites","reads","setReads","read","current","previous","flatMap","id","filter","screen","map","$id","screenId","String","templateName","siteName","originKey","sort","a","b","localeCompare","every","crowded","filterOptions","templateFilter","fields","options","headers","search","matched","page","setPage","pageSize","setPageSize","searchKey","gridFilter","searchWords","join","clauses","shown","slice","sitePageCount","Math","ceil","count","row","templateHref","siteSubdomain","hosts","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","host","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","deletedAt","now","rowActions","editor","unlinkedReason","key","icon","path","size","href","undefined","disabled","disabledReason","destructive","onClick","columns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","event","stopPropagation","width","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","length","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","chipsProps","aria-label","filterColumns","rowHeight","hideFooter","gridProps","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 } 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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"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
18
|
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
19
|
import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
|
|
@@ -22,10 +21,11 @@ import { mdiEmailCheckOutline } from "@aglyn/shared-data-mdi";
|
|
|
22
21
|
import { CardDisplay, Container, MdiIcon, SrOnly, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
23
22
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
24
23
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
24
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
25
25
|
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
26
|
-
import {
|
|
26
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
27
27
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
28
|
-
import {
|
|
28
|
+
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
29
29
|
/*
|
|
30
30
|
* The shared drawer, reached by its own path.
|
|
31
31
|
*
|
|
@@ -40,41 +40,27 @@ import { usePagedRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-paged-rows-fi
|
|
|
40
40
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
41
41
|
import { Alert, Chip, IconButton, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
|
42
42
|
import Button from "@mui/material/Button";
|
|
43
|
-
import { collection, deleteDoc, doc
|
|
43
|
+
import { collection, deleteDoc, doc } from "firebase/firestore";
|
|
44
44
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
45
|
-
import { useFirestore,
|
|
45
|
+
import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
|
|
46
|
+
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
46
47
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
48
|
+
import { SUPPRESSION_FILTER_HEADERS, SUPPRESSION_LIST_QUERY } from "../constants/list-queries.js";
|
|
47
49
|
import { describeSuppressionReason as describeReason, readSuppressionTotals, SUPPRESSION_REASONS } from "./suppression-totals.js";
|
|
48
50
|
import { orgSiteName, useEmailOrgMount } from "./email-org-mount.js";
|
|
49
51
|
/*
|
|
50
|
-
* What the suppressions grid's Filters panel offers
|
|
51
|
-
*
|
|
52
|
-
* the
|
|
53
|
-
|
|
54
|
-
* the column and the breakdown describe it.
|
|
55
|
-
*/ const SUPPRESSION_FILTER_FIELDS = [
|
|
56
|
-
inMemoryListField('email', 'text'),
|
|
57
|
-
inMemoryListField('reason', 'select', 'reasonKey')
|
|
58
|
-
];
|
|
59
|
-
const SUPPRESSION_FILTER_HEADERS = {
|
|
60
|
-
email: 'Address',
|
|
61
|
-
reason: 'Reason'
|
|
62
|
-
};
|
|
63
|
-
const SUPPRESSION_FILTER_OPTIONS = {
|
|
52
|
+
* What the suppressions grid's Filters panel offers, every clause and the
|
|
53
|
+
* search on the list's query (AGL-3321): see `SUPPRESSION_LIST_QUERY`. The
|
|
54
|
+
* Reason choices are the stored reasons, by the words the column uses.
|
|
55
|
+
*/ const SUPPRESSION_FILTER_OPTIONS = {
|
|
64
56
|
reason: Object.entries(SUPPRESSION_REASONS).map(([value, described])=>({
|
|
65
57
|
value,
|
|
66
58
|
label: described.label
|
|
67
59
|
}))
|
|
68
60
|
};
|
|
69
|
-
const
|
|
70
|
-
'
|
|
61
|
+
const SUPPRESSION_SELECT_FIELDS = [
|
|
62
|
+
'reason'
|
|
71
63
|
];
|
|
72
|
-
const withReasonKey = (row)=>{
|
|
73
|
-
var _row_reason;
|
|
74
|
-
return _extends({}, row, {
|
|
75
|
-
reasonKey: String((_row_reason = row.reason) != null ? _row_reason : 'unsubscribe')
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
64
|
/**
|
|
79
65
|
* `YYYY-MM-DD` from a Firestore timestamp shape, or an em dash.
|
|
80
66
|
*
|
|
@@ -145,7 +131,8 @@ const withReasonKey = (row)=>{
|
|
|
145
131
|
const [addNote, setAddNote] = useState('');
|
|
146
132
|
const [busy, setBusy] = useState(false);
|
|
147
133
|
/*
|
|
148
|
-
* The
|
|
134
|
+
* The page IS the query, ordered by the server (AGL-2501, AGL-2292), and
|
|
135
|
+
* every filter and search word is a predicate on it (AGL-3321).
|
|
149
136
|
*
|
|
150
137
|
* This was `limit(500)` with no `orderBy`, sorted by date in the browser.
|
|
151
138
|
* Firestore answers an unordered limit in DOCUMENT-ID order, and an entry
|
|
@@ -163,34 +150,30 @@ const withReasonKey = (row)=>{
|
|
|
163
150
|
* one. `suppressedAt` would NOT be safe: it is absent on every entry
|
|
164
151
|
* written before AGL-1918, and `orderBy` drops documents that lack the
|
|
165
152
|
* field rather than mis-sorting them.
|
|
166
|
-
*/ const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
153
|
+
*/ const gridFilter = useListGridFilter({
|
|
154
|
+
selectFields: SUPPRESSION_SELECT_FIELDS
|
|
155
|
+
});
|
|
156
|
+
const { rows: entries, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
157
|
+
collection: collection(firestore, 'hosts', hostId, 'suppressions'),
|
|
158
|
+
declaration: SUPPRESSION_LIST_QUERY,
|
|
159
|
+
request: {
|
|
160
|
+
clauses: gridFilter.clauses,
|
|
161
|
+
search: gridFilter.searchWords
|
|
162
|
+
},
|
|
163
|
+
deps: [
|
|
164
|
+
firestore,
|
|
165
|
+
hostId
|
|
166
|
+
],
|
|
170
167
|
idField: '$id'
|
|
171
168
|
});
|
|
172
|
-
const
|
|
173
|
-
|
|
169
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
170
|
+
const refusals = useMemo(()=>listQueryRefusals(plan.refused, {
|
|
171
|
+
fields: SUPPRESSION_LIST_QUERY.fields,
|
|
172
|
+
headers: SUPPRESSION_FILTER_HEADERS,
|
|
173
|
+
options: SUPPRESSION_FILTER_OPTIONS
|
|
174
|
+
}), [
|
|
175
|
+
plan.refused
|
|
174
176
|
]);
|
|
175
|
-
const entryPage = useMemo(()=>entryRows.map(withReasonKey), [
|
|
176
|
-
entryRows
|
|
177
|
-
]);
|
|
178
|
-
const entryFilter = usePagedRowsFilter({
|
|
179
|
-
data: entryWindow,
|
|
180
|
-
rows: entryPage,
|
|
181
|
-
hasMore,
|
|
182
|
-
page,
|
|
183
|
-
setPage,
|
|
184
|
-
pageSize,
|
|
185
|
-
setPageSize
|
|
186
|
-
}, {
|
|
187
|
-
fields: SUPPRESSION_FILTER_FIELDS,
|
|
188
|
-
options: SUPPRESSION_FILTER_OPTIONS,
|
|
189
|
-
headers: SUPPRESSION_FILTER_HEADERS,
|
|
190
|
-
search: SUPPRESSION_SEARCH_FIELDS
|
|
191
|
-
});
|
|
192
|
-
// The rows on screen: the page, or the page of the matches.
|
|
193
|
-
const entries = entryFilter.rows;
|
|
194
177
|
/*==========================================
|
|
195
178
|
* THE BREAKDOWN IS A SERVER AGGREGATE, not a tally of the page.
|
|
196
179
|
*
|
|
@@ -403,7 +386,11 @@ const withReasonKey = (row)=>{
|
|
|
403
386
|
field: 'reason',
|
|
404
387
|
headerName: 'Reason',
|
|
405
388
|
width: 230,
|
|
406
|
-
|
|
389
|
+
// An absent reason is an unsubscribe, as the chip says.
|
|
390
|
+
valueGetter: (_value, row)=>{
|
|
391
|
+
var _row_reason;
|
|
392
|
+
return String((_row_reason = row.reason) != null ? _row_reason : 'unsubscribe');
|
|
393
|
+
},
|
|
407
394
|
renderCell: ({ row })=>{
|
|
408
395
|
const described = describeReason(row.reason);
|
|
409
396
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -488,7 +475,7 @@ const withReasonKey = (row)=>{
|
|
|
488
475
|
color: "text.secondary",
|
|
489
476
|
children: 'Addresses this site’s marketing email skips. Someone lands here ' + 'by clicking unsubscribe, by bouncing permanently, by marking a ' + 'message as spam, or because you added them — this is where the ' + 'gap between a campaign’s recipient count and what it actually ' + 'sent comes from.'
|
|
490
477
|
}),
|
|
491
|
-
|
|
478
|
+
entries.length === 0 && !hasMore && !filtering ? /*#__PURE__*/ _jsx(Typography, {
|
|
492
479
|
variant: "body2",
|
|
493
480
|
color: "text.secondary",
|
|
494
481
|
children: 'Nobody is suppressed. Every address in your audiences is ' + 'currently mailable.'
|
|
@@ -516,29 +503,46 @@ const withReasonKey = (row)=>{
|
|
|
516
503
|
}, reason);
|
|
517
504
|
})
|
|
518
505
|
}),
|
|
519
|
-
/*#__PURE__*/ _jsx(ListFilterChips,
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
506
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
507
|
+
fields: SUPPRESSION_LIST_QUERY.fields,
|
|
508
|
+
headers: SUPPRESSION_FILTER_HEADERS,
|
|
509
|
+
clauses: gridFilter.clauses,
|
|
510
|
+
onChange: gridFilter.setClauses,
|
|
511
|
+
options: SUPPRESSION_FILTER_OPTIONS
|
|
512
|
+
}),
|
|
513
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
514
|
+
refused: refusals,
|
|
515
|
+
notices: plan.notices
|
|
516
|
+
}),
|
|
517
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
526
518
|
"aria-label": "Suppressed addresses",
|
|
527
519
|
rows: entries,
|
|
528
|
-
columns:
|
|
520
|
+
columns: listFilterGridColumns(columns, SUPPRESSION_LIST_QUERY.fields, SUPPRESSION_FILTER_OPTIONS, SUPPRESSION_FILTER_HEADERS),
|
|
529
521
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
530
522
|
// Paged by the footer below, so the grid must not also slice.
|
|
531
|
-
hideFooter: true
|
|
532
|
-
|
|
523
|
+
hideFooter: true,
|
|
524
|
+
// The panel and the search go to the query; the grid neither
|
|
525
|
+
// filters nor sorts the page it holds.
|
|
526
|
+
filterMode: "server",
|
|
527
|
+
filterModel: gridFilter.filterModel,
|
|
528
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
529
|
+
quickFilter: true,
|
|
530
|
+
disableColumnSorting: true,
|
|
533
531
|
noRowsLabel: "No suppressions match these filters"
|
|
534
|
-
})
|
|
535
|
-
/*#__PURE__*/ _jsx(ListPagination,
|
|
532
|
+
}),
|
|
533
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
534
|
+
page: page,
|
|
535
|
+
pageSize: pageSize,
|
|
536
|
+
rowCount: entries.length,
|
|
537
|
+
hasMore: hasMore,
|
|
538
|
+
onPageChange: setPage,
|
|
539
|
+
onPageSizeChange: setPageSize,
|
|
536
540
|
// The collection's real size, so the footer's count line says
|
|
537
541
|
// "1–10 of 812" rather than "of more than 10" — the aggregate
|
|
538
542
|
// above already knows it, and it is the same number the chips
|
|
539
543
|
// are a breakdown of. A filtered list's size is unknown.
|
|
540
|
-
count: totals && !
|
|
541
|
-
})
|
|
544
|
+
count: totals && !filtering ? Object.values(totals).reduce((a, b)=>a + b, 0) : undefined
|
|
545
|
+
})
|
|
542
546
|
]
|
|
543
547
|
})
|
|
544
548
|
]
|