@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-email",
3
- "version": "1.0.0-beta.188",
3
+ "version": "1.0.0-beta.190",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.188",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.188",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.188",
31
- "@aglyn/shared-data-types": "1.0.0-beta.188",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.188",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.188",
34
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.188",
35
- "@aglyn/shared-ui-next": "1.0.0-beta.188",
36
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.188",
37
- "@aglyn/shared-ui-theme": "1.0.0-beta.188",
38
- "@aglyn/shared-util-email": "1.0.0-beta.188",
39
- "@aglyn/shared-util-http": "1.0.0-beta.188",
40
- "@aglyn/shared-util-timestamp": "1.0.0-beta.188",
41
- "@aglyn/shared-util-tools": "1.0.0-beta.188",
42
- "@aglyn/tenant-data-admin": "1.0.0-beta.188",
43
- "@aglyn/tenant-feature-instance": "1.0.0-beta.188",
28
+ "@aglyn/aglyn": "1.0.0-beta.190",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.190",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.190",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.190",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.190",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.190",
34
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.190",
35
+ "@aglyn/shared-ui-next": "1.0.0-beta.190",
36
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.190",
37
+ "@aglyn/shared-ui-theme": "1.0.0-beta.190",
38
+ "@aglyn/shared-util-email": "1.0.0-beta.190",
39
+ "@aglyn/shared-util-http": "1.0.0-beta.190",
40
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.190",
41
+ "@aglyn/shared-util-tools": "1.0.0-beta.190",
42
+ "@aglyn/tenant-data-admin": "1.0.0-beta.190",
43
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.190",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -14,12 +14,16 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
19
  import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
19
20
  import { mdiDeleteOutline, mdiContentCopy, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
20
21
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
21
23
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
24
+ import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
22
25
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
26
+ import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
23
27
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
28
  import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
25
29
  import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useFirestoreCollection, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
@@ -30,6 +34,34 @@ import { useMemo } from "react";
30
34
  import { templateProvenance } from "../model/template-provenance.js";
31
35
  import { createEmailScreen } from "../utils/create-email-screen.js";
32
36
  /** How many of the site's screens one read of this list covers. */ const TEMPLATE_CEILING = 200;
37
+ /*
38
+ * What the templates grid's Filters panel offers (AGL-3317). The card holds
39
+ * the templates its capped window read, so it answers the panel over all of
40
+ * them; the notice under the table says when the cap truncated the window.
41
+ * Origin reads `originKey`, the provenance the column draws.
42
+ */ const TEMPLATE_FILTER_FIELDS = [
43
+ inMemoryListField('displayName', 'text', 'templateName'),
44
+ inMemoryListField('origin', 'select', 'originKey')
45
+ ];
46
+ const TEMPLATE_FILTER_HEADERS = {
47
+ displayName: 'Template',
48
+ origin: 'Origin'
49
+ };
50
+ const TEMPLATE_FILTER_OPTIONS = {
51
+ origin: [
52
+ {
53
+ value: 'local',
54
+ label: 'Yours'
55
+ },
56
+ {
57
+ value: 'installed',
58
+ label: 'Installed'
59
+ }
60
+ ]
61
+ };
62
+ const TEMPLATE_SEARCH_FIELDS = [
63
+ 'templateName'
64
+ ];
33
65
  // The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/
34
66
  // versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the
35
67
  // pre-AGL-621/622 shape — so every "Edit"/"Design" jump out of the Emails
@@ -102,9 +134,22 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
102
134
  ].filter((screen)=>!screen.deletedAt && screen.kind === 'email').sort((a, b)=>{
103
135
  var _a_displayName, _b_displayName;
104
136
  return String((_a_displayName = a.displayName) != null ? _a_displayName : '').localeCompare(String((_b_displayName = b.displayName) != null ? _b_displayName : ''));
137
+ }).map((screen)=>{
138
+ var _screen_displayName;
139
+ return _extends({}, screen, {
140
+ templateName: String((_screen_displayName = screen.displayName) != null ? _screen_displayName : 'Untitled template'),
141
+ originKey: templateProvenance(screen).origin === 'installed' ? 'installed' : 'local'
142
+ });
105
143
  }), [
106
144
  readScreens
107
145
  ]);
