@aglyn/plugins-email 1.0.0-beta.232 → 1.0.0-beta.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -16
- package/src/lib/components/email-screens-card.js +33 -6
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/list-members-panel.js +38 -7
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/lists-card.js +35 -7
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.js +34 -7
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/suppressions-card.js +24 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/constants/list-queries.js +73 -9
- package/src/lib/constants/list-queries.js.map +1 -1
- package/src/lib/model/email-design-document.d.ts +1 -1
- package/src/lib/model/email-design-document.js +1 -1
- package/src/lib/model/email-design-document.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.233",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.233",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.233",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.233",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.233",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.233",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.233",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.233",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
|
|
36
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.233",
|
|
37
|
+
"@aglyn/shared-util-email": "1.0.0-beta.233",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.233",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.233",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.233",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.233",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.233",
|
|
43
43
|
"@swc/helpers": "0.5.23"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -29,10 +29,11 @@ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filt
|
|
|
29
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
30
|
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
|
|
31
31
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
32
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
32
33
|
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
33
34
|
import { collection, doc, updateDoc } from "firebase/firestore";
|
|
34
35
|
import { useRouter } from "next/navigation";
|
|
35
|
-
import { useMemo } from "react";
|
|
36
|
+
import { useMemo, useState } from "react";
|
|
36
37
|
import { EMAIL_TEMPLATE_BASE, EMAIL_TEMPLATE_FILTER_HEADERS, EMAIL_TEMPLATE_QUERY } from "../constants/list-queries.js";
|
|
37
38
|
import { templateProvenance } from "../model/template-provenance.js";
|
|
38
39
|
import { createEmailScreen } from "../utils/create-email-screen.js";
|
|
@@ -43,6 +44,12 @@ import { emailTemplateSoftDelete } from "../utils/email-template-soft-delete.js"
|
|
|
43
44
|
// pre-AGL-621/622 shape — so every "Edit"/"Design" jump out of the Emails
|
|
44
45
|
// page landed on a 404, including the one right after creating a new email
|
|
45
46
|
// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.
|
|
47
|
+
/** Origin sorts the loaded page: installed or yours (AGL-3680). */ const TEMPLATE_PAGE_SORTS = {
|
|
48
|
+
origin: (row)=>templateProvenance(row).origin === 'installed' ? 'installed' : 'local'
|
|
49
|
+
};
|
|
50
|
+
const TEMPLATE_SORT_HEADERS = {
|
|
51
|
+
origin: 'Origin'
|
|
52
|
+
};
|
|
46
53
|
const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCREEN_BESIGNER, {
|
|
47
54
|
orgSlug,
|
|
48
55
|
host,
|
|
@@ -108,12 +115,19 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
108
115
|
});
|
|
109
116
|
const { confirm } = useConfirmationContext();
|
|
110
117
|
const gridFilter = useListGridFilter();
|
|
118
|
+
/*
|
|
119
|
+
* EVERY HEADER SORTS (AGL-3680). Template orders the QUERY by `nameLower`,
|
|
120
|
+
* A to Z by default and Z to A while nothing narrows the list; Origin is
|
|
121
|
+
* the presence of the install provenance, which no query can order by, so
|
|
122
|
+
* it sorts the page on screen and says so.
|
|
123
|
+
*/ const [askedSort, setAskedSort] = useState(EMAIL_TEMPLATE_QUERY.sorts[0]);
|
|
111
124
|
const { rows: emailScreens, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
112
125
|
collection: collection(firestore, 'hosts', hostId, 'screens'),
|
|
113
126
|
declaration: EMAIL_TEMPLATE_QUERY,
|
|
114
127
|
request: {
|
|
115
128
|
clauses: gridFilter.clauses,
|
|
116
129
|
search: gridFilter.searchWords,
|
|
130
|
+
sort: askedSort,
|
|
117
131
|
base: EMAIL_TEMPLATE_BASE
|
|
118
132
|
},
|
|
119
133
|
deps: [
|
|
@@ -122,6 +136,16 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
122
136
|
],
|
|
123
137
|
idField: '$id'
|
|
124
138
|
});
|
|
139
|
+
const columnSort = useListColumnSort({
|
|
140
|
+
sorts: EMAIL_TEMPLATE_QUERY.sorts,
|
|
141
|
+
defaultSort: EMAIL_TEMPLATE_QUERY.sorts[0],
|
|
142
|
+
sort: askedSort,
|
|
143
|
+
onSortChange: setAskedSort,
|
|
144
|
+
orderBy: plan.orderBy,
|
|
145
|
+
rows: emailScreens,
|
|
146
|
+
pageSorts: TEMPLATE_PAGE_SORTS,
|
|
147
|
+
headers: TEMPLATE_SORT_HEADERS
|
|
148
|
+
});
|
|
125
149
|
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
126
150
|
const refusals = useMemo(()=>listQueryRefusals(plan.refused, {
|
|
127
151
|
fields: EMAIL_TEMPLATE_QUERY.fields,
|
|
@@ -308,23 +332,26 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
308
332
|
}),
|
|
309
333
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
310
334
|
refused: refusals,
|
|
311
|
-
notices:
|
|
335
|
+
notices: [
|
|
336
|
+
...plan.notices,
|
|
337
|
+
...columnSort.notices
|
|
338
|
+
]
|
|
312
339
|
}),
|
|
313
340
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
314
341
|
"aria-label": "Email templates",
|
|
315
|
-
rows:
|
|
342
|
+
rows: columnSort.rows,
|
|
343
|
+
columnSort: columnSort,
|
|
316
344
|
columns: listFilterGridColumns(columns, EMAIL_TEMPLATE_QUERY.fields, {}, EMAIL_TEMPLATE_FILTER_HEADERS),
|
|
317
345
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
318
346
|
onOpen: (_id, row)=>router.push(templateHref(row)),
|
|
319
347
|
// Paged by the footer below, so the grid must not also slice.
|
|
320
348
|
hideFooter: true,
|
|
321
|
-
// The panel and the
|
|
322
|
-
//
|
|
349
|
+
// The panel, the search and the Template header go to the
|
|
350
|
+
// query (Origin sorts the page, and says so).
|
|
323
351
|
filterMode: "server",
|
|
324
352
|
filterModel: gridFilter.filterModel,
|
|
325
353
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
326
354
|
quickFilter: true,
|
|
327
|
-
disableColumnSorting: true,
|
|
328
355
|
noRowsLabel: "No templates match these filters"
|
|
329
356
|
}),
|
|
330
357
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { HOST_EMAIL_TEMPLATES_ZONE } from './email-zones'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */\n orgId?: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, orgId, basePath } = props\n /**\n * The shell's zone renderer (AGL-3596), for other ways to start a design —\n * the `hostEmailTemplates` zone this plugin declares; `null` outside the\n * console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n const createZone = CreateZone ? (\n <CreateZone slot={HOST_EMAIL_TEMPLATES_ZONE.id} hostId={hostId} orgId={orgId} />\n ) : null\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n </Stack>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n {/* Other ways to start a design, the header's zone again. */}\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button variant=\"contained\" onClick={() => void handleCreate()}>\n {'Create your first template'}\n </Button>\n </Stack>\n </>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","useConsoleWidgetSlot","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","HOST_EMAIL_TEMPLATES_ZONE","emailTemplateSoftDelete","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","orgId","basePath","CreateZone","createZone","slot","id","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","base","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","value","event","stopPropagation","width","origin","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","dialog","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo, useState } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { HOST_EMAIL_TEMPLATES_ZONE } from './email-zones'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\n/** Origin sorts the loaded page: installed or yours (AGL-3680). */\nconst TEMPLATE_PAGE_SORTS = {\n origin: (row: any) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n}\nconst TEMPLATE_SORT_HEADERS = { origin: 'Origin' }\n\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */\n orgId?: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, orgId, basePath } = props\n /**\n * The shell's zone renderer (AGL-3596), for other ways to start a design —\n * the `hostEmailTemplates` zone this plugin declares; `null` outside the\n * console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n const createZone = CreateZone ? (\n <CreateZone slot={HOST_EMAIL_TEMPLATES_ZONE.id} hostId={hostId} orgId={orgId} />\n ) : null\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n /*\n * EVERY HEADER SORTS (AGL-3680). Template orders the QUERY by `nameLower`,\n * A to Z by default and Z to A while nothing narrows the list; Origin is\n * the presence of the install provenance, which no query can order by, so\n * it sorts the page on screen and says so.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(\n EMAIL_TEMPLATE_QUERY.sorts[0],\n )\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const columnSort = useListColumnSort<any>({\n sorts: EMAIL_TEMPLATE_QUERY.sorts,\n defaultSort: EMAIL_TEMPLATE_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: emailScreens,\n pageSorts: TEMPLATE_PAGE_SORTS,\n headers: TEMPLATE_SORT_HEADERS,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n </Stack>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n {/* Other ways to start a design, the header's zone again. */}\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button variant=\"contained\" onClick={() => void handleCreate()}>\n {'Create your first template'}\n </Button>\n </Stack>\n </>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Email templates\"\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the Template header go to the\n // query (Origin sorts the page, and says so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","useConsoleWidgetSlot","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","useListColumnSort","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","HOST_EMAIL_TEMPLATES_ZONE","emailTemplateSoftDelete","TEMPLATE_PAGE_SORTS","origin","row","TEMPLATE_SORT_HEADERS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","orgId","basePath","CreateZone","createZone","slot","id","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","askedSort","setAskedSort","sorts","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","sort","base","deps","idField","columnSort","defaultSort","onSortChange","orderBy","pageSorts","headers","filtering","length","some","word","trim","refusals","refused","fields","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","value","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","dialog","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
|
|
@@ -74,6 +74,7 @@ import { collection, deleteDoc, doc } from "firebase/firestore";
|
|
|
74
74
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
75
75
|
import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
|
|
76
76
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
77
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
77
78
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
78
79
|
import { LIST_MEMBER_FILTER_HEADERS, LIST_MEMBER_FILTER_OPTIONS, LIST_MEMBER_QUERY } from "../constants/list-queries.js";
|
|
79
80
|
function consentLabel(member, group) {
|
|
@@ -134,6 +135,9 @@ function consentLabel(member, group) {
|
|
|
134
135
|
/** The How filter's choices are picked, so the panel shows a select. */ const MEMBER_SELECT_FIELDS = [
|
|
135
136
|
'via'
|
|
136
137
|
];
|
|
138
|
+
/** The page-sorted header, for its notice (AGL-3680). */ const MEMBER_SORT_HEADERS = {
|
|
139
|
+
consent: 'Consent'
|
|
140
|
+
};
|
|
137
141
|
/** How a row reads `via`: a row that predates the field was added by hand. */ const viaOf = (member)=>member.via === 'rule' ? 'rule' : 'manual';
|
|
138
142
|
/** Free text — a paste, a typed address, commas or newlines — to addresses. */ export function splitAddresses(value) {
|
|
139
143
|
return value.split(/[\s,;]+/).map((entry)=>entry.trim())// A pasted mail-client line is `Name <a@b.co>`; keep the address part.
|
|
@@ -155,7 +159,9 @@ export function ListMembersPanel(props) {
|
|
|
155
159
|
* `list-members.ts` still adopts predates it. Ordering on it would drop the
|
|
156
160
|
* oldest members from their own list, silently, which is the failure that
|
|
157
161
|
* turned an audience into a random sample (AGL-2501). The id is the one key
|
|
158
|
-
* every row has.
|
|
162
|
+
* every row has, so it stays the default. The Joined header asks for
|
|
163
|
+
* `addedAt` only since the writer stamps it on every row it touches and
|
|
164
|
+
* `backfill-list-member-sort-fields.mjs` on the rest (AGL-3680).
|
|
159
165
|
*
|
|
160
166
|
* Every filter and search word is a predicate on the same query
|
|
161
167
|
* (AGL-3321) — see `LIST_MEMBER_QUERY` — so a page is a page of the
|
|
@@ -163,12 +169,20 @@ export function ListMembersPanel(props) {
|
|
|
163
169
|
*/ const gridFilter = useListGridFilter({
|
|
164
170
|
selectFields: MEMBER_SELECT_FIELDS
|
|
165
171
|
});
|
|
172
|
+
/*
|
|
173
|
+
* EVERY HEADER SORTS (AGL-3680). Address, Name, Joined and How order the
|
|
174
|
+
* QUERY — `enrollListMember` stamps each on every row, null where there is
|
|
175
|
+
* none, so no order drops a member — each `alone`: under a filter or the
|
|
176
|
+
* search the list keeps its id order and says so. Consent is read per
|
|
177
|
+
* viewing consent group, so it sorts the page on screen.
|
|
178
|
+
*/ const [askedSort, setAskedSort] = useState(null);
|
|
166
179
|
const { rows: members, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
167
180
|
collection: collection(firestore, scope[0], scope[1], 'lists', listId, 'members'),
|
|
168
181
|
declaration: LIST_MEMBER_QUERY,
|
|
169
182
|
request: {
|
|
170
183
|
clauses: gridFilter.clauses,
|
|
171
|
-
search: gridFilter.searchWords
|
|
184
|
+
search: gridFilter.searchWords,
|
|
185
|
+
sort: askedSort
|
|
172
186
|
},
|
|
173
187
|
deps: [
|
|
174
188
|
firestore,
|
|
@@ -178,6 +192,20 @@ export function ListMembersPanel(props) {
|
|
|
178
192
|
],
|
|
179
193
|
idField: '$id'
|
|
180
194
|
});
|
|
195
|
+
const pageSorts = useMemo(()=>({
|
|
196
|
+
consent: (row)=>consentLabel(row, consentGroup).label
|
|
197
|
+
}), [
|
|
198
|
+
consentGroup
|
|
199
|
+
]);
|
|
200
|
+
const columnSort = useListColumnSort({
|
|
201
|
+
sorts: LIST_MEMBER_QUERY.sorts,
|
|
202
|
+
sort: askedSort,
|
|
203
|
+
onSortChange: setAskedSort,
|
|
204
|
+
orderBy: plan.orderBy,
|
|
205
|
+
rows: members,
|
|
206
|
+
pageSorts,
|
|
207
|
+
headers: MEMBER_SORT_HEADERS
|
|
208
|
+
});
|
|
181
209
|
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
182
210
|
const { onFilterChange } = props;
|
|
183
211
|
const filterKey = filtering ? JSON.stringify(plan.filters) : '';
|
|
@@ -682,22 +710,25 @@ export function ListMembersPanel(props) {
|
|
|
682
710
|
}),
|
|
683
711
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
684
712
|
refused: refusals,
|
|
685
|
-
notices:
|
|
713
|
+
notices: [
|
|
714
|
+
...plan.notices,
|
|
715
|
+
...columnSort.notices
|
|
716
|
+
]
|
|
686
717
|
}),
|
|
687
718
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
688
719
|
"aria-label": `Members of ${listName}`,
|
|
689
|
-
rows:
|
|
720
|
+
rows: columnSort.rows,
|
|
721
|
+
columnSort: columnSort,
|
|
690
722
|
columns: listFilterGridColumns(columns, LIST_MEMBER_QUERY.fields, LIST_MEMBER_FILTER_OPTIONS, LIST_MEMBER_FILTER_HEADERS),
|
|
691
723
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
692
724
|
// Paged by the footer below, so the grid must not also slice.
|
|
693
725
|
hideFooter: true,
|
|
694
|
-
// The panel and the
|
|
695
|
-
//
|
|
726
|
+
// The panel, the search and the headers go to the query (Consent
|
|
727
|
+
// sorts the page, and says so); the grid itself sorts nothing.
|
|
696
728
|
filterMode: "server",
|
|
697
729
|
filterModel: gridFilter.filterModel,
|
|
698
730
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
699
731
|
quickFilter: true,
|
|
700
|
-
disableColumnSorting: true,
|
|
701
732
|
noRowsLabel: "No members match these filters"
|
|
702
733
|
}),
|
|
703
734
|
/*#__PURE__*/ _jsx(ListPagination, {
|