@aglyn/plugins-email 1.0.0-beta.188 → 1.0.0-beta.190

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.
@@ -19,8 +19,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
19
19
  import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
20
20
  import { mdiDeleteOutline, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
21
21
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
23
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
24
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
25
+ import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
26
+ import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
24
27
  import { TABLE_PAGE_SIZE_DEFAULT, TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
25
28
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
29
  import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
@@ -39,6 +42,35 @@ import { OrgSiteSelect, orgSiteEmailsPath, orgSiteName, orgSitePageLabel, useEma
39
42
  * once: a site with more than this is named under the table, with a link to
40
43
  * its own Templates page, which reads its whole set.
41
44
  */ export const ORG_TEMPLATE_CEILING = 50;
45
+ /*
46
+ * What the organization's templates grid's Filters panel offers (AGL-3317).
47
+ * The table holds every template its page of sites read, so it answers the
48
+ * panel over all of them before the footer's slice. Site offers the sites on
49
+ * that page; Origin reads `originKey`, the provenance the column draws.
50
+ */ const ORG_TEMPLATE_FILTER_FIELDS = [
51
+ inMemoryListField('displayName', 'text', 'templateName'),
52
+ inMemoryListField('site', 'select', 'hostId'),
53
+ inMemoryListField('origin', 'select', 'originKey')
54
+ ];
55
+ const ORG_TEMPLATE_FILTER_HEADERS = {
56
+ displayName: 'Template',
57
+ site: 'Site',
58
+ origin: 'Origin'
59
+ };
60
+ const ORIGIN_OPTIONS = [
61
+ {
62
+ value: 'local',
63
+ label: 'Yours'
64
+ },
65
+ {
66
+ value: 'installed',
67
+ label: 'Installed'
68
+ }
69
+ ];
70
+ const ORG_TEMPLATE_SEARCH_FIELDS = [
71
+ 'templateName',
72
+ 'siteName'
73
+ ];
42
74
  /**
43
75
  * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.
44
76
  *
@@ -133,11 +165,17 @@ function OrgEmailTemplatesTable(props) {
133
165
  */ const rows = useMemo(()=>sites.flatMap((site)=>{
134
166
  var _ref;
135
167
  var _reads_site_id;
136
- return ((_ref = (_reads_site_id = reads[site.id]) == null ? void 0 : _reads_site_id.rows) != null ? _ref : []).filter((screen)=>!screen['deletedAt']).map((screen)=>_extends({}, screen, {
168
+ return ((_ref = (_reads_site_id = reads[site.id]) == null ? void 0 : _reads_site_id.rows) != null ? _ref : []).filter((screen)=>!screen['deletedAt']).map((screen)=>{
169
+ var _screen_displayName;
170
+ return _extends({}, screen, {
137
171
  $id: `${site.id}:${screen['$id']}`,
138
172
  screenId: String(screen['$id']),
139
- hostId: site.id
140
- }));
173
+ hostId: site.id,
174
+ templateName: String((_screen_displayName = screen['displayName']) != null ? _screen_displayName : 'Untitled template'),
175
+ siteName: orgSiteName(mount, site.id),
176
+ originKey: templateProvenance(screen).origin === 'installed' ? 'installed' : 'local'
177
+ });
178
+ });
141
179
  }).sort((a, b)=>{
142
180
  var _a_displayName, _b_displayName;
143
181
  return String((_a_displayName = a['displayName']) != null ? _a_displayName : '').localeCompare(String((_b_displayName = b['displayName']) != null ? _b_displayName : '')) || orgSiteName(mount, a.hostId).localeCompare(orgSiteName(mount, b.hostId));
@@ -158,13 +196,35 @@ function OrgEmailTemplatesTable(props) {
158
196
  * The templates in hand, a page at a time on the shared footer. Choosing
159
197
  * another page of sites starts the templates from their first page: page
160
198
  * four of one set of sites is not a position in another.
161
- */ const [page, setPage] = useState(0);
199
+ */ const filterOptions = useMemo(()=>({
200
+ site: sites.map((site)=>({
201
+ value: site.id,
202
+ label: orgSiteName(mount, site.id)
203
+ })),
204
+ origin: ORIGIN_OPTIONS
205
+ }), [
206
+ sites,
207
+ mount
208
+ ]);
209
+ const templateFilter = useListRowsFilter({
210
+ rows,
211
+ fields: ORG_TEMPLATE_FILTER_FIELDS,
212
+ options: filterOptions,
213
+ headers: ORG_TEMPLATE_FILTER_HEADERS,
214
+ search: ORG_TEMPLATE_SEARCH_FIELDS
215
+ });
216
+ const matched = templateFilter.rows;
217
+ const [page, setPage] = useState(0);
162
218
  const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
219
+ const searchKey = templateFilter.gridFilter.searchWords.join(' ');
220
+ // A narrowed list starts again at its first page.
163
221
  useEffect(()=>setPage(0), [
164
- sitePage.page
222
+ sitePage.page,
223
+ templateFilter.gridFilter.clauses,
224
+ searchKey
165
225
  ]);
166
- const shown = useMemo(()=>rows.slice(page * pageSize, page * pageSize + pageSize), [
167
- rows,
226
+ const shown = useMemo(()=>matched.slice(page * pageSize, page * pageSize + pageSize), [
227
+ matched,
168
228
  page,
169
229
  pageSize
170
230
  ]);
@@ -263,15 +323,16 @@ function OrgEmailTemplatesTable(props) {
263
323
  field: 'site',
264
324
  headerName: 'Site',
265
325
  width: 170,
266
- valueGetter: (_value, row)=>orgSiteName(mount, row.hostId)
326
+ valueGetter: (_value, row)=>row.hostId,
327
+ renderCell: ({ row })=>row.siteName
267
328
  },
268
329
  {
269
330
  // Whose template this is, as on a site's own list.
270
331
  field: 'origin',
271
332
  headerName: 'Origin',
272
333
  width: 130,
273
- valueGetter: (_value, row)=>templateProvenance(row).origin === 'installed' ? 'Installed' : 'Yours',
274
- renderCell: ({ value })=>value === 'Installed' ? /*#__PURE__*/ _jsx(Chip, {
334
+ valueGetter: (_value, row)=>row.originKey,
335
+ renderCell: ({ value })=>value === 'installed' ? /*#__PURE__*/ _jsx(Chip, {
275
336
  size: "small",
276
337
  label: "Installed"
277
338
  }) : /*#__PURE__*/ _jsx(Typography, {
@@ -382,23 +443,26 @@ function OrgEmailTemplatesTable(props) {
382
443
  children: settled ? 'None of these sites has an email template yet. Design one ' + 'here, then send it from a campaign — a new template opens in ' + 'the besigner with email-safe components only.' : 'Loading templates…'
383
444
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
384
445
  children: [
385
- /*#__PURE__*/ _jsx(ListTable, {
446
+ /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, templateFilter.chipsProps)),
447
+ /*#__PURE__*/ _jsx(ListTable, _extends({
386
448
  "aria-label": "Email templates",
387
449
  rows: shown,
388
- columns: columns,
450
+ columns: templateFilter.filterColumns(columns),
389
451
  rowHeight: TABLE_ROW_HEIGHT,
390
452
  // Paged by the footer below, so the grid must not also slice.
391
- hideFooter: true,
453
+ hideFooter: true
454
+ }, templateFilter.gridProps, {
455
+ noRowsLabel: "No templates match these filters",
392
456
  onOpen: (_id, row)=>{
393
457
  const href = templateHref(row);
394
458
  if (href) router.push(href);
395
459
  }
396
- }),
460
+ })),
397
461
  /*#__PURE__*/ _jsx(ListPagination, {
398
462
  page: page,
399
463
  pageSize: pageSize,
400
464
  rowCount: shown.length,
401
- count: rows.length,
465
+ count: matched.length,
402
466
  onPageChange: setPage,
403
467
  onPageSizeChange: setPageSize
404
468
  })
@@ -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 { 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 {\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/** 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 })),\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 [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n useEffect(() => setPage(0), [sitePage.page])\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 { 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) => orgSiteName(mount, row.hostId),\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' : 'Yours',\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: '#create-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.\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 <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={columns}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\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} 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","ListPagination","ListRowActions","ListTable","listActionsColumn","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","SiteTemplatesReader","props","hostId","onRead","firestore","data","status","idField","window","rows","truncated","settled","displayName","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","sites","reads","setReads","read","current","previous","flatMap","site","id","filter","screen","map","$id","screenId","String","sort","a","b","localeCompare","every","crowded","page","setPage","pageSize","setPageSize","shown","slice","sitePageCount","Math","ceil","count","templateName","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","label","icon","path","size","href","undefined","disabled","disabledReason","destructive","onClick","columns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","value","event","stopPropagation","width","origin","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","aria-label","rowHeight","hideFooter","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 {\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: '#create-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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"}
@@ -14,14 +14,18 @@
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";
17
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
19
  import { pluginDocsHelp } from "@aglyn/aglyn";
19
20
  import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
20
21
  import { mdiEmailCheckOutline } from "@aglyn/shared-data-mdi";
21
22
  import { CardDisplay, Container, MdiIcon, SrOnly, useConfirmationContext } from "@aglyn/shared-ui-jsx";
23
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
24
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
25
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
26
+ import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
24
27
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
28
+ import { usePagedRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter";
25
29
  /*
26
30
  * The shared drawer, reached by its own path.
27
31
  *
@@ -37,10 +41,39 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
37
41
  import { Alert, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
38
42
  import Button from "@mui/material/Button";
39
43
  import { collection, deleteDoc, doc, limit, orderBy, query } from "firebase/firestore";
40
- import { useCallback, useEffect, useState } from "react";
44
+ import { useCallback, useEffect, useMemo, useState } from "react";
41
45
  import { useFirestore, usePagedCollection, useUser } from "@aglyn/tenant-feature-instance";
42
46
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
43
- import { describeSuppressionReason as describeReason, readSuppressionTotals } from "./suppression-totals.js";
47
+ import { describeSuppressionReason as describeReason, readSuppressionTotals, SUPPRESSION_REASONS } from "./suppression-totals.js";
48
+ /*
49
+ * What the suppressions grid's Filters panel offers (AGL-3317). The list is a
50
+ * paged listener, so a filter matches over what its window has read, which
51
+ * the first filter widens (`usePagedRowsFilter`). Reason reads `reasonKey`,
52
+ * the stored reason with an absent one read as an unsubscribe, which is how
53
+ * the column and the breakdown describe it.
54
+ */ const SUPPRESSION_FILTER_FIELDS = [
55
+ inMemoryListField('email', 'text'),
56
+ inMemoryListField('reason', 'select', 'reasonKey')
57
+ ];
58
+ const SUPPRESSION_FILTER_HEADERS = {
59
+ email: 'Address',
60
+ reason: 'Reason'
61
+ };
62
+ const SUPPRESSION_FILTER_OPTIONS = {
63
+ reason: Object.entries(SUPPRESSION_REASONS).map(([value, described])=>({
64
+ value,
65
+ label: described.label
66
+ }))
67
+ };
68
+ const SUPPRESSION_SEARCH_FIELDS = [
69
+ 'email'
70
+ ];
71
+ const withReasonKey = (row)=>{
72
+ var _row_reason;
73
+ return _extends({}, row, {
74
+ reasonKey: String((_row_reason = row.reason) != null ? _row_reason : 'unsubscribe')
75
+ });
76
+ };
44
77
  /**
45
78
  * `YYYY-MM-DD` from a Firestore timestamp shape, or an em dash.
46
79
  *
@@ -127,12 +160,34 @@ import { describeSuppressionReason as describeReason, readSuppressionTotals } fr
127
160
  * one. `suppressedAt` would NOT be safe: it is absent on every entry
128
161
  * written before AGL-1918, and `orderBy` drops documents that lack the
129
162
  * field rather than mis-sorting them.
130
- */ const { rows: entries, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, 'hosts', hostId, 'suppressions'), orderBy('createdAt', 'desc'), limit(pageLimit)), [
163
+ */ const { data: entryData, rows: entryRows, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, 'hosts', hostId, 'suppressions'), orderBy('createdAt', 'desc'), limit(pageLimit)), [
131
164
  firestore,
132
165
  hostId
133
166
  ], {
134
167
  idField: '$id'
135
168
  });
169
+ const entryWindow = useMemo(()=>entryData == null ? void 0 : entryData.map(withReasonKey), [
170
+ entryData
171
+ ]);
172
+ const entryPage = useMemo(()=>entryRows.map(withReasonKey), [
173
+ entryRows
174
+ ]);
175
+ const entryFilter = usePagedRowsFilter({
176
+ data: entryWindow,
177
+ rows: entryPage,
178
+ hasMore,
179
+ page,
180
+ setPage,
181
+ pageSize,
182
+ setPageSize
183
+ }, {
184
+ fields: SUPPRESSION_FILTER_FIELDS,
185
+ options: SUPPRESSION_FILTER_OPTIONS,
186
+ headers: SUPPRESSION_FILTER_HEADERS,
187
+ search: SUPPRESSION_SEARCH_FIELDS
188
+ });
189
+ // The rows on screen: the page, or the page of the matches.
190
+ const entries = entryFilter.rows;
136
191
  /*==========================================
137
192
  * THE BREAKDOWN IS A SERVER AGGREGATE, not a tally of the page.
138
193
  *
@@ -345,7 +400,7 @@ import { describeSuppressionReason as describeReason, readSuppressionTotals } fr
345
400
  field: 'reason',
346
401
  headerName: 'Reason',
347
402
  width: 170,
348
- valueGetter: (_value, row)=>describeReason(row.reason).label,
403
+ valueGetter: (_value, row)=>row.reasonKey,
349
404
  renderCell: ({ row })=>{
350
405
  const described = describeReason(row.reason);
351
406
  return /*#__PURE__*/ _jsx(Chip, {
@@ -412,7 +467,7 @@ import { describeSuppressionReason as describeReason, readSuppressionTotals } fr
412
467
  color: "text.secondary",
413
468
  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.'
414
469
  }),
415
- entries.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
470
+ entryRows.length === 0 && !hasMore && !entryFilter.filtering ? /*#__PURE__*/ _jsx(Typography, {
416
471
  variant: "body2",
417
472
  color: "text.secondary",
418
473
  children: 'Nobody is suppressed. Every address in your audiences is ' + 'currently mailable.'
@@ -440,27 +495,29 @@ import { describeSuppressionReason as describeReason, readSuppressionTotals } fr
440
495
  }, reason);
441
496
  })
442
497
  }),
443
- /*#__PURE__*/ _jsx(ListTable, {
498
+ /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, entryFilter.chipsProps)),
499
+ entryFilter.filtering && hasMore ? /*#__PURE__*/ _jsx(Typography, {
500
+ variant: "caption",
501
+ color: "text.secondary",
502
+ children: `Filtering the ${entryFilter.read} suppressions read so far — the next page reads more.`
503
+ }) : null,
504
+ /*#__PURE__*/ _jsx(ListTable, _extends({
444
505
  "aria-label": "Suppressed addresses",
445
506
  rows: entries,
446
- columns: columns,
507
+ columns: entryFilter.filterColumns(columns),
447
508
  rowHeight: TABLE_ROW_HEIGHT,
448
509
  // Paged by the footer below, so the grid must not also slice.
449
510
  hideFooter: true
450
- }),
451
- /*#__PURE__*/ _jsx(ListPagination, {
452
- page: page,
453
- pageSize: pageSize,
454
- rowCount: entries.length,
455
- hasMore: hasMore,
511
+ }, entryFilter.gridProps, {
512
+ noRowsLabel: "No suppressions match these filters"
513
+ })),
514
+ /*#__PURE__*/ _jsx(ListPagination, _extends({}, entryFilter.pagination, {
456
515
  // The collection's real size, so the footer's count line says
457
516
  // "1–10 of 812" rather than "of more than 10" — the aggregate
458
517
  // above already knows it, and it is the same number the chips
459
- // are a breakdown of.
460
- count: totals ? Object.values(totals).reduce((a, b)=>a + b, 0) : undefined,
461
- onPageChange: setPage,
462
- onPageSizeChange: setPageSize
463
- })
518
+ // are a breakdown of. A filtered list's size is unknown.
519
+ count: totals && !entryFilter.filtering ? Object.values(totals).reduce((a, b)=>a + b, 0) : undefined
520
+ }))
464
521
  ]
465
522
  })
466
523
  ]