146
+ const templateFilter = useListRowsFilter({
147
+ rows: emailScreens,
148
+ fields: TEMPLATE_FILTER_FIELDS,
149
+ options: TEMPLATE_FILTER_OPTIONS,
150
+ headers: TEMPLATE_FILTER_HEADERS,
151
+ search: TEMPLATE_SEARCH_FIELDS
152
+ });
108
153
  const handleCreate = async ()=>{
109
154
  try {
110
155
  const { screenId, versionId } = await createEmailScreen(hostId, createHostResource, createHostVersion);
@@ -214,8 +259,8 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
214
259
  */ field: 'origin',
215
260
  headerName: 'Origin',
216
261
  width: 140,
217
- valueGetter: (_value, row)=>templateProvenance(row).origin === 'installed' ? 'Installed' : 'Yours',
218
- renderCell: ({ value })=>value === 'Installed' ? /*#__PURE__*/ _jsx(Chip, {
262
+ valueGetter: (_value, row)=>row.originKey,
263
+ renderCell: ({ value })=>value === 'installed' ? /*#__PURE__*/ _jsx(Chip, {
219
264
  size: "small",
220
265
  label: "Installed"
221
266
  }) : /*#__PURE__*/ _jsx(Typography, {
@@ -255,12 +300,19 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
255
300
  variant: "body2",
256
301
  color: "text.secondary",
257
302
  children: 'Design a reusable email here, then send it from a campaign. A ' + 'new template opens in the besigner with email-safe components ' + 'only.'
258
- }) : /*#__PURE__*/ _jsx(ListTable, {
259
- "aria-label": "Email templates",
260
- rows: emailScreens,
261
- columns: columns,
262
- rowHeight: TABLE_ROW_HEIGHT,
263
- onOpen: (_id, row)=>router.push(templateHref(row))
303
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
304
+ children: [
305
+ /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, templateFilter.chipsProps)),
306
+ /*#__PURE__*/ _jsx(ListTable, _extends({
307
+ "aria-label": "Email templates",
308
+ rows: templateFilter.rows,
309
+ columns: templateFilter.filterColumns(columns),
310
+ rowHeight: TABLE_ROW_HEIGHT,
311
+ onOpen: (_id, row)=>router.push(templateHref(row))
312
+ }, templateFilter.gridProps, {
313
+ noRowsLabel: "No templates match these filters"
314
+ }))
315
+ ]
264
316
  }),
265
317
  truncated ? /*#__PURE__*/ _jsx(Alert, {
266
318
  severity: "info",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport {\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 { TABLE_ROW_HEIGHT } 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 DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, Timestamp, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\n\n/** How many of the site's screens one read of this list covers. */\nconst TEMPLATE_CEILING = 200\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## Why the read is a CEILING with a probe\n *\n * `screens` holds every kind of screen the site has and the email ones are\n * picked out here, so the cap cannot be a page: a page of the collection is\n * not a page of this list. `collectionCeiling` orders on the document name,\n * which is the one ordering that cannot drop a screen — `orderBy('displayName')`\n * matches only documents that HAVE the field, so a screen created without a\n * name would vanish rather than sort oddly. It reads one past the ceiling so\n * the card can SAY when the site holds more than it drew, and the footer under\n * the table pages the window the card already has.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const { data: screenDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'screens'),\n TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readScreens, truncated } = ceilingedWindow<any>(\n screenDocs,\n TEMPLATE_CEILING,\n )\n const emailScreens = useMemo(\n () =>\n [...readScreens]\n .filter((screen: any) => !screen.deletedAt && screen.kind === 'email')\n .sort((a: any, b: any) =>\n String(a.displayName ?? '').localeCompare(\n String(b.displayName ?? ''),\n ),\n ),\n [readScreens],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {\n deletedAt: Timestamp.now(),\n })\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The templates are a window this card already holds, so the grid pages\n * it. The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\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 return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#create-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n ) : (\n <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={columns}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n />\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`This site holds more than ${TEMPLATE_CEILING} screens, and the ` +\n 'templates listed are drawn from the first of them. Any beyond ' +\n 'that are not in this list.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","collectionCeiling","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Stack","Typography","collection","doc","Timestamp","updateDoc","useRouter","useMemo","templateProvenance","createEmailScreen","TEMPLATE_CEILING","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","data","screenDocs","idField","rows","readScreens","truncated","emailScreens","filter","screen","deletedAt","kind","sort","a","b","String","displayName","localeCompare","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","templateName","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","value","event","stopPropagation","width","origin","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","length","aria-label","rowHeight","onOpen","_id","severity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, Timestamp, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\n\n/** How many of the site's screens one read of this list covers. */\nconst TEMPLATE_CEILING = 200\n\n/*\n * What the templates grid's Filters panel offers (AGL-3317). The card holds\n * the templates its capped window read, so it answers the panel over all of\n * them; the notice under the table says when the cap truncated the window.\n * Origin reads `originKey`, the provenance the column draws.\n */\nconst TEMPLATE_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text', 'templateName'),\n inMemoryListField('origin', 'select', 'originKey'),\n]\nconst TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n origin: 'Origin',\n}\nconst TEMPLATE_FILTER_OPTIONS = {\n origin: [\n { value: 'local', label: 'Yours' },\n { value: 'installed', label: 'Installed' },\n ],\n}\nconst TEMPLATE_SEARCH_FIELDS = ['templateName'] as const\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## Why the read is a CEILING with a probe\n *\n * `screens` holds every kind of screen the site has and the email ones are\n * picked out here, so the cap cannot be a page: a page of the collection is\n * not a page of this list. `collectionCeiling` orders on the document name,\n * which is the one ordering that cannot drop a screen — `orderBy('displayName')`\n * matches only documents that HAVE the field, so a screen created without a\n * name would vanish rather than sort oddly. It reads one past the ceiling so\n * the card can SAY when the site holds more than it drew, and the footer under\n * the table pages the window the card already has.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const { data: screenDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'screens'),\n TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readScreens, truncated } = ceilingedWindow<any>(\n screenDocs,\n TEMPLATE_CEILING,\n )\n const emailScreens = useMemo(\n () =>\n [...readScreens]\n .filter((screen: any) => !screen.deletedAt && screen.kind === 'email')\n .sort((a: any, b: any) =>\n String(a.displayName ?? '').localeCompare(\n String(b.displayName ?? ''),\n ),\n )\n .map((screen: any) => ({\n ...screen,\n templateName: String(screen.displayName ?? 'Untitled template'),\n originKey:\n templateProvenance(screen).origin === 'installed' ? 'installed' : 'local',\n })),\n [readScreens],\n )\n const templateFilter = useListRowsFilter<any>({\n rows: emailScreens,\n fields: TEMPLATE_FILTER_FIELDS,\n options: TEMPLATE_FILTER_OPTIONS,\n headers: TEMPLATE_FILTER_HEADERS,\n search: TEMPLATE_SEARCH_FIELDS,\n })\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {\n deletedAt: Timestamp.now(),\n })\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The templates are a window this card already holds, so the grid pages\n * it. The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) => row.originKey,\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#create-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips {...templateFilter.chipsProps} />\n <ListTable\n aria-label=\"Email templates\"\n rows={templateFilter.rows}\n columns={templateFilter.filterColumns(columns)}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // The panel and the search are the grid's; the card answers\n // them over every template its window read.\n {...templateFilter.gridProps}\n noRowsLabel=\"No templates match these filters\"\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`This site holds more than ${TEMPLATE_CEILING} screens, and the ` +\n 'templates listed are drawn from the first of them. Any beyond ' +\n 'that are not in this list.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListRowActions","ListTable","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","useListRowsFilter","useSnackbar","ceilingedWindow","collectionCeiling","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Stack","Typography","collection","doc","Timestamp","updateDoc","useRouter","useMemo","templateProvenance","createEmailScreen","TEMPLATE_CEILING","TEMPLATE_FILTER_FIELDS","TEMPLATE_FILTER_HEADERS","displayName","origin","TEMPLATE_FILTER_OPTIONS","value","label","TEMPLATE_SEARCH_FIELDS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","data","screenDocs","idField","rows","readScreens","truncated","emailScreens","filter","screen","deletedAt","kind","sort","a","b","String","localeCompare","map","templateName","originKey","templateFilter","fields","options","headers","search","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","rowActions","key","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","length","chipsProps","aria-label","filterColumns","rowHeight","onOpen","_id","gridProps","noRowsLabel","severity"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,UAAU,EAAEC,cAAc,EAAEC,KAAK,QAAQ,eAAc;AAChE,SACEC,gBAAgB,EAChBC,cAAc,EACdC,aAAa,EACbC,gBAAgB,QACX,yBAAwB;AAC/B,SACEC,OAAO,EACPC,WAAW,EACXC,OAAO,EACPC,sBAAsB,QACjB,uBAAsB;AAC7B,OAAOC,qBAAqB,8DAA6D;AACzF,SACEC,cAAc,EACdC,SAAS,EACTC,iBAAiB,QACZ,uDAAsD;AAE7D,SAASC,iBAAiB,QAAQ,8CAA6C;AAC/E,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,eAAe,EACfC,iBAAiB,QACZ,+DAA8D;AACrE,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,oBAAoB,EACpBC,YAAY,EACZC,sBAAsB,EACtBC,kBAAkB,EAClBC,iBAAiB,QACZ,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,UAAU,QACL,gBAAe;AAEtB,SAASC,UAAU,EAAEC,GAAG,EAAEC,SAAS,EAAEC,SAAS,QAAQ,qBAAoB;AAC1E,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,kBAAkB,QAAQ,kCAA8B;AACjE,SAASC,iBAAiB,QAAQ,kCAA8B;AAEhE,iEAAiE,GACjE,MAAMC,mBAAmB;AAEzB;;;;;CAKC,GACD,MAAMC,yBAAyB;IAC7B3B,kBAAkB,eAAe,QAAQ;IACzCA,kBAAkB,UAAU,UAAU;CACvC;AACD,MAAM4B,0BAA4D;IAChEC,aAAa;IACbC,QAAQ;AACV;AACA,MAAMC,0BAA0B;IAC9BD,QAAQ;QACN;YAAEE,OAAO;YAASC,OAAO;QAAQ;QACjC;YAAED,OAAO;YAAaC,OAAO;QAAY;KAC1C;AACH;AACA,MAAMC,yBAAyB;IAAC;CAAe;AAE/C,qEAAqE;AACrE,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,MAAMC,eAAe,CACnBC,SACAC,MACAC,UACAC,YAEAtD,WAAWE,MAAMqD,eAAe,EAAE;QAAEJ;QAASC;QAAMC;QAAUC;IAAU;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,SAASE,iBAAiBC,KAIhC;IACC,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,MAAM,EAAEN,OAAO,EAAES,SAAS,EAAE,GAAGtC,oBAAoBoC;IACnD,MAAMG,YAAYrC;IAClB,MAAMsC,qBAAqBpC;IAC3B,MAAMqC,oBAAoBpC;IAC1B,MAAMqC,SAAS3B;IACf,MAAM,EAAE4B,eAAe,EAAE,GAAG/C;IAC5B,yEAAyE;IACzE,6BAA6B;IAC7B,MAAMgD,YAAY3C,qBAAqB;QACrCmC;QACAS,cAAc,CAACC,OAAOC,OACpBJ,gBAAgB,CAAC,eAAe,EAAEI,KAAKC,IAAI,CAAC,CAAC,CAAC,EAAE;gBAAEC,SAAS;YAAU;IACzE;IACA,MAAM,EAAEC,OAAO,EAAE,GAAG9D;IAEpB,MAAM,EAAE+D,MAAMC,UAAU,EAAE,GAAGjD,uBAC3B,IACEL,kBACEa,WAAW4B,WAAW,SAASH,QAAQ,YACvCjB,mBAEJ;QAACoB;QAAWH;KAAO,EACnB;QAAEiB,SAAS;IAAM;IAEnB,MAAM,EAAEC,MAAMC,WAAW,EAAEC,SAAS,EAAE,GAAG3D,gBACvCuD,YACAjC;IAEF,MAAMsC,eAAezC,QACnB,IACE;eAAIuC;SAAY,CACbG,MAAM,CAAC,CAACC,SAAgB,CAACA,OAAOC,SAAS,IAAID,OAAOE,IAAI,KAAK,SAC7DC,IAAI,CAAC,CAACC,GAAQC;gBACND,gBACEC;mBADTC,QAAOF,iBAAAA,EAAEzC,WAAW,YAAbyC,iBAAiB,IAAIG,aAAa,CACvCD,QAAOD,iBAAAA,EAAE1C,WAAW,YAAb0C,iBAAiB;WAG3BG,GAAG,CAAC,CAACR;gBAEiBA;mBAFA,aAClBA;gBACHS,cAAcH,QAAON,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB;gBAC3CU,WACEpD,mBAAmB0C,QAAQpC,MAAM,KAAK,cAAc,cAAc;;YAE1E;QAACgC;KAAY;IAEf,MAAMe,iBAAiB3E,kBAAuB;QAC5C2D,MAAMG;QACNc,QAAQnD;QACRoD,SAAShD;QACTiD,SAASpD;QACTqD,QAAQ/C;IACV;IAEA,MAAMgD,eAAe;QACnB,IAAI;YACF,MAAM,EAAE5C,QAAQ,EAAEC,SAAS,EAAE,GAAG,MAAMd,kBACpCkB,QACAI,oBACAC;YAEF,IAAIZ,WAAWS,WAAW;gBACxB,KAAKI,OAAOkC,IAAI,CAAChD,aAAaC,SAASS,WAAWP,UAAUC;YAC9D;QACF,EAAE,OAAO6C,OAAY;;YACnBC,QAAQD,KAAK,CAACA;YACdlC,wBAAgBkC,yBAAAA,MAAOE,OAAO,mBAAI,gCAAgC;gBAChE9B,SAAS;YACX;QACF;IACF;IAEA,MAAM+B,eAAe,OAAOrB;YAIlBA;QAHR,MAAMsB,YAAY,MAAM/B,QAAQ;YAC9BgC,OAAO;YACPC,aACE,CAAC,CAAC,GAAExB,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB,oBAAoB,mBAAmB,CAAC,GAClE;YACFyB,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,WAAW;QAChB,MAAMnE,UAAUF,IAAI2B,WAAW,SAASH,QAAQ,WAAWuB,OAAO8B,GAAG,GAAG;YACtE7B,WAAW/C,UAAU6E,GAAG;QAC1B;IACF;IAEA,MAAMC,eAAe,CAAChC,SAAgB,GAAGtB,SAAS,WAAW,EAAEsB,OAAO8B,GAAG,EAAE;IAE3E,MAAMrB,eAAe,CAACT;YACbA;eAAPM,QAAON,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB;;IAE/B,MAAMiC,aAAa,CAACjC,SAAsC;YACxD;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAMhH,cAAcgH,IAAI;oBAAEC,MAAM;;gBAC/CC,MAAMN,aAAahC;YACrB;YACA;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAM/G,iBAAiB+G,IAAI;oBAAEC,MAAM;;gBAClD;;;;;OAKC,GACDC,MACEpE,WAAWS,YACPV,aAAaC,SAASS,WAAWqB,OAAO8B,GAAG,EAAE9B,OAAO3B,SAAS,IAC7DkE;gBACNC,UAAU,CAACtE,WAAW,CAACS;gBACvB8D,gBAAgB;YAClB;YACA;gBACEP,KAAK;gBACLnE,OAAO3B;gBACP+F,oBAAM,KAAC3G;oBAAQ4G,MAAMjH,eAAeiH,IAAI;oBAAEC,MAAM;;gBAChDK,SAAS,IACPzD,UAAU0D,OAAO,CAAC,eAAe;wBAC/BC,IAAI5C,OAAO8B,GAAG;wBACdzC,MAAMoB,aAAaT;oBACrB;YACJ;YACA;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAMlH,iBAAiBkH,IAAI;oBAAEC,MAAM;;gBAClDQ,aAAa;gBACbH,SAAS,IAAM,KAAKrB,aAAarB;YACnC;SACD;IAED;;;GAGC,GACD,MAAM8C,UAAwB;QAC5B;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,aAAa,CAACC,QAAQC,MAAQ5C,aAAa4C;YAC3CC,YAAY,CAAC,EAAED,GAAG,EAAEvF,KAAK,EAAE,iBACzB,KAACxC;oBACCgH,MAAMN,aAAaqB;oBACnB,+DAA+D;oBAC/D,4CAA4C;oBAC5CX,SAAS,CAACa,QACRA,MAAMC,eAAe;8BAGtB1F;;QAGP;QACA;YACE;;;;OAIC,GACDiF,OAAO;YACPC,YAAY;YACZS,OAAO;YACPN,aAAa,CAACC,QAAQC,MAAQA,IAAI3C,SAAS;YAC3C4C,YAAY,CAAC,EAAExF,KAAK,EAAE,GACpBA,UAAU,4BACR,KAACjB;oBAAKwF,MAAK;oBAAQtE,OAAM;mCAEzB,KAAChB;oBAAWuC,SAAQ;oBAAQqC,OAAM;8BAC/B;;QAGT;QACA9F,kBACE,CAACwH,oBACC,KAAC1H;gBAAeoC,OAAO0C,aAAa4C;gBAAMK,OAAOzB,WAAWoB;gBAE9D;YAAEI,OAAO;QAAG;KAEf;IAED,qBACE,MAAClI;QACCoI,QAAQ;QACRC,MAAM5I,eAAe,kBAAkB;YAAE6I,QAAQ;QAAqB;QACtEC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACrH;gBACCyF,MAAK;gBACL/C,SAAQ;gBACRoD,SAAS,IAAM,KAAK1B;0BAEnB;;QAGP;;YAEC/B,UAAUiF,MAAM;0BACjB,MAACpH;gBAAMqH,SAAS;;oBACbrE,aAAasE,MAAM,KAAK,kBACvB,KAACrH;wBAAWuC,SAAQ;wBAAQqC,OAAM;kCAC/B,mEACC,mEACA;uCAGJ;;0CACE,KAACjG,8BAAoBiF,eAAe0D,UAAU;0CAC9C,KAACzI;gCACC0I,cAAW;gCACX3E,MAAMgB,eAAehB,IAAI;gCACzBmD,SAASnC,eAAe4D,aAAa,CAACzB;gCACtC0B,WAAWzI;gCACX0I,QAAQ,CAACC,KAAKrB,MAAQtE,OAAOkC,IAAI,CAACe,aAAaqB;+BAG3C1C,eAAegE,SAAS;gCAC5BC,aAAY;;;;oBAIjB/E,0BACC,KAAClD;wBAAMkI,UAAS;kCACb,CAAC,0BAA0B,EAAErH,iBAAiB,kBAAkB,CAAC,GAChE,mEACA;yBAEF;;;;;AAIZ;AACAe,iBAAiBZ,WAAW,GAAG;AAE/B,eAAeY,iBAAgB"}
@@ -61,52 +61,24 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
61
61
  */ import { readMarketingBasis } from "@aglyn/aglyn";
62
62
  import { mdiAccountRemoveOutline } from "@aglyn/shared-data-mdi";
63
63
  import { MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
64
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
64
65
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
65
66
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
67
+ import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
66
68
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
69
+ import { usePagedRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter";
67
70
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
68
71
  import { Alert, Box, Button, Checkbox, Chip, FormControlLabel, Stack, TextField, Typography } from "@mui/material";
69
72
  import { collection, deleteDoc, doc, documentId, limit, orderBy, query } from "firebase/firestore";
70
- import { useCallback, useEffect, useState } from "react";
73
+ import { useCallback, useEffect, useMemo, useState } from "react";
71
74
  import { useFirestore, usePagedCollection, useUser } from "@aglyn/tenant-feature-instance";
72
75
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
73
- /**
74
- * How the basis reads on screen, for the group this console is viewed as.
75
- *
76
- * ## Read through `readMarketingBasis`, never off the row
77
- *
78
- * This picked `marketingConsent`, `marketingConsentBasis` and
79
- * `marketingConsentAtMs` off the top of the membership document, which is
80
- * where `enrollListMember` used to put them. They now live in an entry per
81
- * site under `marketingConsentByHost`, because a basis runs to the brand it
82
- * was given to rather than to the account that holds the row. Nothing here
83
- * moved with them, so every top-level read answered `undefined` and every
84
- * member of every list — including people carrying a perfectly good attested
85
- * basis — was reported as having no basis on record, on the one screen an
86
- * operator consults before mailing them. `performCampaignSend` reads the same
87
- * documents through the function below and mailed them, correctly, at the same
88
- * time.
89
- *
90
- * The fix is not a second field name. It is that the send path's reader is the
91
- * only reader: a surface that parses the stored shape itself agrees with the
92
- * sender only until the shape moves, and it moves without anybody thinking to
93
- * look here.
94
- *
95
- * ## Five states, because the model carries five
96
- *
97
- * An attestation and an opt-in are not the same fact — one is a person's own
98
- * decision, the other a claim an account made on their behalf — and a table
99
- * rendering both as a tick is the conflation `list-members.ts` stores the
100
- * basis to prevent. The three that are not a grant are not one state either.
101
- * Absence is not refusal, and neither of them is "opted in, to somebody else
102
- * in this account": that third one is a person the sender deliberately leaves
103
- * alone, and an operator who cannot see why reads it as the product having
104
- * lost their subscriber.
105
- */ function consentLabel(member, group) {
76
+ function consentLabel(member, group) {
106
77
  const record = readMarketingBasis(member, group);
107
78
  const when = record.basisAtMs ? ` · ${new Date(record.basisAtMs).toLocaleDateString()}` : '';
108
79
  if (record.basis === 'declined') {
109
80
  return {
81
+ key: 'opted-out',
110
82
  label: `Opted out${when}`,
111
83
  color: 'error'
112
84
  };
@@ -118,9 +90,11 @@ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
118
90
  * `marketingConsentBasis`, and the provenance object a backfill writes —
119
91
  * and the reader has already resolved both into this one answer.
120
92
  */ return record.assertedBy === 'operator' ? {
93
+ key: 'attested',
121
94
  label: `Attested by your team${when}`,
122
95
  color: 'warning'
123
96
  } : {
97
+ key: 'opted-in',
124
98
  label: `Opted in${when}`,
125
99
  color: 'default'
126
100
  };
@@ -132,21 +106,72 @@ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
132
106
  * of another brand in this account and this brand may not borrow it.
133
107
  */ if (record.otherGrant === 'other-host') {
134
108
  return {
109
+ key: 'other-site',
135
110
  label: 'Opted in to another site',
136
111
  color: 'warning'
137
112
  };
138
113
  }
139
114
  if (record.otherGrant === 'unscoped') {
140
115
  return {
116
+ key: 'no-site',
141
117
  label: 'Opted in — no site recorded',
142
118
  color: 'warning'
143
119
  };
144
120
  }
145
121
  return {
122
+ key: 'none',
146
123
  label: 'No basis on record',
147
124
  color: 'default'
148
125
  };
149
126
  }
127
+ /*
128
+ * What the membership grid's Filters panel offers (AGL-3317). The table is a
129
+ * paged listener, so a filter matches over what its window has read, which
130
+ * the first filter widens (`usePagedRowsFilter`). `How` and `Consent` read
131
+ * the keys `withFilterKeys` derives, the same answers the columns draw.
132
+ */ const MEMBER_FILTER_FIELDS = [
133
+ inMemoryListField('email', 'text'),
134
+ inMemoryListField('name', 'text'),
135
+ inMemoryListField('via', 'select', 'viaKey'),
136
+ inMemoryListField('consent', 'select', 'consentKey')
137
+ ];
138
+ const MEMBER_FILTER_HEADERS = {
139
+ email: 'Address',
140
+ name: 'Name',
141
+ via: 'How',
142
+ consent: 'Consent'
143
+ };
144
+ const VIA_LABELS = {
145
+ rule: 'Rule',
146
+ manual: 'Added'
147
+ };
148
+ const CONSENT_LABELS = {
149
+ 'opted-in': 'Opted in',
150
+ attested: 'Attested by your team',
151
+ 'opted-out': 'Opted out',
152
+ 'other-site': 'Opted in to another site',
153
+ 'no-site': 'Opted in — no site recorded',
154
+ none: 'No basis on record'
155
+ };
156
+ const MEMBER_FILTER_OPTIONS = {
157
+ via: Object.entries(VIA_LABELS).map(([value, label])=>({
158
+ value,
159
+ label
160
+ })),
161
+ consent: Object.entries(CONSENT_LABELS).map(([value, label])=>({
162
+ value,
163
+ label
164
+ }))
165
+ };
166
+ const MEMBER_SEARCH_FIELDS = [
167
+ 'email',
168
+ 'name',
169
+ 'source'
170
+ ];
171
+ const withFilterKeys = (member, group)=>_extends({}, member, {
172
+ viaKey: member.via === 'rule' ? 'rule' : 'manual',
173
+ consentKey: consentLabel(member, group).key
174
+ });
150
175
  /** Free text — a paste, a typed address, commas or newlines — to addresses. */ export function splitAddresses(value) {
151
176
  return value.split(/[\s,;]+/).map((entry)=>entry.trim())// A pasted mail-client line is `Name <a@b.co>`; keep the address part.
152
177
  .map((entry)=>entry.replace(/^.*<|>.*$/g, '')).filter(Boolean);
@@ -168,7 +193,7 @@ export function ListMembersPanel(props) {
168
193
  * oldest members from their own list, silently, which is the failure that
169
194
  * turned an audience into a random sample (AGL-2501). The id is the one key
170
195
  * every row has.
171
- */ const { rows: members, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, scope[0], scope[1], 'lists', listId, 'members'), orderBy(documentId()), limit(pageLimit)), [
196
+ */ const { data: memberData, rows: memberRows, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>query(collection(firestore, scope[0], scope[1], 'lists', listId, 'members'), orderBy(documentId()), limit(pageLimit)), [
172
197
  firestore,
173
198
  scope[0],
174
199
  scope[1],
@@ -176,6 +201,30 @@ export function ListMembersPanel(props) {
176
201
  ], {
177
202
  idField: '$id'
178
203
  });
204
+ const memberWindow = useMemo(()=>memberData == null ? void 0 : memberData.map((member)=>withFilterKeys(member, consentGroup)), [
205
+ memberData,
206
+ consentGroup
207
+ ]);
208
+ const memberPage = useMemo(()=>memberRows.map((member)=>withFilterKeys(member, consentGroup)), [
209
+ memberRows,
210
+ consentGroup
211
+ ]);
212
+ const memberFilter = usePagedRowsFilter({
213
+ data: memberWindow,
214
+ rows: memberPage,
215
+ hasMore,
216
+ page,
217
+ setPage,
218
+ pageSize,
219
+ setPageSize
220
+ }, {
221
+ fields: MEMBER_FILTER_FIELDS,
222
+ options: MEMBER_FILTER_OPTIONS,
223
+ headers: MEMBER_FILTER_HEADERS,
224
+ search: MEMBER_SEARCH_FIELDS
225
+ });
226
+ // The rows on screen: the page, or the page of the matches.
227
+ const members = memberFilter.rows;
179
228
  const [addInput, setAddInput] = useState('');
180
229
  const [addName, setAddName] = useState('');
181
230
  const [preview, setPreview] = useState(null);
@@ -434,13 +483,13 @@ export function ListMembersPanel(props) {
434
483
  field: 'via',
435
484
  headerName: 'How',
436
485
  width: 200,
437
- valueGetter: (_value, row)=>row.via === 'rule' ? 'Rule' : 'Added',
486
+ valueGetter: (_value, row)=>row.viaKey,
438
487
  /*
439
488
  `via` says whether a rule put them here or a person did, which is what
440
489
  decides whether they LEAVE on their own: the materializer reconciles its
441
490
  own rows away and never touches a manual one. `source` is the finer
442
491
  provenance underneath it.
443
- */ renderCell: ({ row, value })=>/*#__PURE__*/ _jsxs(Stack, {
492
+ */ renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
444
493
  direction: "row",
445
494
  spacing: 1,
446
495
  sx: {
@@ -451,7 +500,7 @@ export function ListMembersPanel(props) {
451
500
  /*#__PURE__*/ _jsx(Chip, {
452
501
  size: "small",
453
502
  variant: "outlined",
454
- label: value
503
+ label: VIA_LABELS[row.viaKey]
455
504
  }),
456
505
  row.source ? /*#__PURE__*/ _jsx(Typography, {
457
506
  variant: "caption",
@@ -466,7 +515,7 @@ export function ListMembersPanel(props) {
466
515
  field: 'consent',
467
516
  headerName: 'Consent',
468
517
  width: 190,
469
- valueGetter: (_value, row)=>consentLabel(row, consentGroup).label,
518
+ valueGetter: (_value, row)=>row.consentKey,
470
519
  renderCell: ({ row })=>{
471
520
  const consent = consentLabel(row, consentGroup);
472
521
  return /*#__PURE__*/ _jsx(Chip, {
@@ -643,28 +692,29 @@ export function ListMembersPanel(props) {
643
692
  }, result.input);
644
693
  })
645
694
  }) : null,
646
- members.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
695
+ memberRows.length === 0 && !hasMore && !memberFilter.filtering ? /*#__PURE__*/ _jsx(Typography, {
647
696
  variant: "body2",
648
697
  color: "text.secondary",
649
698
  children: 'Nobody is on this list yet.'
650
699
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
651
700
  children: [
652
- /*#__PURE__*/ _jsx(ListTable, {
701
+ /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, memberFilter.chipsProps)),
702
+ memberFilter.filtering && hasMore ? /*#__PURE__*/ _jsx(Typography, {
703
+ variant: "caption",
704
+ color: "text.secondary",
705
+ children: `Filtering the ${memberFilter.read} members read so far — the next page reads more.`
706
+ }) : null,
707
+ /*#__PURE__*/ _jsx(ListTable, _extends({
653
708
  "aria-label": `Members of ${listName}`,
654
709
  rows: members,
655
- columns: columns,
710
+ columns: memberFilter.filterColumns(columns),
656
711
  rowHeight: TABLE_ROW_HEIGHT,
657
712
  // Paged by the footer below, so the grid must not also slice.
658
713
  hideFooter: true
659
- }),
660
- /*#__PURE__*/ _jsx(ListPagination, {
661
- page: page,
662
- pageSize: pageSize,
663
- rowCount: members.length,
664
- hasMore: hasMore,
665
- onPageChange: setPage,
666
- onPageSizeChange: setPageSize
667
- })
714
+ }, memberFilter.gridProps, {
715
+ noRowsLabel: "No members match these filters"
716
+ })),
717
+ /*#__PURE__*/ _jsx(ListPagination, _extends({}, memberFilter.pagination))
668
718
  ]
669
719
  }),
670
720
  /*#__PURE__*/ _jsx(Typography, {