@aglyn/plugins-email 1.0.0-beta.234 → 1.0.0-beta.236
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 +2 -2
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/email-template-detail.js +2 -4
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/email-topic-detail.js +1 -1
- package/src/lib/components/email-topic-detail.js.map +1 -1
- package/src/lib/components/list-detail-card.js +5 -6
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-edit-card.js +1 -1
- package/src/lib/components/list-edit-card.js.map +1 -1
- package/src/lib/components/lists-card.js +2 -2
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.js +4 -4
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/org-sending-card.js +1 -1
- package/src/lib/components/org-sending-card.js.map +1 -1
- package/src/lib/components/org-sending-domain-detail.js +1 -1
- package/src/lib/components/org-sending-domain-detail.js.map +1 -1
- package/src/lib/components/org-suppressions-card.js +1 -1
- package/src/lib/components/org-suppressions-card.js.map +1 -1
- package/src/lib/components/sending-domain-detail.js +1 -1
- package/src/lib/components/sending-domain-detail.js.map +1 -1
- package/src/lib/components/sending-domains-card.js +1 -3
- package/src/lib/components/sending-domains-card.js.map +1 -1
- package/src/lib/components/sending-sender-drawer.js +1 -3
- package/src/lib/components/sending-sender-drawer.js.map +1 -1
- package/src/lib/components/site-consent-group-card.js +1 -1
- package/src/lib/components/site-consent-group-card.js.map +1 -1
- package/src/lib/components/suppressions-card.js +1 -1
- package/src/lib/components/suppressions-card.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.236",
|
|
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.236",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.236",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.236",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.236",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.236",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.236",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.236",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.236",
|
|
36
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.236",
|
|
37
|
+
"@aglyn/shared-util-email": "1.0.0-beta.236",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.236",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.236",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.236",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.236",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.236",
|
|
43
43
|
"@swc/helpers": "0.5.23"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
19
19
|
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
20
|
-
import { mdiDeleteOutline, mdiContentCopy,
|
|
20
|
+
import { mdiDeleteOutline, mdiContentCopy, mdiPageNextOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
|
|
21
21
|
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
23
23
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
@@ -190,7 +190,7 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
190
190
|
key: 'details',
|
|
191
191
|
label: 'Open details',
|
|
192
192
|
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
193
|
-
path:
|
|
193
|
+
path: mdiPageNextOutline.path,
|
|
194
194
|
size: 0.8
|
|
195
195
|
}),
|
|
196
196
|
href: templateHref(screen)
|
|
@@ -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 { 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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"}
|
|
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 mdiPageNextOutline,\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={mdiPageNextOutline.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","mdiPageNextOutline","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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"}
|
|
@@ -31,12 +31,10 @@ import { EMAIL_TEMPLATE_RECIPIENTS_ZONE, EMAIL_TEMPLATE_REPORT_ZONE } from "./em
|
|
|
31
31
|
import { Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
32
32
|
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
33
33
|
const previewDocsHelp = pluginDocsHelp('designedEmails', {
|
|
34
|
-
anchor: '#
|
|
35
|
-
excerpt: 'The template as an inbox receives it, drawn by the same renderer the ' + 'send path uses. Merge tokens are left standing — a real send fills them ' + 'from each recipient.'
|
|
34
|
+
anchor: '#template-preview'
|
|
36
35
|
});
|
|
37
36
|
const templateDocsHelp = pluginDocsHelp('designedEmails', {
|
|
38
|
-
anchor: '#
|
|
39
|
-
excerpt: 'One email template: its name, where it came from, and what it looks ' + 'like. The emails sent from it are reported beneath, by the plugin that ' + 'sends them.'
|
|
37
|
+
anchor: '#template-page'
|
|
40
38
|
});
|
|
41
39
|
/**
|
|
42
40
|
* ONE EMAIL TEMPLATE: what it looks like, and what it has done.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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 {\n buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { templateProvenance } from '../model/template-provenance'\nimport EmailDesignPreview from './email-design-preview'\nimport {\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './email-zones'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'The template as an inbox receives it, drawn by the same renderer the ' +\n 'send path uses. Merge tokens are left standing — a real send fills them ' +\n 'from each recipient.',\n})\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'One email template: its name, where it came from, and what it looks ' +\n 'like. The emails sent from it are reported beneath, by the plugin that ' +\n 'sends them.',\n})\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document and its published version. The messages sent from the\n * template are a send's, and sends are the campaign owner's: this page hosts\n * two zones that plugin fills — the report of what those messages did, and\n * who received them — handing over the site, the template and this page's\n * base path, and reads none of the sends itself (AGL-3080).\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const Zone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n </Stack>\n </CardDisplay>\n\n {/*\n What the emails sent from this template did, then who received them:\n each its own boundary, because each card's code arrives after the\n card above it has drawn and must not send it back to a loading state.\n */}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_REPORT_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n basePath={basePath}\n />\n </Suspense>\n ) : null}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","MdiIcon","useConsoleHostRoute","useFirestore","useFirestoreDoc","Alert","Button","Stack","TextField","Typography","doc","updateDoc","Suspense","useMemo","useState","useSnackbar","templateProvenance","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","Section","useConsoleWidgetSlot","previewDocsHelp","anchor","excerpt","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","Zone","firestore","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","provenance","listUrl","besignerUrl","SCREEN_BESIGNER","host","headerActions","direction","spacing","useFlexGap","sx","alignItems","flexWrap","component","componentVariant","nativeButton","href","size","color","startIcon","path","disabled","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","undefined","note","severity","warn","label","value","String","onChange","event","target","minWidth","helperText","onClick","fallback","slot","id","nodes","loading","Boolean","subject","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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 {\n buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { templateProvenance } from '../model/template-provenance'\nimport EmailDesignPreview from './email-design-preview'\nimport {\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './email-zones'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', { anchor: '#template-preview' })\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', { anchor: '#template-page' })\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document and its published version. The messages sent from the\n * template are a send's, and sends are the campaign owner's: this page hosts\n * two zones that plugin fills — the report of what those messages did, and\n * who received them — handing over the site, the template and this page's\n * base path, and reads none of the sends itself (AGL-3080).\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const Zone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n </Stack>\n </CardDisplay>\n\n {/*\n What the emails sent from this template did, then who received them:\n each its own boundary, because each card's code arrives after the\n card above it has drawn and must not send it back to a loading state.\n */}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_REPORT_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n basePath={basePath}\n />\n </Suspense>\n ) : null}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","MdiIcon","useConsoleHostRoute","useFirestore","useFirestoreDoc","Alert","Button","Stack","TextField","Typography","doc","updateDoc","Suspense","useMemo","useState","useSnackbar","templateProvenance","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","Section","useConsoleWidgetSlot","previewDocsHelp","anchor","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","Zone","firestore","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","provenance","listUrl","besignerUrl","SCREEN_BESIGNER","host","headerActions","direction","spacing","useFlexGap","sx","alignItems","flexWrap","component","componentVariant","nativeButton","href","size","color","startIcon","path","disabled","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","undefined","note","severity","warn","label","value","String","onChange","event","target","minWidth","helperText","onClick","fallback","slot","id","nodes","loading","Boolean","subject","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-topic-detail.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 {\n PageHeaderRecord,\n pluginDocsHelp,\n resolveSubscriptionTopic,\n DECLARED_SUBSCRIPTION_TOPICS,\n} from '@aglyn/aglyn'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport Link from 'next/link'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useEmailOrgMount } from './email-org-mount'\nimport { useOrgEmailTopics, writeEmailTopic } from './use-org-email-topics'\n\nexport interface EmailTopicDetailProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n topicId: string\n /** `/…/emails`, so the page can route back to the list. */\n basePath: string\n}\n\n/**\n * One topic's own page — where every change to an existing topic is made.\n *\n * The console's shape for a list surface is create-in-a-drawer, edit-on-the\n * record's page, and this is the second half of it for topics. The list card\n * lists and routes; nothing about a saved topic is editable from a form\n * stacked above that table.\n *\n * ## Saving a built-in\n *\n * The four built-ins have no stored document until somebody changes one, so\n * this page's save is the same `setDoc` for a built-in and a custom topic:\n * the write CREATES the override document at the built-in's id. That is the\n * whole overlay design — see `subscription-topics.ts`.\n *\n * ## Retire, not delete\n *\n * A retired topic leaves the composer's picker and the recipient's preference\n * page, and stays resolvable everywhere else. It has to: campaigns already\n * sent under it minted unsubscribe links carrying its id, those links are in\n * inboxes, and an id that stops resolving is a preference page that cannot\n * name the message the recipient is holding. There is no delete on this page,\n * and the Firestore rules refuse one too.\n */\nexport function EmailTopicDetail(props: EmailTopicDetailProps) {\n const { hostId, topicId, basePath } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The catalog is the org's: read through the site under one, and straight\n // from the organization on its own page.\n const orgMount = useEmailOrgMount()\n const { topics, scope } = useOrgEmailTopics(hostId, {\n orgId: orgMount?.orgId,\n })\n\n /*\n * `resolveSubscriptionTopic` rather than a bare `find`, so an id that no longer\n * names anything lands on a real topic instead of an empty form. A stale\n * bookmark is the common way to arrive here with one.\n */\n const topic = resolveSubscriptionTopic(topicId, topics)\n const known = topics.some((it) => it.id === topicId)\n\n const [name, setName] = useState('')\n const [description, setDescription] = useState('')\n /**\n * Whether joining this stream needs a confirmation click.\n *\n * Three states, held as a string because a `Select` carries one and a\n * boolean cannot: `''` defers to the site, `'on'` and `'off'` are decisions\n * about this stream. Collapsing the first into a boolean would make \"not\n * chosen\" indistinguishable from \"off\", and the site default would then be\n * unreachable once anybody saved a topic.\n */\n const [doubleOptIn, setDoubleOptIn] = useState('')\n const [loaded, setLoaded] = useState(false)\n const [busy, setBusy] = useState(false)\n\n /*\n * Seed the fields ONCE, when the catalog first arrives.\n *\n * Re-seeding on every change of `topic` would overwrite what the operator is\n * typing the moment the collection listener fires — and it fires on this\n * page's own save.\n */\n useEffect(() => {\n if (loaded || !topics.length) return\n setName(topic.name)\n setDescription(topic.description)\n setDoubleOptIn(\n typeof topic.doubleOptIn === 'boolean'\n ? topic.doubleOptIn\n ? 'on'\n : 'off'\n : '',\n )\n setLoaded(true)\n }, [loaded, topics, topic])\n\n const storedDoubleOptIn =\n typeof topic.doubleOptIn === 'boolean'\n ? topic.doubleOptIn\n ? 'on'\n : 'off'\n : ''\n const dirty =\n loaded &&\n (name !== topic.name ||\n description !== topic.description ||\n doubleOptIn !== storedDoubleOptIn)\n\n /**\n * Save what is in the form, optionally changing the retired flag with it.\n *\n * The write itself is `writeEmailTopic`, which the list card's row menu also\n * goes through — a topic retired from the list and one retired here have to\n * leave the same document behind.\n */\n const save = useCallback(\n async (patch?: { archived?: boolean }) => {\n if (!scope || busy) return\n const nextName = name.trim()\n if (!nextName) {\n return void enqueueSnackbar('Give the topic a name', {\n variant: 'warning',\n persist: false,\n })\n }\n setBusy(true)\n try {\n await writeEmailTopic(firestore, scope, {\n id: topic.id,\n name: nextName,\n description: description.trim(),\n archived: patch?.archived ?? !!topic.archived,\n // `null` clears the field, which is how \"whatever the site says\"\n // is expressed. A stored `false` is a different answer and has to\n // survive as one.\n doubleOptIn:\n doubleOptIn === 'on' ? true : doubleOptIn === 'off' ? false : null,\n })\n enqueueSnackbar('Topic saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n },\n [\n scope,\n busy,\n name,\n description,\n doubleOptIn,\n firestore,\n topic,\n enqueueSnackbar,\n ],\n )\n\n const toggleArchived = useCallback(async () => {\n if (topic.archived) return void save({ archived: false })\n const accepted = await confirm({\n title: `Retire “${topic.name}”?`,\n description:\n 'It leaves the composer and the preference page, so nothing new can ' +\n 'be sent under it and recipients stop seeing it as a choice. ' +\n 'Campaigns already sent under it keep working — their unsubscribe ' +\n 'links still name this topic. You can bring it back at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating on\n // the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n await save({ archived: true })\n }, [topic, confirm, save])\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Topic'}\n subheader={\n <Stack direction=\"row\" spacing={1} sx={{ mt: 1, alignItems: 'center' }}>\n {topic.archived ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Retired\"\n />\n ) : null}\n {DECLARED_SUBSCRIPTION_TOPICS.some((it) => it.id === topic.id) ? (\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Built in'}\n </Typography>\n ) : null}\n </Stack>\n }\n help={pluginDocsHelp('emailCampaigns', { anchor: '#topics' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button component={Link} href={`${basePath}/topics`} size=\"small\">\n {'All topics'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n {!scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site has no organization, so it has no topic list.'}\n </Typography>\n ) : (\n <>\n {!known && loaded ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'That topic no longer exists, so this is the one campaigns ' +\n 'fall back to.'}\n </Typography>\n ) : null}\n <TextField\n label=\"Name\"\n size=\"small\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n disabled={busy || !loaded}\n helperText=\"What recipients see on the preference page.\"\n />\n <TextField\n label=\"Description\"\n size=\"small\"\n multiline\n minRows={2}\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n disabled={busy || !loaded}\n helperText=\"One sentence telling them what this stream is.\"\n />\n {/*\n CONFIRMATION — off unless somebody chooses it.\n\n No jurisdiction verified requires a double opt-in by statute.\n They require prior express consent plus a burden of proof on\n the sender, and a confirmation click is the proof the courts\n have so far accepted — so this is an option a merchant reaches\n for, never a default the product imposes.\n\n A three-way select rather than a switch, because the third\n state is real: this topic can defer to the site's own setting,\n and a switch has nowhere to put that.\n */}\n <TextField\n select\n label=\"Confirmation\"\n size=\"small\"\n value={doubleOptIn}\n onChange={(event) => setDoubleOptIn(event.target.value)}\n disabled={busy || !loaded}\n helperText={\n 'Ask new subscribers to confirm by email before anything is ' +\n 'sent to them. Until they do, nothing on this stream reaches ' +\n 'them and they stay on your list.'\n }\n slotProps={{ select: { native: true } }}\n >\n <option value=\"\">{'Use the site setting'}</option>\n <option value=\"on\">{'Require a confirmation click'}</option>\n <option value=\"off\">{'No confirmation for this stream'}</option>\n </TextField>\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n disabled={!dirty || busy}\n onClick={() => void save()}\n >\n {'Save'}\n </Button>\n <Button\n color={topic.archived ? 'primary' : 'error'}\n disabled={busy || !loaded}\n onClick={() => void toggleArchived()}\n >\n {topic.archived ? 'Restore' : 'Retire'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the topic; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={topic.name || topic.id} />\n {card}\n </>\n )\n}\n\nexport default EmailTopicDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","resolveSubscriptionTopic","DECLARED_SUBSCRIPTION_TOPICS","CardDisplay","useConfirmationContext","useSnackbar","Button","Chip","Stack","TextField","Typography","Link","useCallback","useEffect","useState","useFirestore","useEmailOrgMount","useOrgEmailTopics","writeEmailTopic","EmailTopicDetail","props","hostId","topicId","basePath","firestore","enqueueSnackbar","confirm","orgMount","topics","scope","orgId","topic","known","some","it","id","name","setName","description","setDescription","doubleOptIn","setDoubleOptIn","loaded","setLoaded","busy","setBusy","length","storedDoubleOptIn","dirty","save","patch","nextName","trim","variant","persist","archived","error","console","toggleArchived","accepted","title","confirmationText","then","catch","card","header","subheader","direction","spacing","sx","mt","alignItems","size","color","label","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","component","href","value","onChange","event","target","disabled","helperText","multiline","minRows","select","slotProps","native","option","onClick"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-topic-detail.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 {\n PageHeaderRecord,\n pluginDocsHelp,\n resolveSubscriptionTopic,\n DECLARED_SUBSCRIPTION_TOPICS,\n} from '@aglyn/aglyn'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport Link from 'next/link'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useEmailOrgMount } from './email-org-mount'\nimport { useOrgEmailTopics, writeEmailTopic } from './use-org-email-topics'\n\nexport interface EmailTopicDetailProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n topicId: string\n /** `/…/emails`, so the page can route back to the list. */\n basePath: string\n}\n\n/**\n * One topic's own page — where every change to an existing topic is made.\n *\n * The console's shape for a list surface is create-in-a-drawer, edit-on-the\n * record's page, and this is the second half of it for topics. The list card\n * lists and routes; nothing about a saved topic is editable from a form\n * stacked above that table.\n *\n * ## Saving a built-in\n *\n * The four built-ins have no stored document until somebody changes one, so\n * this page's save is the same `setDoc` for a built-in and a custom topic:\n * the write CREATES the override document at the built-in's id. That is the\n * whole overlay design — see `subscription-topics.ts`.\n *\n * ## Retire, not delete\n *\n * A retired topic leaves the composer's picker and the recipient's preference\n * page, and stays resolvable everywhere else. It has to: campaigns already\n * sent under it minted unsubscribe links carrying its id, those links are in\n * inboxes, and an id that stops resolving is a preference page that cannot\n * name the message the recipient is holding. There is no delete on this page,\n * and the Firestore rules refuse one too.\n */\nexport function EmailTopicDetail(props: EmailTopicDetailProps) {\n const { hostId, topicId, basePath } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The catalog is the org's: read through the site under one, and straight\n // from the organization on its own page.\n const orgMount = useEmailOrgMount()\n const { topics, scope } = useOrgEmailTopics(hostId, {\n orgId: orgMount?.orgId,\n })\n\n /*\n * `resolveSubscriptionTopic` rather than a bare `find`, so an id that no longer\n * names anything lands on a real topic instead of an empty form. A stale\n * bookmark is the common way to arrive here with one.\n */\n const topic = resolveSubscriptionTopic(topicId, topics)\n const known = topics.some((it) => it.id === topicId)\n\n const [name, setName] = useState('')\n const [description, setDescription] = useState('')\n /**\n * Whether joining this stream needs a confirmation click.\n *\n * Three states, held as a string because a `Select` carries one and a\n * boolean cannot: `''` defers to the site, `'on'` and `'off'` are decisions\n * about this stream. Collapsing the first into a boolean would make \"not\n * chosen\" indistinguishable from \"off\", and the site default would then be\n * unreachable once anybody saved a topic.\n */\n const [doubleOptIn, setDoubleOptIn] = useState('')\n const [loaded, setLoaded] = useState(false)\n const [busy, setBusy] = useState(false)\n\n /*\n * Seed the fields ONCE, when the catalog first arrives.\n *\n * Re-seeding on every change of `topic` would overwrite what the operator is\n * typing the moment the collection listener fires — and it fires on this\n * page's own save.\n */\n useEffect(() => {\n if (loaded || !topics.length) return\n setName(topic.name)\n setDescription(topic.description)\n setDoubleOptIn(\n typeof topic.doubleOptIn === 'boolean'\n ? topic.doubleOptIn\n ? 'on'\n : 'off'\n : '',\n )\n setLoaded(true)\n }, [loaded, topics, topic])\n\n const storedDoubleOptIn =\n typeof topic.doubleOptIn === 'boolean'\n ? topic.doubleOptIn\n ? 'on'\n : 'off'\n : ''\n const dirty =\n loaded &&\n (name !== topic.name ||\n description !== topic.description ||\n doubleOptIn !== storedDoubleOptIn)\n\n /**\n * Save what is in the form, optionally changing the retired flag with it.\n *\n * The write itself is `writeEmailTopic`, which the list card's row menu also\n * goes through — a topic retired from the list and one retired here have to\n * leave the same document behind.\n */\n const save = useCallback(\n async (patch?: { archived?: boolean }) => {\n if (!scope || busy) return\n const nextName = name.trim()\n if (!nextName) {\n return void enqueueSnackbar('Give the topic a name', {\n variant: 'warning',\n persist: false,\n })\n }\n setBusy(true)\n try {\n await writeEmailTopic(firestore, scope, {\n id: topic.id,\n name: nextName,\n description: description.trim(),\n archived: patch?.archived ?? !!topic.archived,\n // `null` clears the field, which is how \"whatever the site says\"\n // is expressed. A stored `false` is a different answer and has to\n // survive as one.\n doubleOptIn:\n doubleOptIn === 'on' ? true : doubleOptIn === 'off' ? false : null,\n })\n enqueueSnackbar('Topic saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n },\n [\n scope,\n busy,\n name,\n description,\n doubleOptIn,\n firestore,\n topic,\n enqueueSnackbar,\n ],\n )\n\n const toggleArchived = useCallback(async () => {\n if (topic.archived) return void save({ archived: false })\n const accepted = await confirm({\n title: `Retire “${topic.name}”?`,\n description:\n 'It leaves the composer and the preference page, so nothing new can ' +\n 'be sent under it and recipients stop seeing it as a choice. ' +\n 'Campaigns already sent under it keep working — their unsubscribe ' +\n 'links still name this topic. You can bring it back at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating on\n // the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n await save({ archived: true })\n }, [topic, confirm, save])\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Topic'}\n subheader={\n <Stack direction=\"row\" spacing={1} sx={{ mt: 1, alignItems: 'center' }}>\n {topic.archived ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Retired\"\n />\n ) : null}\n {DECLARED_SUBSCRIPTION_TOPICS.some((it) => it.id === topic.id) ? (\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Built in'}\n </Typography>\n ) : null}\n </Stack>\n }\n help={pluginDocsHelp('emailCampaigns', { anchor: '#preference-page' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button component={Link} href={`${basePath}/topics`} size=\"small\">\n {'All topics'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n {!scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site has no organization, so it has no topic list.'}\n </Typography>\n ) : (\n <>\n {!known && loaded ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'That topic no longer exists, so this is the one campaigns ' +\n 'fall back to.'}\n </Typography>\n ) : null}\n <TextField\n label=\"Name\"\n size=\"small\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n disabled={busy || !loaded}\n helperText=\"What recipients see on the preference page.\"\n />\n <TextField\n label=\"Description\"\n size=\"small\"\n multiline\n minRows={2}\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n disabled={busy || !loaded}\n helperText=\"One sentence telling them what this stream is.\"\n />\n {/*\n CONFIRMATION — off unless somebody chooses it.\n\n No jurisdiction verified requires a double opt-in by statute.\n They require prior express consent plus a burden of proof on\n the sender, and a confirmation click is the proof the courts\n have so far accepted — so this is an option a merchant reaches\n for, never a default the product imposes.\n\n A three-way select rather than a switch, because the third\n state is real: this topic can defer to the site's own setting,\n and a switch has nowhere to put that.\n */}\n <TextField\n select\n label=\"Confirmation\"\n size=\"small\"\n value={doubleOptIn}\n onChange={(event) => setDoubleOptIn(event.target.value)}\n disabled={busy || !loaded}\n helperText={\n 'Ask new subscribers to confirm by email before anything is ' +\n 'sent to them. Until they do, nothing on this stream reaches ' +\n 'them and they stay on your list.'\n }\n slotProps={{ select: { native: true } }}\n >\n <option value=\"\">{'Use the site setting'}</option>\n <option value=\"on\">{'Require a confirmation click'}</option>\n <option value=\"off\">{'No confirmation for this stream'}</option>\n </TextField>\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n disabled={!dirty || busy}\n onClick={() => void save()}\n >\n {'Save'}\n </Button>\n <Button\n color={topic.archived ? 'primary' : 'error'}\n disabled={busy || !loaded}\n onClick={() => void toggleArchived()}\n >\n {topic.archived ? 'Restore' : 'Retire'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the topic; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={topic.name || topic.id} />\n {card}\n </>\n )\n}\n\nexport default EmailTopicDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","resolveSubscriptionTopic","DECLARED_SUBSCRIPTION_TOPICS","CardDisplay","useConfirmationContext","useSnackbar","Button","Chip","Stack","TextField","Typography","Link","useCallback","useEffect","useState","useFirestore","useEmailOrgMount","useOrgEmailTopics","writeEmailTopic","EmailTopicDetail","props","hostId","topicId","basePath","firestore","enqueueSnackbar","confirm","orgMount","topics","scope","orgId","topic","known","some","it","id","name","setName","description","setDescription","doubleOptIn","setDoubleOptIn","loaded","setLoaded","busy","setBusy","length","storedDoubleOptIn","dirty","save","patch","nextName","trim","variant","persist","archived","error","console","toggleArchived","accepted","title","confirmationText","then","catch","card","header","subheader","direction","spacing","sx","mt","alignItems","size","color","label","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","component","href","value","onChange","event","target","disabled","helperText","multiline","minRows","select","slotProps","native","option","onClick"],"mappings":"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"}
|
|
@@ -64,6 +64,9 @@ import { describeDynamicListRule } from "./dynamic-list-rule-fields.js";
|
|
|
64
64
|
import { OrgSiteSelect, useEmailDataScope, useEmailOrgMount } from "./email-org-mount.js";
|
|
65
65
|
import { listMembersResourceKey } from "../transfer/email-transfer-catalog.js";
|
|
66
66
|
import ListMembersPanel from "./list-members-panel.js";
|
|
67
|
+
/** One Audience card — its loading and missing branch carries the same link. */ const AUDIENCE_DOCS_HELP = pluginDocsHelp('emailCampaigns', {
|
|
68
|
+
anchor: '#list-members'
|
|
69
|
+
});
|
|
67
70
|
export function ListDetailCard(props) {
|
|
68
71
|
var _ref, _ref1, _list_name;
|
|
69
72
|
var _list_lastEvaluatedAt;
|
|
@@ -237,9 +240,7 @@ export function ListDetailCard(props) {
|
|
|
237
240
|
*/ const settled = Boolean(scope) && status !== 'loading';
|
|
238
241
|
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
239
242
|
header: 'Audience',
|
|
240
|
-
help:
|
|
241
|
-
anchor: '#email-lists'
|
|
242
|
-
}),
|
|
243
|
+
help: AUDIENCE_DOCS_HELP,
|
|
243
244
|
contentGutterX: true,
|
|
244
245
|
contentGutterY: true,
|
|
245
246
|
HeaderProps: {
|
|
@@ -266,9 +267,7 @@ export function ListDetailCard(props) {
|
|
|
266
267
|
/* The card, named so the page chrome above it is a plain list of
|
|
267
268
|
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsx(CardDisplay, {
|
|
268
269
|
header: 'Audience',
|
|
269
|
-
help:
|
|
270
|
-
anchor: '#list-members'
|
|
271
|
-
}),
|
|
270
|
+
help: AUDIENCE_DOCS_HELP,
|
|
272
271
|
contentGutterX: true,
|
|
273
272
|
contentGutterY: true,
|
|
274
273
|
contentBordered: "all",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-detail-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\n/**\n * ONE AUDIENCE: who it is, how it is built, and every person on it.\n *\n * ## Why a route rather than an expanded row\n *\n * The membership used to unfold inside the list table, which cost three\n * things a merchant actually uses. A list was not linkable, so \"check the\n * Wholesale audience\" could not be sent to anybody. The back button walked out\n * of the whole surface rather than out of the list. And the list's own\n * settings had to live under a table they were not about.\n *\n * ## What it reads\n *\n * The list document — one document, whatever the audience's size — plus one\n * server aggregate for the subscriber total, plus the paged membership window\n * the table below draws. The membership is PII and there is one document per\n * subscriber, so it is read HERE, where somebody asked for it, and nowhere\n * that merely lists audiences.\n *\n * ## The rule is stated, not just flagged\n *\n * A dynamic list is a claim about who is on it, and \"Rule\" as a chip is not\n * that claim. The summary spells out every dimension the rule actually\n * carries, so the answer to \"why is this person not in the audience\" is on the\n * screen the question gets asked on.\n *\n * ## On the organization's page, the site it is worked AS\n *\n * The list is the organization's, but adding somebody to it is done as one\n * site — the enrollment and the consent it records are that site's — and the\n * Consent column reads what each person agreed to with one site's group. So\n * with no site in the URL the page asks which, defaulting to the site the\n * list's filters draw from, then the reader's pick this session, then the\n * org's only site. Nothing below the picker is built until there is an\n * answer: a consent column for no site would be a column about nobody.\n */\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport {\n consentGroupForHost,\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n type ConsentGroup,\n} from '@aglyn/aglyn'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport {\n mdiPencilOutline,\n mdiTrayArrowDown,\n mdiTrayArrowUp,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { collection, doc, getCountFromServer } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { describeDynamicListRule } from './dynamic-list-rule-fields'\nimport {\n OrgSiteSelect,\n useEmailDataScope,\n useEmailOrgMount,\n} from './email-org-mount'\nimport { listMembersResourceKey } from '../transfer/email-transfer-catalog'\nimport ListMembersPanel, { type ListMembersFilter } from './list-members-panel'\n\nexport interface ListDetailCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /**\n * The controller this audience is being read AS, resolved by the page from\n * the org document it already holds — under a site.\n *\n * Passed through rather than resolved here: the group is a fact about the\n * ORG, one lookup serves every section, and a card that resolved its own\n * would be a second place for the answer to come from.\n */\n consentGroup?: ConsentGroup\n /**\n * The org document, on the organization's page, where the group can only be\n * resolved once the reader has said which site they are working as.\n */\n org?: Record<string, unknown>\n listId: string\n /** The emails hub URL, for the way back to the audiences list. */\n basePath: string\n}\n\nexport function ListDetailCard(props: ListDetailCardProps) {\n const { hostId, listId, basePath, org } = props\n const firestore = useFirestore()\n const orgMount = useEmailOrgMount()\n const { scope } = useEmailDataScope(hostId)\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () =>\n scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null,\n [firestore, scope, listId],\n )\n\n /*\n * The subscriber total, as a server aggregate.\n *\n * Not the length of the page below: a count beside a paged table is exactly\n * where a page length gets mistaken for a total. Taken once, and re-taken\n * only when a membership is added or removed — a figure that disagreed with\n * the table directly under it would read as the add having failed.\n */\n const [count, setCount] = useState<number | null>(null)\n const [membershipVersion, setMembershipVersion] = useState(0)\n useEffect(() => {\n if (!scope) return undefined\n let active = true\n void getCountFromServer(\n collection(firestore, scope[0], scope[1], 'lists', listId, 'members'),\n )\n .then((snapshot) => {\n if (active) setCount(snapshot.data().count)\n })\n .catch(() => {\n if (active) setCount(null)\n })\n return () => {\n active = false\n }\n }, [firestore, scope, listId, membershipVersion])\n\n const onMembershipChanged = useCallback(\n () => setMembershipVersion((version) => version + 1),\n [],\n )\n\n /*\n * THE SITE THIS AUDIENCE IS WORKED AS. Under a site, that site. On the\n * organization's page, the reader's choice — else the site the filters draw\n * from, else the session's pick, else the only site.\n */\n const [chosenHostId, setChosenHostId] = useState('')\n const listHostId = String(list?.['hostId'] ?? '')\n const enrollHostId: string =\n hostId ??\n (chosenHostId ||\n (orgMount?.hosts.some((site) => site.id === listHostId)\n ? listHostId\n : '') ||\n orgMount?.pickedHostId ||\n '')\n const consentGroup = useMemo<ConsentGroup | null>(\n () =>\n hostId != null\n ? (props.consentGroup ?? null)\n : enrollHostId\n ? consentGroupForHost(org, enrollHostId)\n : null,\n [hostId, props.consentGroup, enrollHostId, org],\n )\n\n /*\n * IMPORT AND EXPORT open the console's transfer wizard and export dialog\n * (AGL-3529) on this list — `email.list-members:<listId>`, one list at a\n * time — worked as the site chosen above. The wizard carries everything the import's\n * controls are: the preview, the screening, the statement of permission,\n * the staged and resumable write, the undo. Outside the console shell\n * there is no launcher, and the buttons are not drawn.\n */\n const transfer = useTransferLauncher()\n const [memberFilter, setMemberFilter] = useState<ListMembersFilter | null>(\n null,\n )\n const listName = String(list?.['name'] ?? '')\n // Each button only for whom the route takes it (AGL-3554), on the site the\n // list enrolls as — the workspace's verdict until one is chosen.\n const transferTarget = {\n resource: listMembersResourceKey(listId),\n scope: 'host' as const,\n hostId: enrollHostId || null,\n }\n const canExportMembers = Boolean(transfer?.can('export', transferTarget))\n const canImportMembers = Boolean(transfer?.can('import', transferTarget))\n\n const audiencesHref = `${basePath}/audiences`\n const headerActions = (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={audiencesHref}\n size=\"small\"\n color=\"primary\"\n >\n {'All audiences'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${audiencesHref}/${listId}/edit`}\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n >\n {'Edit list'}\n </Button>\n {transfer && canExportMembers ? (\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowDown.path} size={0.8} />}\n // Consent is read as a site; on the org page one has to be chosen.\n disabled={!enrollHostId}\n onClick={() =>\n transfer.openExport({\n resource: listMembersResourceKey(listId),\n scope: 'host',\n hostId: enrollHostId,\n ...(listName ? { title: `Export ${listName}` } : {}),\n ...(memberFilter ? { filter: memberFilter } : {}),\n })\n }\n >\n {'Export'}\n </Button>\n ) : null}\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n {transfer && enrollHostId ? (\n <TransferResumeImport\n target={transferTarget}\n {...(listName ? { title: `Import into ${listName}` } : {})}\n onFinished={onMembershipChanged}\n />\n ) : null}\n {transfer && canImportMembers ? (\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiTrayArrowUp.path} size={0.8} />}\n // An import enrolls as a site; on the org page one has to be chosen.\n disabled={!enrollHostId}\n onClick={() =>\n transfer.openImport({\n resource: listMembersResourceKey(listId),\n scope: 'host',\n hostId: enrollHostId,\n ...(listName ? { title: `Import into ${listName}` } : {}),\n onFinished: onMembershipChanged,\n })\n }\n >\n {'Import'}\n </Button>\n ) : null}\n </Stack>\n )\n\n if (!list) {\n /*\n * Loading and MISSING are different answers, and the difference matters\n * more here than almost anywhere. Not \"no members\" — a list that cannot be\n * read is a different situation from an empty one, and rendering an empty\n * membership for the first is how somebody comes to believe an audience\n * they built has lost everybody. Reading `status` rather than the absence\n * of data is also what stops the refusal being flashed on every arrival,\n * before the org scope has even resolved.\n */\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Audience'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n const dynamic = list['kind'] === 'dynamic'\n /*\n * The filters, whichever kind of list this is.\n *\n * On a live list they are what the sweep evaluates. On a fixed one they are\n * a saved SEARCH — the same filters, used to find people to add rather than\n * to decide membership — so they are read and described either way, and only\n * the sentence about what happens next differs.\n */\n const rule = normalizeDynamicListRule(list['rule'])\n const hasFilters = rule.sources.length > 0\n const summary = hasFilters ? describeDynamicListRule(rule) : []\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Audience'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#list-members' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n {dynamic ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n label={\n list['lastEvaluatedAt']?.toDate\n ? `Rule · ${list['lastEvaluatedAt']\n .toDate()\n .toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Fixed membership\" />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {count === null\n ? 'Counting subscribers…'\n : count === 1\n ? '1 subscriber'\n : `${count.toLocaleString()} subscribers`}\n </Typography>\n </Stack>\n\n {/*\n A live list's filters are stated HERE rather than only on the edit\n page: they are the answer to \"why is this person not in the\n audience\", which is a question asked while looking at the membership.\n A fixed list states them inside the find-people control instead,\n beside the button that acts on them.\n */}\n {dynamic && hasFilters ? (\n <Stack spacing={0.5}>\n {summary.map((clause) => (\n <Typography key={clause} variant=\"body2\" color=\"text.secondary\">\n {clause}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n {orgMount ? (\n <OrgSiteSelect\n mount={orgMount}\n label=\"Enroll as\"\n value={enrollHostId}\n onChange={setChosenHostId}\n helperText={\n 'People you add are enrolled as this site, and the Consent ' +\n 'column reads what they agreed to with it.'\n }\n />\n ) : null}\n\n {orgMount && !enrollHostId ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Choose the site to work this audience as. Whether someone may ' +\n 'be emailed depends on what they agreed to with a site, so the ' +\n 'members and their consent are read for one site at a time.'}\n </Typography>\n ) : scope && consentGroup ? (\n <ListMembersPanel\n key={enrollHostId}\n hostId={enrollHostId}\n consentGroup={consentGroup}\n scope={scope as readonly [string, string]}\n listId={listId}\n listName={String(list['name'] ?? '')}\n onMembershipChanged={onMembershipChanged}\n /*\n Offered only on a FIXED list. A live list's membership is the\n sweep's to decide, and hand-adding its own matches would write\n `via: 'manual'` copies of rows the sweep already owns — rows it\n then may not reconcile away when the person stops matching.\n */\n findRule={dynamic || !hasFilters ? null : rule}\n ruleSummary={summary}\n onFilterChange={setMemberFilter}\n />\n ) : null}\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the audience; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={String(list['name'] || listId)} />\n {card}\n </>\n )\n}\nListDetailCard.displayName = 'ListDetailCard'\n\nexport default ListDetailCard\n"],"names":["TransferResumeImport","consentGroupForHost","normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","useTransferLauncher","mdiPencilOutline","mdiTrayArrowDown","mdiTrayArrowUp","AppLink","CardDisplay","MdiIcon","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","useCallback","useEffect","useMemo","useState","useFirestore","useFirestoreDoc","describeDynamicListRule","OrgSiteSelect","useEmailDataScope","useEmailOrgMount","listMembersResourceKey","ListMembersPanel","ListDetailCard","props","list","hostId","listId","basePath","org","firestore","orgMount","scope","data","status","count","setCount","membershipVersion","setMembershipVersion","undefined","active","then","snapshot","catch","onMembershipChanged","version","chosenHostId","setChosenHostId","listHostId","String","enrollHostId","hosts","some","site","id","pickedHostId","consentGroup","transfer","memberFilter","setMemberFilter","listName","transferTarget","resource","canExportMembers","Boolean","can","canImportMembers","audiencesHref","headerActions","direction","spacing","useFlexGap","sx","flexWrap","component","componentVariant","nativeButton","href","size","color","variant","startIcon","path","disabled","onClick","openExport","title","filter","target","onFinished","openImport","settled","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dynamic","rule","hasFilters","sources","length","summary","card","contentBordered","alignItems","label","toDate","toLocaleString","map","clause","mount","value","onChange","helperText","findRule","ruleSummary","onFilterChange","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-detail-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\n/**\n * ONE AUDIENCE: who it is, how it is built, and every person on it.\n *\n * ## Why a route rather than an expanded row\n *\n * The membership used to unfold inside the list table, which cost three\n * things a merchant actually uses. A list was not linkable, so \"check the\n * Wholesale audience\" could not be sent to anybody. The back button walked out\n * of the whole surface rather than out of the list. And the list's own\n * settings had to live under a table they were not about.\n *\n * ## What it reads\n *\n * The list document — one document, whatever the audience's size — plus one\n * server aggregate for the subscriber total, plus the paged membership window\n * the table below draws. The membership is PII and there is one document per\n * subscriber, so it is read HERE, where somebody asked for it, and nowhere\n * that merely lists audiences.\n *\n * ## The rule is stated, not just flagged\n *\n * A dynamic list is a claim about who is on it, and \"Rule\" as a chip is not\n * that claim. The summary spells out every dimension the rule actually\n * carries, so the answer to \"why is this person not in the audience\" is on the\n * screen the question gets asked on.\n *\n * ## On the organization's page, the site it is worked AS\n *\n * The list is the organization's, but adding somebody to it is done as one\n * site — the enrollment and the consent it records are that site's — and the\n * Consent column reads what each person agreed to with one site's group. So\n * with no site in the URL the page asks which, defaulting to the site the\n * list's filters draw from, then the reader's pick this session, then the\n * org's only site. Nothing below the picker is built until there is an\n * answer: a consent column for no site would be a column about nobody.\n */\n\nimport { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport {\n consentGroupForHost,\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n type ConsentGroup,\n} from '@aglyn/aglyn'\nimport { useTransferLauncher } from '@aglyn/aglyn/app-utils/transfer-launcher-context'\nimport {\n mdiPencilOutline,\n mdiTrayArrowDown,\n mdiTrayArrowUp,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { collection, doc, getCountFromServer } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { describeDynamicListRule } from './dynamic-list-rule-fields'\nimport {\n OrgSiteSelect,\n useEmailDataScope,\n useEmailOrgMount,\n} from './email-org-mount'\nimport { listMembersResourceKey } from '../transfer/email-transfer-catalog'\nimport ListMembersPanel, { type ListMembersFilter } from './list-members-panel'\n\n/** One Audience card — its loading and missing branch carries the same link. */\nconst AUDIENCE_DOCS_HELP = pluginDocsHelp('emailCampaigns', { anchor: '#list-members' })\n\nexport interface ListDetailCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /**\n * The controller this audience is being read AS, resolved by the page from\n * the org document it already holds — under a site.\n *\n * Passed through rather than resolved here: the group is a fact about the\n * ORG, one lookup serves every section, and a card that resolved its own\n * would be a second place for the answer to come from.\n */\n consentGroup?: ConsentGroup\n /**\n * The org document, on the organization's page, where the group can only be\n * resolved once the reader has said which site they are working as.\n */\n org?: Record<string, unknown>\n listId: string\n /** The emails hub URL, for the way back to the audiences list. */\n basePath: string\n}\n\nexport function ListDetailCard(props: ListDetailCardProps) {\n const { hostId, listId, basePath, org } = props\n const firestore = useFirestore()\n const orgMount = useEmailOrgMount()\n const { scope } = useEmailDataScope(hostId)\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () =>\n scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null,\n [firestore, scope, listId],\n )\n\n /*\n * The subscriber total, as a server aggregate.\n *\n * Not the length of the page below: a count beside a paged table is exactly\n * where a page length gets mistaken for a total. Taken once, and re-taken\n * only when a membership is added or removed — a figure that disagreed with\n * the table directly under it would read as the add having failed.\n */\n const [count, setCount] = useState<number | null>(null)\n const [membershipVersion, setMembershipVersion] = useState(0)\n useEffect(() => {\n if (!scope) return undefined\n let active = true\n void getCountFromServer(\n collection(firestore, scope[0], scope[1], 'lists', listId, 'members'),\n )\n .then((snapshot) => {\n if (active) setCount(snapshot.data().count)\n })\n .catch(() => {\n if (active) setCount(null)\n })\n return () => {\n active = false\n }\n }, [firestore, scope, listId, membershipVersion])\n\n const onMembershipChanged = useCallback(\n () => setMembershipVersion((version) => version + 1),\n [],\n )\n\n /*\n * THE SITE THIS AUDIENCE IS WORKED AS. Under a site, that site. On the\n * organization's page, the reader's choice — else the site the filters draw\n * from, else the session's pick, else the only site.\n */\n const [chosenHostId, setChosenHostId] = useState('')\n const listHostId = String(list?.['hostId'] ?? '')\n const enrollHostId: string =\n hostId ??\n (chosenHostId ||\n (orgMount?.hosts.some((site) => site.id === listHostId)\n ? listHostId\n : '') ||\n orgMount?.pickedHostId ||\n '')\n const consentGroup = useMemo<ConsentGroup | null>(\n () =>\n hostId != null\n ? (props.consentGroup ?? null)\n : enrollHostId\n ? consentGroupForHost(org, enrollHostId)\n : null,\n [hostId, props.consentGroup, enrollHostId, org],\n )\n\n /*\n * IMPORT AND EXPORT open the console's transfer wizard and export dialog\n * (AGL-3529) on this list — `email.list-members:<listId>`, one list at a\n * time — worked as the site chosen above. The wizard carries everything the import's\n * controls are: the preview, the screening, the statement of permission,\n * the staged and resumable write, the undo. Outside the console shell\n * there is no launcher, and the buttons are not drawn.\n */\n const transfer = useTransferLauncher()\n const [memberFilter, setMemberFilter] = useState<ListMembersFilter | null>(\n null,\n )\n const listName = String(list?.['name'] ?? '')\n // Each button only for whom the route takes it (AGL-3554), on the site the\n // list enrolls as — the workspace's verdict until one is chosen.\n const transferTarget = {\n resource: listMembersResourceKey(listId),\n scope: 'host' as const,\n hostId: enrollHostId || null,\n }\n const canExportMembers = Boolean(transfer?.can('export', transferTarget))\n const canImportMembers = Boolean(transfer?.can('import', transferTarget))\n\n const audiencesHref = `${basePath}/audiences`\n const headerActions = (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={audiencesHref}\n size=\"small\"\n color=\"primary\"\n >\n {'All audiences'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={`${audiencesHref}/${listId}/edit`}\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n >\n {'Edit list'}\n </Button>\n {transfer && canExportMembers ? (\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiTrayArrowDown.path} size={0.8} />}\n // Consent is read as a site; on the org page one has to be chosen.\n disabled={!enrollHostId}\n onClick={() =>\n transfer.openExport({\n resource: listMembersResourceKey(listId),\n scope: 'host',\n hostId: enrollHostId,\n ...(listName ? { title: `Export ${listName}` } : {}),\n ...(memberFilter ? { filter: memberFilter } : {}),\n })\n }\n >\n {'Export'}\n </Button>\n ) : null}\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n {transfer && enrollHostId ? (\n <TransferResumeImport\n target={transferTarget}\n {...(listName ? { title: `Import into ${listName}` } : {})}\n onFinished={onMembershipChanged}\n />\n ) : null}\n {transfer && canImportMembers ? (\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiTrayArrowUp.path} size={0.8} />}\n // An import enrolls as a site; on the org page one has to be chosen.\n disabled={!enrollHostId}\n onClick={() =>\n transfer.openImport({\n resource: listMembersResourceKey(listId),\n scope: 'host',\n hostId: enrollHostId,\n ...(listName ? { title: `Import into ${listName}` } : {}),\n onFinished: onMembershipChanged,\n })\n }\n >\n {'Import'}\n </Button>\n ) : null}\n </Stack>\n )\n\n if (!list) {\n /*\n * Loading and MISSING are different answers, and the difference matters\n * more here than almost anywhere. Not \"no members\" — a list that cannot be\n * read is a different situation from an empty one, and rendering an empty\n * membership for the first is how somebody comes to believe an audience\n * they built has lost everybody. Reading `status` rather than the absence\n * of data is also what stops the refusal being flashed on every arrival,\n * before the org scope has even resolved.\n */\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Audience'}\n help={AUDIENCE_DOCS_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n const dynamic = list['kind'] === 'dynamic'\n /*\n * The filters, whichever kind of list this is.\n *\n * On a live list they are what the sweep evaluates. On a fixed one they are\n * a saved SEARCH — the same filters, used to find people to add rather than\n * to decide membership — so they are read and described either way, and only\n * the sentence about what happens next differs.\n */\n const rule = normalizeDynamicListRule(list['rule'])\n const hasFilters = rule.sources.length > 0\n const summary = hasFilters ? describeDynamicListRule(rule) : []\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Audience'}\n help={AUDIENCE_DOCS_HELP}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n {dynamic ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n label={\n list['lastEvaluatedAt']?.toDate\n ? `Rule · ${list['lastEvaluatedAt']\n .toDate()\n .toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Fixed membership\" />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {count === null\n ? 'Counting subscribers…'\n : count === 1\n ? '1 subscriber'\n : `${count.toLocaleString()} subscribers`}\n </Typography>\n </Stack>\n\n {/*\n A live list's filters are stated HERE rather than only on the edit\n page: they are the answer to \"why is this person not in the\n audience\", which is a question asked while looking at the membership.\n A fixed list states them inside the find-people control instead,\n beside the button that acts on them.\n */}\n {dynamic && hasFilters ? (\n <Stack spacing={0.5}>\n {summary.map((clause) => (\n <Typography key={clause} variant=\"body2\" color=\"text.secondary\">\n {clause}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n {orgMount ? (\n <OrgSiteSelect\n mount={orgMount}\n label=\"Enroll as\"\n value={enrollHostId}\n onChange={setChosenHostId}\n helperText={\n 'People you add are enrolled as this site, and the Consent ' +\n 'column reads what they agreed to with it.'\n }\n />\n ) : null}\n\n {orgMount && !enrollHostId ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Choose the site to work this audience as. Whether someone may ' +\n 'be emailed depends on what they agreed to with a site, so the ' +\n 'members and their consent are read for one site at a time.'}\n </Typography>\n ) : scope && consentGroup ? (\n <ListMembersPanel\n key={enrollHostId}\n hostId={enrollHostId}\n consentGroup={consentGroup}\n scope={scope as readonly [string, string]}\n listId={listId}\n listName={String(list['name'] ?? '')}\n onMembershipChanged={onMembershipChanged}\n /*\n Offered only on a FIXED list. A live list's membership is the\n sweep's to decide, and hand-adding its own matches would write\n `via: 'manual'` copies of rows the sweep already owns — rows it\n then may not reconcile away when the person stops matching.\n */\n findRule={dynamic || !hasFilters ? null : rule}\n ruleSummary={summary}\n onFilterChange={setMemberFilter}\n />\n ) : null}\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the audience; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={String(list['name'] || listId)} />\n {card}\n </>\n )\n}\nListDetailCard.displayName = 'ListDetailCard'\n\nexport default ListDetailCard\n"],"names":["TransferResumeImport","consentGroupForHost","normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","useTransferLauncher","mdiPencilOutline","mdiTrayArrowDown","mdiTrayArrowUp","AppLink","CardDisplay","MdiIcon","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","useCallback","useEffect","useMemo","useState","useFirestore","useFirestoreDoc","describeDynamicListRule","OrgSiteSelect","useEmailDataScope","useEmailOrgMount","listMembersResourceKey","ListMembersPanel","AUDIENCE_DOCS_HELP","anchor","ListDetailCard","props","list","hostId","listId","basePath","org","firestore","orgMount","scope","data","status","count","setCount","membershipVersion","setMembershipVersion","undefined","active","then","snapshot","catch","onMembershipChanged","version","chosenHostId","setChosenHostId","listHostId","String","enrollHostId","hosts","some","site","id","pickedHostId","consentGroup","transfer","memberFilter","setMemberFilter","listName","transferTarget","resource","canExportMembers","Boolean","can","canImportMembers","audiencesHref","headerActions","direction","spacing","useFlexGap","sx","flexWrap","component","componentVariant","nativeButton","href","size","color","variant","startIcon","path","disabled","onClick","openExport","title","filter","target","onFinished","openImport","settled","header","help","contentGutterX","contentGutterY","HeaderProps","action","dynamic","rule","hasFilters","sources","length","summary","card","contentBordered","alignItems","label","toDate","toLocaleString","map","clause","mount","value","onChange","helperText","findRule","ruleSummary","onFilterChange","displayName"],"mappings":"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"}
|
|
@@ -179,7 +179,7 @@ export function ListEditCard(props) {
|
|
|
179
179
|
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
180
180
|
header: 'Edit list',
|
|
181
181
|
help: pluginDocsHelp('emailCampaigns', {
|
|
182
|
-
anchor: '#
|
|
182
|
+
anchor: '#lists-built-from-a-rule'
|
|
183
183
|
}),
|
|
184
184
|
contentGutterX: true,
|
|
185
185
|
contentGutterY: true,
|