@aglyn/plugins-email 1.0.0-beta.189 → 1.0.0-beta.191
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/consent-group-confirmation-card.d.ts +47 -0
- package/src/lib/components/consent-group-confirmation-card.js +188 -0
- package/src/lib/components/consent-group-confirmation-card.js.map +1 -0
- package/src/lib/components/email-screens-card.js +61 -9
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/emails-console-page.js +19 -4
- package/src/lib/components/emails-console-page.js.map +1 -1
- package/src/lib/components/list-members-panel.js +101 -51
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/lists-card.js +74 -15
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.js +79 -15
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/suppressions-card.js +75 -18
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/server.js +31 -11
- package/src/lib/server.js.map +1 -1
|
@@ -14,23 +14,59 @@
|
|
|
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 { createResourceUid, pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
20
|
import { mdiDeleteOutline, 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 { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
22
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";
|
|
23
26
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
27
|
+
import { usePagedRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter";
|
|
24
28
|
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
25
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
26
30
|
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
27
31
|
import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
|
|
28
32
|
import { collection, doc, getCountFromServer, limit, orderBy, query, setDoc } from "firebase/firestore";
|
|
29
33
|
import { useRouter } from "next/navigation";
|
|
30
|
-
import { useEffect, useState } from "react";
|
|
34
|
+
import { useEffect, useMemo, useState } from "react";
|
|
31
35
|
import { useFirestore, usePagedCollection, useUser } from "@aglyn/tenant-feature-instance";
|
|
32
36
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
33
37
|
import { useEmailDataScope } from "./email-org-mount.js";
|
|
38
|
+
/*
|
|
39
|
+
* What the lists grid's Filters panel offers (AGL-3317). The list is a paged
|
|
40
|
+
* listener, so a filter matches over what its window has read, which the
|
|
41
|
+
* first filter widens (`usePagedRowsFilter`). Membership reads `kindKey`,
|
|
42
|
+
* the stored kind with a list that predates `kind` counted as manual, which
|
|
43
|
+
* is how the column draws it.
|
|
44
|
+
*/ const LIST_FILTER_FIELDS = [
|
|
45
|
+
inMemoryListField('name', 'text'),
|
|
46
|
+
inMemoryListField('kind', 'select', 'kindKey')
|
|
47
|
+
];
|
|
48
|
+
const LIST_FILTER_HEADERS = {
|
|
49
|
+
name: 'List',
|
|
50
|
+
kind: 'Membership'
|
|
51
|
+
};
|
|
52
|
+
const LIST_FILTER_OPTIONS = {
|
|
53
|
+
kind: [
|
|
54
|
+
{
|
|
55
|
+
value: 'manual',
|
|
56
|
+
label: 'Manual'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
value: 'dynamic',
|
|
60
|
+
label: 'Rule'
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
const LIST_SEARCH_FIELDS = [
|
|
65
|
+
'name'
|
|
66
|
+
];
|
|
67
|
+
/** A list row with the membership kind the filter matches on. */ const withKindKey = (list)=>_extends({}, list, {
|
|
68
|
+
kindKey: list.kind === 'dynamic' ? 'dynamic' : 'manual'
|
|
69
|
+
});
|
|
34
70
|
/**
|
|
35
71
|
* Email lists (AGL-254): static audiences at `orgs/{orgId}/lists`, shared
|
|
36
72
|
* across the org's sites. Members arrive via the enrollList automation
|
|
@@ -78,12 +114,34 @@ import { useEmailDataScope } from "./email-org-mount.js";
|
|
|
78
114
|
* newsletter enrollment both resolve an existing list and deliberately
|
|
79
115
|
* never create one, and `lists` is absent from `IMPORTABLE_FIELDS`, so no
|
|
80
116
|
* restore path can produce a nameless document for `orderBy` to drop.
|
|
81
|
-
*/ const { rows:
|
|
117
|
+
*/ const { data: listData, rows: listRows, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>scope ? query(collection(firestore, scope[0], scope[1], 'lists'), orderBy('name'), limit(pageLimit)) : null, [
|
|
82
118
|
firestore,
|
|
83
119
|
scope
|
|
84
120
|
], {
|
|
85
121
|
idField: '$id'
|
|
86
122
|
});
|
|
123
|
+
const listWindow = useMemo(()=>listData == null ? void 0 : listData.map(withKindKey), [
|
|
124
|
+
listData
|
|
125
|
+
]);
|
|
126
|
+
const listPage = useMemo(()=>listRows.map(withKindKey), [
|
|
127
|
+
listRows
|
|
128
|
+
]);
|
|
129
|
+
const listFilter = usePagedRowsFilter({
|
|
130
|
+
data: listWindow,
|
|
131
|
+
rows: listPage,
|
|
132
|
+
hasMore,
|
|
133
|
+
page,
|
|
134
|
+
setPage,
|
|
135
|
+
pageSize,
|
|
136
|
+
setPageSize
|
|
137
|
+
}, {
|
|
138
|
+
fields: LIST_FILTER_FIELDS,
|
|
139
|
+
options: LIST_FILTER_OPTIONS,
|
|
140
|
+
headers: LIST_FILTER_HEADERS,
|
|
141
|
+
search: LIST_SEARCH_FIELDS
|
|
142
|
+
});
|
|
143
|
+
// The rows on screen: the page, or the page of the matches.
|
|
144
|
+
const lists = listFilter.rows;
|
|
87
145
|
const [counts, setCounts] = useState({});
|
|
88
146
|
useEffect(()=>{
|
|
89
147
|
// `lists` can only be non-empty when `scope` resolved, but the
|
|
@@ -265,7 +323,7 @@ import { useEmailDataScope } from "./email-org-mount.js";
|
|
|
265
323
|
headerName: 'Membership',
|
|
266
324
|
flex: 1,
|
|
267
325
|
minWidth: 220,
|
|
268
|
-
valueGetter: (_value, row)=>row.
|
|
326
|
+
valueGetter: (_value, row)=>row.kindKey,
|
|
269
327
|
renderCell: ({ row })=>{
|
|
270
328
|
var _row_lastEvaluatedAt;
|
|
271
329
|
return row.kind === 'dynamic' ? /*#__PURE__*/ _jsx(Chip, {
|
|
@@ -346,25 +404,26 @@ import { useEmailDataScope } from "./email-org-mount.js";
|
|
|
346
404
|
}) : null,
|
|
347
405
|
includeDescription: false
|
|
348
406
|
}),
|
|
349
|
-
|
|
407
|
+
listRows.length === 0 && !hasMore && !listFilter.filtering ? null : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
350
408
|
children: [
|
|
351
|
-
/*#__PURE__*/ _jsx(
|
|
409
|
+
/*#__PURE__*/ _jsx(ListFilterChips, _extends({}, listFilter.chipsProps)),
|
|
410
|
+
listFilter.filtering && hasMore ? /*#__PURE__*/ _jsx(Typography, {
|
|
411
|
+
variant: "caption",
|
|
412
|
+
color: "text.secondary",
|
|
413
|
+
children: `Filtering the ${listFilter.read} lists read so far — the next page reads more.`
|
|
414
|
+
}) : null,
|
|
415
|
+
/*#__PURE__*/ _jsx(ListTable, _extends({
|
|
352
416
|
"aria-label": "Email lists",
|
|
353
417
|
rows: lists,
|
|
354
|
-
columns: columns,
|
|
418
|
+
columns: listFilter.filterColumns(columns),
|
|
355
419
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
356
420
|
onOpen: (_id, row)=>router.push(listHref(row)),
|
|
357
421
|
// Paged by the footer below, so the grid must not also slice.
|
|
358
422
|
hideFooter: true
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
rowCount: lists.length,
|
|
364
|
-
hasMore: hasMore,
|
|
365
|
-
onPageChange: setPage,
|
|
366
|
-
onPageSizeChange: setPageSize
|
|
367
|
-
})
|
|
423
|
+
}, listFilter.gridProps, {
|
|
424
|
+
noRowsLabel: "No lists match these filters"
|
|
425
|
+
})),
|
|
426
|
+
/*#__PURE__*/ _jsx(ListPagination, _extends({}, listFilter.pagination))
|
|
368
427
|
]
|
|
369
428
|
})
|
|
370
429
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/lists-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 { createResourceUid, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } 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 { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n limit,\n orderBy,\n query,\n setDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useState } from 'react'\nimport {\n useFirestore,\n usePagedCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useEmailDataScope } from './email-org-mount'\n\nexport interface OrgListsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every list route hangs beneath. */\n basePath: string\n}\n\n/**\n * Email lists (AGL-254): static audiences at `orgs/{orgId}/lists`, shared\n * across the org's sites. Members arrive via the enrollList automation\n * step, popup email capture, or manual adds; campaigns target a list from\n * the audience picker.\n *\n * ## A list is a resource, so it has pages\n *\n * The row opens `…/audiences/{listId}`, and everything a list can be asked or\n * told lives there or on its edit page. The membership used to expand INSIDE\n * this table, which made three things impossible at once: a merchant could not\n * link anybody to one audience, the browser's back button did not walk out of\n * a list, and the rename lived under a table it was not about.\n *\n * Row click and the row's own link are two affordances rather than one. A\n * click handler cannot be middle-clicked, copied, or opened from the browser's\n * context menu, and a link alone loses the whole-row target.\n */\nexport function OrgListsCard(props: OrgListsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const { data: user } = useUser()\n // The org lookup is async (AGL-1061): `scope` is null until it settles,\n // and stays null for a host with no owning org. Lists have always been\n // org-shared (AGL-254) — there is no host path to fall back to any more\n // (AGL-1050), so creating one is held rather than misdirected. On the\n // organization's page the mount names the org and nothing is looked up.\n const { scope } = useEmailDataScope(hostId)\n\n /*\n * Ordered by the server and paged, rather than capped and re-sorted here\n * (AGL-2501, AGL-2292).\n *\n * `limit(50)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids from `createResourceUid()` — an arbitrary\n * fifty of the org's lists, which the `localeCompare` below then arranged\n * alphabetically. An agency past fifty lists saw a believable A-to-Z page\n * that was missing most of the alphabet, and the campaign composer's\n * audience picker reads the same collection.\n *\n * `name` is safe to order on here, which is a claim about the writers and\n * not a preference: this card is the only thing that CREATES a list\n * (`handleCreate` refuses an empty name), the automation step and the\n * newsletter enrollment both resolve an existing list and deliberately\n * never create one, and `lists` is absent from `IMPORTABLE_FIELDS`, so no\n * restore path can produce a nameless document for `orderBy` to drop.\n */\n const {\n rows: lists,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n scope\n ? query(\n collection(firestore, scope[0], scope[1], 'lists'),\n orderBy('name'),\n limit(pageLimit),\n )\n : null,\n [firestore, scope],\n { idField: '$id' },\n )\n const [counts, setCounts] = useState<Record<string, number>>({})\n useEffect(() => {\n // `lists` can only be non-empty when `scope` resolved, but the\n // effect must say so for itself — it reads `scope` outside the query.\n if (!scope) return undefined\n let active = true\n void Promise.all(\n lists.map(async (list: any) => {\n try {\n const snapshot = await getCountFromServer(\n collection(\n firestore,\n scope[0],\n scope[1],\n 'lists',\n list.$id,\n 'members',\n ),\n )\n return [list.$id, snapshot.data().count] as const\n } catch {\n return [list.$id, 0] as const\n }\n }),\n ).then((entries) => {\n if (active) setCounts(Object.fromEntries(entries))\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, scope, JSON.stringify(lists.map((l: any) => l.$id))])\n\n /*\n * Creating is a DRAWER, and the drawer is the shared one.\n *\n * The card used to carry a name box, a membership select and a Create\n * button stacked above the table. A create form wedged over a list has\n * nowhere to grow, which is how the rule behind a dynamic list came to be\n * authored through four controls out of nine — the form could not hold the\n * rest. Naming happens here; everything the audience IS happens on its edit\n * page, which has the room.\n */\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<unknown>(null)\n const [creating, setCreating] = useState(false)\n\n const handleCreate = async (values: Record<string, any>) => {\n const name = String(values?.['displayName'] ?? '').trim()\n // Without an org there is nowhere org-shared to put the list, and the\n // host path that used to absorb it is gone (AGL-1050). The button is\n // disabled in this state; this is the same answer for a stale closure.\n if (!name || !scope || creating) return\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n try {\n await setDoc(doc(firestore, scope[0], scope[1], 'lists', id), {\n name,\n /*\n * Manual until somebody says otherwise.\n *\n * A list arrives holding the people you put on it, and becomes\n * rule-driven on its edit page where the rule can actually be\n * written. Offering the choice in the naming drawer would ask for a\n * decision before the thing it decides — a rule — can be seen.\n */\n kind: 'manual',\n createdAt: Timestamp.now(),\n })\n setCreateOpen(false)\n enqueueSnackbar(`List \"${name}\" created`, {\n variant: 'success',\n persist: false,\n })\n router.push(`${basePath}/audiences/${id}`)\n } catch (error) {\n console.error(error)\n setCreateError(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setCreating(false)\n }\n }\n\n const handleDelete = async (list: any) => {\n if (!scope) return\n const accepted = await confirm({\n title: 'Delete list?',\n description: `\"${list.name}\" and its enrollments stop being targetable; automations enrolling into it start reporting errors.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating on\n // the resolved value alone made this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n // The list owns a `members` subcollection of enrolled contacts, and\n // Firestore doesn't cascade — deleting the doc from here left that PII\n // behind, unreachable but still readable to every org member. Only the\n // Admin SDK can recursiveDelete, so this goes through the erase route\n // (AGL-946).\n try {\n const response = await authorizedFetch(user, '/api/resources/erase', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n scope: scope[0],\n scopeId: scope[1],\n kind: 'lists',\n id: list.$id,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) throw new Error(result?.error ?? 'List delete failed')\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'List delete failed', {\n variant: 'error',\n })\n }\n enqueueSnackbar('List deleted', { variant: 'success', persist: false })\n }\n\n const listHref = (list: any) => `${basePath}/audiences/${list.$id}`\n\n const rowActions = (list: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: listHref(list),\n },\n {\n key: 'edit',\n label: 'Edit list',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: `${listHref(list)}/edit`,\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(list),\n },\n ]\n\n /*\n * One row per list. The name is a link AND the row opens the list: a click\n * handler cannot be middle-clicked, copied or opened from the context menu,\n * and a link alone loses the whole-row target.\n */\n const columns: GridColDef[] = [\n {\n field: 'name',\n headerName: 'List',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <AppLink\n href={listHref(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 {row.name}\n </AppLink>\n ),\n },\n {\n field: 'kind',\n headerName: 'Membership',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) =>\n row.kind === 'dynamic' ? 'Rule' : 'Manual',\n renderCell: ({ row }) =>\n row.kind === 'dynamic' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n /*\n Freshness, not just kind. A dynamic list that has stopped being\n swept looks exactly like one whose population has not changed,\n so the last evaluation time is the only thing that tells them\n apart — and it is the first thing to look at when a merchant\n reports that somebody is missing from an audience.\n */\n label={\n row.lastEvaluatedAt?.toDate\n ? `Rule · ${row.lastEvaluatedAt.toDate().toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Manual\" />\n ),\n },\n {\n field: 'subscribers',\n headerName: 'Subscribers',\n width: 130,\n valueGetter: (_value, row) => counts[row.$id] ?? '…',\n },\n listActionsColumn(\n (row) => (\n <ListRowActions\n label={String(row.name ?? row.$id)}\n items={rowActions(row)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Email lists\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!scope || creating}\n onClick={() => setCreateOpen(true)}\n >\n {creating ? 'Creating…' : 'Create audience'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Audiences shared across your organization. A manual list holds ' +\n 'the people you enroll into it. A dynamic list holds everyone ' +\n 'matching a rule, re-checked about every fifteen minutes. Target ' +\n 'either from the campaign composer.'}\n </Typography>\n {/*\n A list document stores a name and nothing else descriptive, so the\n shared Description box is refused: a field the writer discards on\n write is worse than a field that was never offered.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new audience\"\n submitLabel=\"Create audience\"\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {'This audience could not be created.'}\n </Alert>\n ) : null\n }\n includeDescription={false}\n />\n {lists.length === 0 ? null : (\n <>\n <ListTable\n aria-label=\"Email lists\"\n rows={lists}\n columns={columns}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(listHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={lists.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOrgListsCard.displayName = 'OrgListsCard'\n\nexport default OrgListsCard\n"],"names":["createResourceUid","pluginDocsHelp","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","CreateArtifactDrawer","useSnackbar","Timestamp","Alert","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","limit","orderBy","query","setDoc","useRouter","useEffect","useState","useFirestore","usePagedCollection","useUser","authorizedFetch","useEmailDataScope","OrgListsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","data","user","scope","rows","lists","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","counts","setCounts","undefined","active","Promise","all","map","list","snapshot","$id","count","then","entries","Object","fromEntries","JSON","stringify","l","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","handleCreate","values","name","String","trim","id","kind","createdAt","now","variant","persist","push","error","console","handleDelete","accepted","title","description","confirmationText","confirmationButtonProps","color","catch","response","method","headers","body","scopeId","result","json","ok","Error","message","listHref","rowActions","key","label","icon","path","size","href","destructive","onClick","columns","field","headerName","flex","minWidth","renderCell","row","event","stopPropagation","valueGetter","_value","lastEvaluatedAt","toDate","toLocaleString","width","items","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","disabled","spacing","open","onClose","submitLabel","onSubmit","errorSlot","severity","sx","mt","mb","includeDescription","length","aria-label","rowHeight","onOpen","_id","hideFooter","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/lists-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 { createResourceUid, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } 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 { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { usePagedRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-paged-rows-filter'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n limit,\n orderBy,\n query,\n setDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n useFirestore,\n usePagedCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useEmailDataScope } from './email-org-mount'\n\nexport interface OrgListsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every list route hangs beneath. */\n basePath: string\n}\n\n/*\n * What the lists grid's Filters panel offers (AGL-3317). The list is a paged\n * listener, so a filter matches over what its window has read, which the\n * first filter widens (`usePagedRowsFilter`). Membership reads `kindKey`,\n * the stored kind with a list that predates `kind` counted as manual, which\n * is how the column draws it.\n */\nconst LIST_FILTER_FIELDS = [\n inMemoryListField('name', 'text'),\n inMemoryListField('kind', 'select', 'kindKey'),\n]\nconst LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'List',\n kind: 'Membership',\n}\nconst LIST_FILTER_OPTIONS = {\n kind: [\n { value: 'manual', label: 'Manual' },\n { value: 'dynamic', label: 'Rule' },\n ],\n}\nconst LIST_SEARCH_FIELDS = ['name'] as const\n\n/** A list row with the membership kind the filter matches on. */\nconst withKindKey = (list: any) => ({\n ...list,\n kindKey: list.kind === 'dynamic' ? 'dynamic' : 'manual',\n})\n\n/**\n * Email lists (AGL-254): static audiences at `orgs/{orgId}/lists`, shared\n * across the org's sites. Members arrive via the enrollList automation\n * step, popup email capture, or manual adds; campaigns target a list from\n * the audience picker.\n *\n * ## A list is a resource, so it has pages\n *\n * The row opens `…/audiences/{listId}`, and everything a list can be asked or\n * told lives there or on its edit page. The membership used to expand INSIDE\n * this table, which made three things impossible at once: a merchant could not\n * link anybody to one audience, the browser's back button did not walk out of\n * a list, and the rename lived under a table it was not about.\n *\n * Row click and the row's own link are two affordances rather than one. A\n * click handler cannot be middle-clicked, copied, or opened from the browser's\n * context menu, and a link alone loses the whole-row target.\n */\nexport function OrgListsCard(props: OrgListsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const { data: user } = useUser()\n // The org lookup is async (AGL-1061): `scope` is null until it settles,\n // and stays null for a host with no owning org. Lists have always been\n // org-shared (AGL-254) — there is no host path to fall back to any more\n // (AGL-1050), so creating one is held rather than misdirected. On the\n // organization's page the mount names the org and nothing is looked up.\n const { scope } = useEmailDataScope(hostId)\n\n /*\n * Ordered by the server and paged, rather than capped and re-sorted here\n * (AGL-2501, AGL-2292).\n *\n * `limit(50)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids from `createResourceUid()` — an arbitrary\n * fifty of the org's lists, which the `localeCompare` below then arranged\n * alphabetically. An agency past fifty lists saw a believable A-to-Z page\n * that was missing most of the alphabet, and the campaign composer's\n * audience picker reads the same collection.\n *\n * `name` is safe to order on here, which is a claim about the writers and\n * not a preference: this card is the only thing that CREATES a list\n * (`handleCreate` refuses an empty name), the automation step and the\n * newsletter enrollment both resolve an existing list and deliberately\n * never create one, and `lists` is absent from `IMPORTABLE_FIELDS`, so no\n * restore path can produce a nameless document for `orderBy` to drop.\n */\n const {\n data: listData,\n rows: listRows,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n scope\n ? query(\n collection(firestore, scope[0], scope[1], 'lists'),\n orderBy('name'),\n limit(pageLimit),\n )\n : null,\n [firestore, scope],\n { idField: '$id' },\n )\n const listWindow = useMemo(() => listData?.map(withKindKey), [listData])\n const listPage = useMemo(() => listRows.map(withKindKey), [listRows])\n const listFilter = usePagedRowsFilter<any>(\n {\n data: listWindow,\n rows: listPage,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n },\n {\n fields: LIST_FILTER_FIELDS,\n options: LIST_FILTER_OPTIONS,\n headers: LIST_FILTER_HEADERS,\n search: LIST_SEARCH_FIELDS,\n },\n )\n // The rows on screen: the page, or the page of the matches.\n const lists = listFilter.rows\n const [counts, setCounts] = useState<Record<string, number>>({})\n useEffect(() => {\n // `lists` can only be non-empty when `scope` resolved, but the\n // effect must say so for itself — it reads `scope` outside the query.\n if (!scope) return undefined\n let active = true\n void Promise.all(\n lists.map(async (list: any) => {\n try {\n const snapshot = await getCountFromServer(\n collection(\n firestore,\n scope[0],\n scope[1],\n 'lists',\n list.$id,\n 'members',\n ),\n )\n return [list.$id, snapshot.data().count] as const\n } catch {\n return [list.$id, 0] as const\n }\n }),\n ).then((entries) => {\n if (active) setCounts(Object.fromEntries(entries))\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, scope, JSON.stringify(lists.map((l: any) => l.$id))])\n\n /*\n * Creating is a DRAWER, and the drawer is the shared one.\n *\n * The card used to carry a name box, a membership select and a Create\n * button stacked above the table. A create form wedged over a list has\n * nowhere to grow, which is how the rule behind a dynamic list came to be\n * authored through four controls out of nine — the form could not hold the\n * rest. Naming happens here; everything the audience IS happens on its edit\n * page, which has the room.\n */\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<unknown>(null)\n const [creating, setCreating] = useState(false)\n\n const handleCreate = async (values: Record<string, any>) => {\n const name = String(values?.['displayName'] ?? '').trim()\n // Without an org there is nowhere org-shared to put the list, and the\n // host path that used to absorb it is gone (AGL-1050). The button is\n // disabled in this state; this is the same answer for a stale closure.\n if (!name || !scope || creating) return\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n try {\n await setDoc(doc(firestore, scope[0], scope[1], 'lists', id), {\n name,\n /*\n * Manual until somebody says otherwise.\n *\n * A list arrives holding the people you put on it, and becomes\n * rule-driven on its edit page where the rule can actually be\n * written. Offering the choice in the naming drawer would ask for a\n * decision before the thing it decides — a rule — can be seen.\n */\n kind: 'manual',\n createdAt: Timestamp.now(),\n })\n setCreateOpen(false)\n enqueueSnackbar(`List \"${name}\" created`, {\n variant: 'success',\n persist: false,\n })\n router.push(`${basePath}/audiences/${id}`)\n } catch (error) {\n console.error(error)\n setCreateError(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setCreating(false)\n }\n }\n\n const handleDelete = async (list: any) => {\n if (!scope) return\n const accepted = await confirm({\n title: 'Delete list?',\n description: `\"${list.name}\" and its enrollments stop being targetable; automations enrolling into it start reporting errors.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating on\n // the resolved value alone made this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n // The list owns a `members` subcollection of enrolled contacts, and\n // Firestore doesn't cascade — deleting the doc from here left that PII\n // behind, unreachable but still readable to every org member. Only the\n // Admin SDK can recursiveDelete, so this goes through the erase route\n // (AGL-946).\n try {\n const response = await authorizedFetch(user, '/api/resources/erase', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n scope: scope[0],\n scopeId: scope[1],\n kind: 'lists',\n id: list.$id,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) throw new Error(result?.error ?? 'List delete failed')\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'List delete failed', {\n variant: 'error',\n })\n }\n enqueueSnackbar('List deleted', { variant: 'success', persist: false })\n }\n\n const listHref = (list: any) => `${basePath}/audiences/${list.$id}`\n\n const rowActions = (list: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: listHref(list),\n },\n {\n key: 'edit',\n label: 'Edit list',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: `${listHref(list)}/edit`,\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(list),\n },\n ]\n\n /*\n * One row per list. The name is a link AND the row opens the list: a click\n * handler cannot be middle-clicked, copied or opened from the context menu,\n * and a link alone loses the whole-row target.\n */\n const columns: GridColDef[] = [\n {\n field: 'name',\n headerName: 'List',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <AppLink\n href={listHref(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 {row.name}\n </AppLink>\n ),\n },\n {\n field: 'kind',\n headerName: 'Membership',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => row.kindKey,\n renderCell: ({ row }) =>\n row.kind === 'dynamic' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n /*\n Freshness, not just kind. A dynamic list that has stopped being\n swept looks exactly like one whose population has not changed,\n so the last evaluation time is the only thing that tells them\n apart — and it is the first thing to look at when a merchant\n reports that somebody is missing from an audience.\n */\n label={\n row.lastEvaluatedAt?.toDate\n ? `Rule · ${row.lastEvaluatedAt.toDate().toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Manual\" />\n ),\n },\n {\n field: 'subscribers',\n headerName: 'Subscribers',\n width: 130,\n valueGetter: (_value, row) => counts[row.$id] ?? '…',\n },\n listActionsColumn(\n (row) => (\n <ListRowActions\n label={String(row.name ?? row.$id)}\n items={rowActions(row)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Email lists\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!scope || creating}\n onClick={() => setCreateOpen(true)}\n >\n {creating ? 'Creating…' : 'Create audience'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Audiences shared across your organization. A manual list holds ' +\n 'the people you enroll into it. A dynamic list holds everyone ' +\n 'matching a rule, re-checked about every fifteen minutes. Target ' +\n 'either from the campaign composer.'}\n </Typography>\n {/*\n A list document stores a name and nothing else descriptive, so the\n shared Description box is refused: a field the writer discards on\n write is worse than a field that was never offered.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new audience\"\n submitLabel=\"Create audience\"\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {'This audience could not be created.'}\n </Alert>\n ) : null\n }\n includeDescription={false}\n />\n {listRows.length === 0 && !hasMore && !listFilter.filtering ? null : (\n <>\n <ListFilterChips {...listFilter.chipsProps} />\n {listFilter.filtering && hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${listFilter.read} lists read so far — the next page reads more.`}\n </Typography>\n ) : null}\n <ListTable\n aria-label=\"Email lists\"\n rows={lists}\n columns={listFilter.filterColumns(columns)}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(listHref(row))}\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 card answers\n // them over what its window read.\n {...listFilter.gridProps}\n noRowsLabel=\"No lists match these filters\"\n />\n <ListPagination {...listFilter.pagination} />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOrgListsCard.displayName = 'OrgListsCard'\n\nexport default OrgListsCard\n"],"names":["createResourceUid","pluginDocsHelp","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListRowActions","ListTable","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","usePagedRowsFilter","CreateArtifactDrawer","useSnackbar","Timestamp","Alert","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","limit","orderBy","query","setDoc","useRouter","useEffect","useMemo","useState","useFirestore","usePagedCollection","useUser","authorizedFetch","useEmailDataScope","LIST_FILTER_FIELDS","LIST_FILTER_HEADERS","name","kind","LIST_FILTER_OPTIONS","value","label","LIST_SEARCH_FIELDS","withKindKey","list","kindKey","OrgListsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","data","user","scope","listData","rows","listRows","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","listWindow","map","listPage","listFilter","fields","options","headers","search","lists","counts","setCounts","undefined","active","Promise","all","snapshot","$id","count","then","entries","Object","fromEntries","JSON","stringify","l","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","handleCreate","values","String","trim","id","createdAt","now","variant","persist","push","error","console","handleDelete","accepted","title","description","confirmationText","confirmationButtonProps","color","catch","response","method","body","scopeId","result","json","ok","Error","message","listHref","rowActions","key","icon","path","size","href","destructive","onClick","columns","field","headerName","flex","minWidth","renderCell","row","event","stopPropagation","valueGetter","_value","lastEvaluatedAt","toDate","toLocaleString","width","items","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","disabled","spacing","open","onClose","submitLabel","onSubmit","errorSlot","severity","sx","mt","mb","includeDescription","length","filtering","chipsProps","read","aria-label","filterColumns","rowHeight","onOpen","_id","hideFooter","gridProps","noRowsLabel","pagination","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,iBAAiB,EAAEC,cAAc,QAAQ,eAAc;AAChE,SACEC,gBAAgB,EAChBC,aAAa,EACbC,gBAAgB,QACX,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AAC5F,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,cAAc,EACdC,SAAS,EACTC,iBAAiB,QACZ,uDAAsD;AAE7D,SAASC,iBAAiB,QAAQ,8CAA6C;AAC/E,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,kBAAkB,QAAQ,mDAAkD;AACrF,SAASC,oBAAoB,QAAQ,6BAA4B;AACjE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,SAAS,QAAQ,+BAA8B;AACxD,SAASC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEtE,SACEC,UAAU,EACVC,GAAG,EACHC,kBAAkB,EAClBC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,MAAM,QACD,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SACEC,YAAY,EACZC,kBAAkB,EAClBC,OAAO,QACF,iCAAgC;AACvC,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,iBAAiB,QAAQ,uBAAmB;AASrD;;;;;;CAMC,GACD,MAAMC,qBAAqB;IACzB3B,kBAAkB,QAAQ;IAC1BA,kBAAkB,QAAQ,UAAU;CACrC;AACD,MAAM4B,sBAAwD;IAC5DC,MAAM;IACNC,MAAM;AACR;AACA,MAAMC,sBAAsB;IAC1BD,MAAM;QACJ;YAAEE,OAAO;YAAUC,OAAO;QAAS;QACnC;YAAED,OAAO;YAAWC,OAAO;QAAO;KACnC;AACH;AACA,MAAMC,qBAAqB;IAAC;CAAO;AAEnC,+DAA+D,GAC/D,MAAMC,cAAc,CAACC,OAAe,aAC/BA;QACHC,SAASD,KAAKN,IAAI,KAAK,YAAY,YAAY;;AAGjD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASQ,aAAaC,KAAwB;IACnD,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,MAAMG,YAAYpB;IAClB,MAAMqB,SAASzB;IACf,MAAM,EAAE0B,eAAe,EAAE,GAAGxC;IAC5B,MAAM,EAAEyC,OAAO,EAAE,GAAGnD;IACpB,MAAM,EAAEoD,MAAMC,IAAI,EAAE,GAAGvB;IACvB,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,sEAAsE;IACtE,wEAAwE;IACxE,MAAM,EAAEwB,KAAK,EAAE,GAAGtB,kBAAkBc;IAEpC;;;;;;;;;;;;;;;;;GAiBC,GACD,MAAM,EACJM,MAAMG,QAAQ,EACdC,MAAMC,QAAQ,EACdC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACZ,GAAGjC,mBACF,CAACkC,YACCT,QACIhC,MACEL,WAAW+B,WAAWM,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,UAC1CjC,QAAQ,SACRD,MAAM2C,cAER,MACN;QAACf;QAAWM;KAAM,EAClB;QAAEU,SAAS;IAAM;IAEnB,MAAMC,aAAavC,QAAQ,IAAM6B,4BAAAA,SAAUW,GAAG,CAACzB,cAAc;QAACc;KAAS;IACvE,MAAMY,WAAWzC,QAAQ,IAAM+B,SAASS,GAAG,CAACzB,cAAc;QAACgB;KAAS;IACpE,MAAMW,aAAa5D,mBACjB;QACE4C,MAAMa;QACNT,MAAMW;QACNT;QACAC;QACAC;QACAC;QACAC;IACF,GACA;QACEO,QAAQpC;QACRqC,SAASjC;QACTkC,SAASrC;QACTsC,QAAQhC;IACV;IAEF,4DAA4D;IAC5D,MAAMiC,QAAQL,WAAWZ,IAAI;IAC7B,MAAM,CAACkB,QAAQC,UAAU,GAAGhD,SAAiC,CAAC;IAC9DF,UAAU;QACR,+DAA+D;QAC/D,sEAAsE;QACtE,IAAI,CAAC6B,OAAO,OAAOsB;QACnB,IAAIC,SAAS;QACb,KAAKC,QAAQC,GAAG,CACdN,MAAMP,GAAG,CAAC,OAAOxB;YACf,IAAI;gBACF,MAAMsC,WAAW,MAAM7D,mBACrBF,WACE+B,WACAM,KAAK,CAAC,EAAE,EACRA,KAAK,CAAC,EAAE,EACR,SACAZ,KAAKuC,GAAG,EACR;gBAGJ,OAAO;oBAACvC,KAAKuC,GAAG;oBAAED,SAAS5B,IAAI,GAAG8B,KAAK;iBAAC;YAC1C,EAAE,eAAM;gBACN,OAAO;oBAACxC,KAAKuC,GAAG;oBAAE;iBAAE;YACtB;QACF,IACAE,IAAI,CAAC,CAACC;YACN,IAAIP,QAAQF,UAAUU,OAAOC,WAAW,CAACF;QAC3C;QACA,OAAO;YACLP,SAAS;QACX;IACA,uDAAuD;IACzD,GAAG;QAAC7B;QAAWM;QAAOiC,KAAKC,SAAS,CAACf,MAAMP,GAAG,CAAC,CAACuB,IAAWA,EAAER,GAAG;KAAG;IAEnE;;;;;;;;;GASC,GACD,MAAM,CAACS,YAAYC,cAAc,GAAGhE,SAAS;IAC7C,MAAM,CAACiE,aAAaC,eAAe,GAAGlE,SAAkB;IACxD,MAAM,CAACmE,UAAUC,YAAY,GAAGpE,SAAS;IAEzC,MAAMqE,eAAe,OAAOC;;QAC1B,MAAM9D,OAAO+D,eAAOD,0BAAAA,MAAQ,CAAC,cAAc,mBAAI,IAAIE,IAAI;QACvD,sEAAsE;QACtE,qEAAqE;QACrE,uEAAuE;QACvE,IAAI,CAAChE,QAAQ,CAACmB,SAASwC,UAAU;QACjCC,YAAY;QACZF,eAAe;QACf,MAAMO,KAAK5G;QACX,IAAI;YACF,MAAM+B,OAAOL,IAAI8B,WAAWM,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,SAAS8C,KAAK;gBAC5DjE;gBACA;;;;;;;SAOC,GACDC,MAAM;gBACNiE,WAAW1F,UAAU2F,GAAG;YAC1B;YACAX,cAAc;YACdzC,gBAAgB,CAAC,MAAM,EAAEf,KAAK,SAAS,CAAC,EAAE;gBACxCoE,SAAS;gBACTC,SAAS;YACX;YACAvD,OAAOwD,IAAI,CAAC,GAAG1D,SAAS,WAAW,EAAEqD,IAAI;QAC3C,EAAE,OAAOM,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdb,eAAea;YACfxD,gBAAgB,yBAAyB;gBAAEqD,SAAS;YAAQ;QAC9D,SAAU;YACRR,YAAY;QACd;IACF;IAEA,MAAMa,eAAe,OAAOlE;QAC1B,IAAI,CAACY,OAAO;QACZ,MAAMuD,WAAW,MAAM1D,QAAQ;YAC7B2D,OAAO;YACPC,aAAa,CAAC,CAAC,EAAErE,KAAKP,IAAI,CAAC,kGAAkG,CAAC;YAC9H6E,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,EACE,uEAAuE;QACvE,8DAA8D;SAC7D/B,IAAI,CAAC,IAAM,MACXgC,KAAK,CAAC,IAAM;QACf,IAAI,CAACN,UAAU;QACf,oEAAoE;QACpE,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,aAAa;QACb,IAAI;;YACF,MAAMO,WAAW,MAAMrF,gBAAgBsB,MAAM,wBAAwB;gBACnEgE,QAAQ;gBACR9C,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9C+C,MAAM/B,KAAKC,SAAS,CAAC;oBACnBlC,OAAOA,KAAK,CAAC,EAAE;oBACfiE,SAASjE,KAAK,CAAC,EAAE;oBACjBlB,MAAM;oBACNgE,IAAI1D,KAAKuC,GAAG;gBACd;YACF;YACA,MAAMuC,SAAS,MAAMJ,SAASK,IAAI,GAAGN,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;YACnD,IAAI,CAACC,SAASM,EAAE,EAAE,MAAM,IAAIC,cAAMH,0BAAAA,OAAQd,KAAK,mBAAI;QACrD,EAAE,OAAOA,OAAY;;YACnB,OAAO,KAAKxD,yBAAgBwD,yBAAAA,MAAOkB,OAAO,oBAAI,sBAAsB;gBAClErB,SAAS;YACX;QACF;QACArD,gBAAgB,gBAAgB;YAAEqD,SAAS;YAAWC,SAAS;QAAM;IACvE;IAEA,MAAMqB,WAAW,CAACnF,OAAc,GAAGK,SAAS,WAAW,EAAEL,KAAKuC,GAAG,EAAE;IAEnE,MAAM6C,aAAa,CAACpF,OAAoC;YACtD;gBACEqF,KAAK;gBACLxF,OAAO;gBACPyF,oBAAM,KAACjI;oBAAQkI,MAAMtI,cAAcsI,IAAI;oBAAEC,MAAM;;gBAC/CC,MAAMN,SAASnF;YACjB;YACA;gBACEqF,KAAK;gBACLxF,OAAO;gBACPyF,oBAAM,KAACjI;oBAAQkI,MAAMrI,iBAAiBqI,IAAI;oBAAEC,MAAM;;gBAClDC,MAAM,GAAGN,SAASnF,MAAM,KAAK,CAAC;YAChC;YACA;gBACEqF,KAAK;gBACLxF,OAAO;gBACPyF,oBAAM,KAACjI;oBAAQkI,MAAMvI,iBAAiBuI,IAAI;oBAAEC,MAAM;;gBAClDE,aAAa;gBACbC,SAAS,IAAM,KAAKzB,aAAalE;YACnC;SACD;IAED;;;;GAIC,GACD,MAAM4F,UAAwB;QAC5B;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,YAAY,CAAC,EAAEC,GAAG,EAAE,iBAClB,KAAC/I;oBACCsI,MAAMN,SAASe;oBACf,+DAA+D;oBAC/D,4CAA4C;oBAC5CP,SAAS,CAACQ,QACRA,MAAMC,eAAe;8BAGtBF,IAAIzG,IAAI;;QAGf;QACA;YACEoG,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVK,aAAa,CAACC,QAAQJ,MAAQA,IAAIjG,OAAO;YACzCgG,YAAY,CAAC,EAAEC,GAAG,EAAE;oBAcZA;uBAbNA,IAAIxG,IAAI,KAAK,0BACX,KAACtB;oBACCoH,MAAK;oBACLhB,OAAM;oBACNX,SAAQ;oBACR;;;;;;aAMC,GACDhE,OACEqG,EAAAA,uBAAAA,IAAIK,eAAe,qBAAnBL,qBAAqBM,MAAM,IACvB,CAAC,OAAO,EAAEN,IAAIK,eAAe,CAACC,MAAM,GAAGC,cAAc,IAAI,GACzD;mCAIR,KAACrI;oBAAKoH,MAAK;oBAAQ3B,SAAQ;oBAAWhE,OAAM;;;QAElD;QACA;YACEgG,OAAO;YACPC,YAAY;YACZY,OAAO;YACPL,aAAa,CAACC,QAAQJ;oBAAQlE;wBAAAA,kBAAAA,MAAM,CAACkE,IAAI3D,GAAG,CAAC,YAAfP,kBAAmB;;QACnD;QACArE,kBACE,CAACuI;gBAEiBA;iCADhB,KAACzI;gBACCoC,OAAO2D,QAAO0C,YAAAA,IAAIzG,IAAI,YAARyG,YAAYA,IAAI3D,GAAG;gBACjCoE,OAAOvB,WAAWc;;WAGtB;YAAEQ,OAAO;QAAG;KAEf;IAED,qBACE,KAACtJ;QACCwJ,QAAO;QACPC,MAAM9J,eAAe,kBAAkB;YAAE+J,QAAQ;QAAe;QAChEC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXC,sBACE,KAAChJ;gBACCqH,MAAK;gBACL3B,SAAQ;gBACRuD,UAAU,CAACxG,SAASwC;gBACpBuC,SAAS,IAAM1C,cAAc;0BAE5BG,WAAW,cAAc;;QAGhC;kBAEA,cAAA,MAAC/E;YAAMgJ,SAAS;;8BACd,KAAC/I;oBAAWuF,SAAQ;oBAAQW,OAAM;8BAC/B,oEACC,kEACA,qEACA;;8BAOJ,KAACzG;oBACCuJ,MAAMtE;oBACNuE,SAAS,IAAMtE,cAAc;oBAC7BmB,OAAM;oBACNoD,aAAY;oBACZC,UAAUnE;oBACVoE,WACExE,4BACE,KAAChF;wBAAMyJ,UAAS;wBAAQC,IAAI;4BAAEC,IAAI;4BAAGC,IAAI;wBAAE;kCACxC;yBAED;oBAENC,oBAAoB;;gBAErBhH,SAASiH,MAAM,KAAK,KAAK,CAAChH,WAAW,CAACU,WAAWuG,SAAS,GAAG,qBAC5D;;sCACE,KAAC1K,8BAAoBmE,WAAWwG,UAAU;wBACzCxG,WAAWuG,SAAS,IAAIjH,wBACvB,KAAC1C;4BAAWuF,SAAQ;4BAAUW,OAAM;sCACjC,CAAC,cAAc,EAAE9C,WAAWyG,IAAI,CAAC,8CAA8C,CAAC;6BAEjF;sCACJ,KAACzK;4BACC0K,cAAW;4BACXtH,MAAMiB;4BACN6D,SAASlE,WAAW2G,aAAa,CAACzC;4BAClC0C,WAAWzK;4BACX0K,QAAQ,CAACC,KAAKtC,MAAQ3F,OAAOwD,IAAI,CAACoB,SAASe;4BAC3C,8DAA8D;4BAC9DuC,UAAU;2BAGN/G,WAAWgH,SAAS;4BACxBC,aAAY;;sCAEd,KAACnL,6BAAmBkE,WAAWkH,UAAU;;;;;;AAMrD;AACA1I,aAAa2I,WAAW,GAAG;AAE3B,eAAe3I,aAAY"}
|
|
@@ -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)=>
|
|
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
|
|
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(()=>
|
|
167
|
-
|
|
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)=>
|
|
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)=>
|
|
274
|
-
renderCell: ({ value })=>value === '
|
|
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(
|
|
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:
|
|
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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"}
|