@aglyn/plugins-email 1.0.0-beta.228 → 1.0.0-beta.230
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.d.ts +2 -0
- package/src/lib/components/email-screens-card.js +45 -10
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/email-zones.d.ts +10 -0
- package/src/lib/components/email-zones.js +1 -0
- package/src/lib/components/email-zones.js.map +1 -1
- package/src/lib/components/emails-console-page.js +1 -0
- package/src/lib/components/emails-console-page.js.map +1 -1
- package/src/lib/plugin.js +11 -1
- package/src/lib/plugin.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.230",
|
|
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.230",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.230",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.230",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.230",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.230",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.230",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.230",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.230",
|
|
36
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.230",
|
|
37
|
+
"@aglyn/shared-util-email": "1.0.0-beta.230",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.230",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.230",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.230",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.230",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.230",
|
|
43
43
|
"@swc/helpers": "0.5.23"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function EmailScreensCard(props: {
|
|
34
34
|
hostId: string;
|
|
35
|
+
/** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */
|
|
36
|
+
orgId?: string;
|
|
35
37
|
/** The emails hub URL, which every template route hangs beneath. */
|
|
36
38
|
basePath: string;
|
|
37
39
|
}): import("react").JSX.Element;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
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
|
+
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
19
20
|
import { mdiDeleteOutline, mdiContentCopy, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
|
|
20
21
|
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
21
22
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
@@ -35,6 +36,7 @@ import { useMemo } from "react";
|
|
|
35
36
|
import { EMAIL_TEMPLATE_BASE, EMAIL_TEMPLATE_FILTER_HEADERS, EMAIL_TEMPLATE_QUERY } from "../constants/list-queries.js";
|
|
36
37
|
import { templateProvenance } from "../model/template-provenance.js";
|
|
37
38
|
import { createEmailScreen } from "../utils/create-email-screen.js";
|
|
39
|
+
import { HOST_EMAIL_TEMPLATES_ZONE } from "./email-zones.js";
|
|
38
40
|
import { emailTemplateSoftDelete } from "../utils/email-template-soft-delete.js";
|
|
39
41
|
// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/
|
|
40
42
|
// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the
|
|
@@ -79,7 +81,17 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
79
81
|
* delete (`emailTemplateSoftDelete`), so the order leaves it out rather than
|
|
80
82
|
* the card dropping it after the read.
|
|
81
83
|
*/ export function EmailScreensCard(props) {
|
|
82
|
-
const { hostId, basePath } = props;
|
|
84
|
+
const { hostId, orgId, basePath } = props;
|
|
85
|
+
/**
|
|
86
|
+
* The shell's zone renderer (AGL-3596), for other ways to start a design —
|
|
87
|
+
* the `hostEmailTemplates` zone this plugin declares; `null` outside the
|
|
88
|
+
* console shell, where there is no workspace to gate on.
|
|
89
|
+
*/ const CreateZone = useConsoleWidgetSlot();
|
|
90
|
+
const createZone = CreateZone ? /*#__PURE__*/ _jsx(CreateZone, {
|
|
91
|
+
slot: HOST_EMAIL_TEMPLATES_ZONE.id,
|
|
92
|
+
hostId: hostId,
|
|
93
|
+
orgId: orgId
|
|
94
|
+
}) : null;
|
|
83
95
|
const { orgSlug, subdomain } = useConsoleHostRoute(hostId);
|
|
84
96
|
const firestore = useFirestore();
|
|
85
97
|
const createHostResource = useHostResourceApi();
|
|
@@ -248,21 +260,44 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
248
260
|
contentGutterX: true,
|
|
249
261
|
contentGutterY: true,
|
|
250
262
|
HeaderProps: {
|
|
251
|
-
action: /*#__PURE__*/
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
action: /*#__PURE__*/ _jsxs(Stack, {
|
|
264
|
+
direction: "row",
|
|
265
|
+
spacing: 1,
|
|
266
|
+
children: [
|
|
267
|
+
createZone,
|
|
268
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
269
|
+
size: "small",
|
|
270
|
+
variant: "contained",
|
|
271
|
+
onClick: ()=>void handleCreate(),
|
|
272
|
+
children: 'New template'
|
|
273
|
+
})
|
|
274
|
+
]
|
|
256
275
|
})
|
|
257
276
|
},
|
|
258
277
|
children: [
|
|
259
278
|
duplicate.dialog,
|
|
260
279
|
/*#__PURE__*/ _jsx(Stack, {
|
|
261
280
|
spacing: 1.5,
|
|
262
|
-
children: emailScreens.length === 0 && !hasMore && !filtering ? /*#__PURE__*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
281
|
+
children: emailScreens.length === 0 && !hasMore && !filtering ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
282
|
+
children: [
|
|
283
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
284
|
+
variant: "body2",
|
|
285
|
+
color: "text.secondary",
|
|
286
|
+
children: 'Design a reusable email here, then send it from a campaign. A ' + 'new template opens in the besigner with email-safe components ' + 'only.'
|
|
287
|
+
}),
|
|
288
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
289
|
+
direction: "row",
|
|
290
|
+
spacing: 1,
|
|
291
|
+
children: [
|
|
292
|
+
createZone,
|
|
293
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
294
|
+
variant: "contained",
|
|
295
|
+
onClick: ()=>void handleCreate(),
|
|
296
|
+
children: 'Create your first template'
|
|
297
|
+
})
|
|
298
|
+
]
|
|
299
|
+
})
|
|
300
|
+
]
|
|
266
301
|
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
267
302
|
children: [
|
|
268
303
|
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","emailTemplateSoftDelete","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","base","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","value","event","stopPropagation","width","origin","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { HOST_EMAIL_TEMPLATES_ZONE } from './email-zones'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */\n orgId?: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, orgId, basePath } = props\n /**\n * The shell's zone renderer (AGL-3596), for other ways to start a design —\n * the `hostEmailTemplates` zone this plugin declares; `null` outside the\n * console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n const createZone = CreateZone ? (\n <CreateZone slot={HOST_EMAIL_TEMPLATES_ZONE.id} hostId={hostId} orgId={orgId} />\n ) : null\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n </Stack>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n {/* Other ways to start a design, the header's zone again. */}\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button variant=\"contained\" onClick={() => void handleCreate()}>\n {'Create your first template'}\n </Button>\n </Stack>\n </>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","useConsoleWidgetSlot","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","HOST_EMAIL_TEMPLATES_ZONE","emailTemplateSoftDelete","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","orgId","basePath","CreateZone","createZone","slot","id","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","base","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","value","event","stopPropagation","width","origin","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","dialog","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { ConsolePluginOrgMount } from '@aglyn/aglyn';
|
|
18
|
+
import type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
|
|
18
19
|
/**
|
|
19
20
|
* The Messages section of the Emails page.
|
|
20
21
|
*
|
|
@@ -68,3 +69,12 @@ export interface EmailTemplateRecipientsZoneProps {
|
|
|
68
69
|
screenId: string;
|
|
69
70
|
}
|
|
70
71
|
export declare const EMAIL_TEMPLATE_RECIPIENTS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<EmailTemplateRecipientsZoneProps>;
|
|
72
|
+
/**
|
|
73
|
+
* A site's email templates list, beside New template and in the list's empty
|
|
74
|
+
* state (AGL-3596): another way to start an email design. A widget here is
|
|
75
|
+
* handed the site and its org — the `hostScreens` contract a site's pages,
|
|
76
|
+
* templates, forms and components hand too — and opens its own way to a new
|
|
77
|
+
* design; it writes nothing through the page.
|
|
78
|
+
*/
|
|
79
|
+
export type HostEmailTemplatesZoneProps = ConsoleHostScreensZoneProps;
|
|
80
|
+
export declare const HOST_EMAIL_TEMPLATES_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<ConsoleHostScreensZoneProps>;
|
|
@@ -17,5 +17,6 @@
|
|
|
17
17
|
export const EMAIL_MESSAGES_ZONE = definePluginZone('emailMessages');
|
|
18
18
|
export const EMAIL_TEMPLATE_REPORT_ZONE = definePluginZone('emailTemplateReport');
|
|
19
19
|
export const EMAIL_TEMPLATE_RECIPIENTS_ZONE = definePluginZone('emailTemplateRecipients');
|
|
20
|
+
export const HOST_EMAIL_TEMPLATES_ZONE = definePluginZone('hostEmailTemplates');
|
|
20
21
|
|
|
21
22
|
//# sourceMappingURL=email-zones.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-zones.ts"],"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 */\nimport type { ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * The Messages section of the Emails page.\n *\n * A message is one email that was or will be sent. Writing one, scheduling\n * it, sending it and reporting what came of it are a campaign's work — the\n * routes that do all of it are served by whichever plugin owns campaigns — so\n * this plugin hosts the section and that plugin draws it. What is handed over\n * is what the shell handed this page: the site, this page's own base path,\n * and the segments under `messages` (`[]` the list, `[id]` one message,\n * `[id, 'edit']` its composer).\n *\n * The same zone serves the ORGANIZATION's Emails page, `/[orgSlug]/emails`,\n * where there is no site: `hostId` is `null` and `orgMount` names the org and\n * its sites, so the widget can list every site's messages and ask which site\n * a new one is sent as. Every message link a widget draws hangs beneath\n * `basePath` at both levels — `${basePath}/messages/{id}`.\n */\nexport interface EmailMessagesZoneProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The Emails page's own path, under the site or the organization. */\n basePath: string\n detail: readonly string[]\n /** The organization and its sites — present only when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const EMAIL_MESSAGES_ZONE =\n definePluginZone<EmailMessagesZoneProps>('emailMessages')\n\n/**\n * What the messages sent from one template did, on the template's page.\n *\n * The template is this plugin's; the messages sent from it are the sends of\n * the plugin that owns campaigns, so the figures, the audiences and the table\n * of those messages are that plugin's to read and draw. Handed the site, the\n * template, and this page's base path, which every message link hangs beneath.\n */\nexport interface EmailTemplateReportZoneProps {\n hostId: string\n screenId: string\n /** The Emails page's own path, under the site. */\n basePath: string\n}\n\nexport const EMAIL_TEMPLATE_REPORT_ZONE =\n definePluginZone<EmailTemplateReportZoneProps>('emailTemplateReport')\n\n/**\n * Who received the messages built from one template, under its report.\n *\n * The template is this plugin's; the recipients of a send are read through\n * the campaign owner's route, so the table is that plugin's to draw.\n */\nexport interface EmailTemplateRecipientsZoneProps {\n hostId: string\n screenId: string\n}\n\nexport const EMAIL_TEMPLATE_RECIPIENTS_ZONE =\n definePluginZone<EmailTemplateRecipientsZoneProps>('emailTemplateRecipients')\n"],"names":["definePluginZone","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-zones.ts"],"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 */\nimport type { ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * The Messages section of the Emails page.\n *\n * A message is one email that was or will be sent. Writing one, scheduling\n * it, sending it and reporting what came of it are a campaign's work — the\n * routes that do all of it are served by whichever plugin owns campaigns — so\n * this plugin hosts the section and that plugin draws it. What is handed over\n * is what the shell handed this page: the site, this page's own base path,\n * and the segments under `messages` (`[]` the list, `[id]` one message,\n * `[id, 'edit']` its composer).\n *\n * The same zone serves the ORGANIZATION's Emails page, `/[orgSlug]/emails`,\n * where there is no site: `hostId` is `null` and `orgMount` names the org and\n * its sites, so the widget can list every site's messages and ask which site\n * a new one is sent as. Every message link a widget draws hangs beneath\n * `basePath` at both levels — `${basePath}/messages/{id}`.\n */\nexport interface EmailMessagesZoneProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The Emails page's own path, under the site or the organization. */\n basePath: string\n detail: readonly string[]\n /** The organization and its sites — present only when `hostId` is `null`. */\n orgMount?: ConsolePluginOrgMount\n}\n\nexport const EMAIL_MESSAGES_ZONE =\n definePluginZone<EmailMessagesZoneProps>('emailMessages')\n\n/**\n * What the messages sent from one template did, on the template's page.\n *\n * The template is this plugin's; the messages sent from it are the sends of\n * the plugin that owns campaigns, so the figures, the audiences and the table\n * of those messages are that plugin's to read and draw. Handed the site, the\n * template, and this page's base path, which every message link hangs beneath.\n */\nexport interface EmailTemplateReportZoneProps {\n hostId: string\n screenId: string\n /** The Emails page's own path, under the site. */\n basePath: string\n}\n\nexport const EMAIL_TEMPLATE_REPORT_ZONE =\n definePluginZone<EmailTemplateReportZoneProps>('emailTemplateReport')\n\n/**\n * Who received the messages built from one template, under its report.\n *\n * The template is this plugin's; the recipients of a send are read through\n * the campaign owner's route, so the table is that plugin's to draw.\n */\nexport interface EmailTemplateRecipientsZoneProps {\n hostId: string\n screenId: string\n}\n\nexport const EMAIL_TEMPLATE_RECIPIENTS_ZONE =\n definePluginZone<EmailTemplateRecipientsZoneProps>('emailTemplateRecipients')\n\n/**\n * A site's email templates list, beside New template and in the list's empty\n * state (AGL-3596): another way to start an email design. A widget here is\n * handed the site and its org — the `hostScreens` contract a site's pages,\n * templates, forms and components hand too — and opens its own way to a new\n * design; it writes nothing through the page.\n */\nexport type HostEmailTemplatesZoneProps = ConsoleHostScreensZoneProps\n\nexport const HOST_EMAIL_TEMPLATES_ZONE =\n definePluginZone<HostEmailTemplatesZoneProps>('hostEmailTemplates')\n"],"names":["definePluginZone","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE","HOST_EMAIL_TEMPLATES_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,gBAAgB,QAAQ,2CAA0C;AA6B3E,OAAO,MAAMC,sBACXD,iBAAyC,iBAAgB;AAiB3D,OAAO,MAAME,6BACXF,iBAA+C,uBAAsB;AAavE,OAAO,MAAMG,iCACXH,iBAAmD,2BAA0B;AAW/E,OAAO,MAAMI,4BACXJ,iBAA8C,sBAAqB"}
|
|
@@ -139,6 +139,7 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
|
|
|
139
139
|
basePath: basePath
|
|
140
140
|
}) : /*#__PURE__*/ _jsx(EmailScreensCard, {
|
|
141
141
|
hostId: hostId,
|
|
142
|
+
orgId: typeof (org == null ? void 0 : org['$id']) === 'string' ? org['$id'] : undefined,
|
|
142
143
|
basePath: basePath
|
|
143
144
|
});
|
|
144
145
|
case 'audiences':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-page.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 consentGroupForHost,\n type ConsentGroup,\n type ConsolePluginOrgMount,\n type ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport { useConsentGroupAccess } from './consent-group-access'\nimport ConsentGroupConfirmationCard from './consent-group-confirmation-card'\nimport ConsentGroupsCard from './consent-groups-card'\nimport EmailScreensCard from './email-screens-card'\nimport EmailTemplateDetail from './email-template-detail'\nimport EmailTopicDetail from './email-topic-detail'\nimport EmailTopicsCard from './email-topics-card'\nimport ListDetailCard from './list-detail-card'\nimport ListEditCard from './list-edit-card'\nimport ListsCard from './lists-card'\nimport OrgEmailTemplatesCard from './org-email-templates-card'\nimport OrgSendingCard from './org-sending-card'\nimport OrgSendingDomainDetail from './org-sending-domain-detail'\nimport OrgSiteSuppressions from './org-site-suppressions'\nimport OrgSuppressionsCard from './org-suppressions-card'\nimport SendingDomainDetail from './sending-domain-detail'\nimport SendingDomainsCard from './sending-domains-card'\nimport SiteConsentGroupCard from './site-consent-group-card'\nimport SuppressionsCard from './suppressions-card'\nimport type { EmailsConsoleSectionId } from './emails-console-sections'\nimport { EmailOrgMountProvider, useEmailOrgMount } from './email-org-mount'\nimport { EMAIL_MESSAGES_ZONE, type EmailMessagesZoneProps } from './email-zones'\n\n/**\n * The Messages section: a zone, drawn by whichever plugin owns campaigns.\n *\n * Its own component because a zone's renderer comes from a hook and\n * `sectionBody` is a plain function — and so that, like every other branch\n * there, it is constructed only while Messages is the section being read.\n */\nfunction EmailMessagesSection(props: EmailMessagesZoneProps) {\n const Zone = useConsoleWidgetSlot()\n return Zone ? <Zone slot={EMAIL_MESSAGES_ZONE.id} {...props} /> : null\n}\n\n/**\n * The organization's Consent groups section: the editor, and under it the\n * switch over how a group's sites treat each other's confirmations.\n *\n * Both ask whether the reader may change consent groups, and the two routes\n * behind them ask the same two permissions, so the section reads the\n * reader's membership once and hands the answer to both.\n */\nfunction OrgConsentGroupsSection(props: { org?: Record<string, unknown> }) {\n const orgId = useEmailOrgMount()?.orgId\n const access = useConsentGroupAccess(orgId || undefined)\n return (\n <Stack spacing={3}>\n <ConsentGroupsCard org={props.org} access={access} />\n <ConsentGroupConfirmationCard org={props.org} access={access} />\n </Stack>\n )\n}\n\n/**\n * The body of one emails section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: EmailsConsoleSectionId,\n hostId: string,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n /**\n * The controller the surface is being viewed as, resolved once by the page.\n *\n * Only the audience detail needs it today — it is what its membership table\n * reads consent FOR — but it is resolved at the page because the answer is a\n * property of the org and this is the one place holding the org document.\n */\n consentGroup: ConsentGroup,\n /**\n * The org document, from which the Consent groups section reads the org's\n * slug and any change in flight.\n */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n /*\n * `/emails/messages/{emailId}` is ONE MESSAGE — the thing that was or\n * will be sent, as against the campaign that groups messages and the\n * template they are built from. A ROUTE rather than an expanded row: it\n * is linkable, which is what a merchant wants to paste into a message\n * about last week's send, and its preview, link table and recipient\n * list are reads the list above it must not pay for.\n *\n * The page links OUT to the campaign this message belongs to, which is\n * a section of the Marketing console.\n */\n /*\n * `…/{emailId}/edit` WRITES the email and `…/{emailId}` reports on\n * it — two jobs with two shapes, and creating stays a drawer on the\n * list. Which of the three is drawn is decided from `detail` by the\n * widget, which builds only the one it is asked for: the composer's\n * listens are not paid for by somebody reading a report.\n */\n return (\n <EmailMessagesSection\n hostId={hostId}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * `/emails/templates/{screenId}` is one TEMPLATE's page.\n *\n * A route for the same reasons a message's page is one: it is\n * linkable, and the listing above it is a cheaper surface a reader who\n * came for one template should not have to mount. The preview, the\n * aggregate figures and the recipients table all hang off this branch,\n * so none of them is constructed while the list is what is being read.\n */\n return detail[0] ? (\n <EmailTemplateDetail\n hostId={hostId}\n screenId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailScreensCard hostId={hostId} basePath={basePath} />\n )\n case 'audiences':\n /*\n * A list is a resource with its own pages, on the same terms a message\n * is: `/emails/audiences/{listId}` is one audience, and `…/edit` is its\n * settings.\n *\n * The membership used to unfold inside the audiences table. That made a\n * list unlinkable, put the back button one press from leaving the whole\n * surface, and asked the reader of a list of lists to hold the table\n * that lists them AND the table of one list's subscribers on the same\n * screen. The subscribers are also the expensive read here — one PII\n * document per person — so putting them behind a route is what stops\n * them being paid for by somebody who came to see which audiences exist.\n *\n * Ternaries rather than a lookup: only the branch taken is CONSTRUCTED,\n * which is the cost this whole function is shaped around.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard\n hostId={hostId}\n listId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <ListDetailCard\n hostId={hostId}\n consentGroup={consentGroup}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={hostId} basePath={basePath} />\n )\n case 'topics':\n // Create is a drawer on the list; EDIT is the topic's own route, which\n // is the section owning its own subtree exactly as `messages` does.\n return detail[0] ? (\n <EmailTopicDetail\n hostId={hostId}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={hostId} basePath={basePath} />\n )\n case 'sending':\n /*\n * `/emails/sending/{domain}` is one domain's page, and it is a route\n * for the reason every other detail here is: the DNS records, the DMARC\n * read and the verification button are what somebody came for, and a\n * link to them is what they paste to whoever actually edits the zone.\n *\n * The domain is the document id, so it is also the segment. It is\n * decoded because a URL carries it encoded and the record is keyed on\n * the bare name.\n */\n return detail[0] ? (\n <SendingDomainDetail\n hostId={hostId}\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <SendingDomainsCard hostId={hostId} basePath={basePath} />\n )\n case 'consent-groups':\n // Which sender this site is. Declared for the organization, so here it\n // is said and linked, never changed.\n return (\n <SiteConsentGroupCard\n hostId={hostId}\n consentGroup={consentGroup}\n org={org}\n />\n )\n case 'suppressions':\n return <SuppressionsCard hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section, at `/[orgSlug]/emails`.\n *\n * The same seven sections as a site's, each answering over the organization.\n * Three of them are already the org's — an audience and a topic are shared by\n * every site, and a consent group is declared for several — so their cards\n * render with no site. The other four are site facts: a message is sent as\n * one site, a template is one site's screen, a sending identity and a\n * suppression list are one site's. Those sections read each site in turn, a\n * page of sites at a time, and link a row into the site that holds it.\n *\n * Built only when that section is the one being read, for the reason\n * {@link sectionBody} is: every card opens its reads on mount.\n */\nfunction orgSectionBody(\n section: EmailsConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n orgMount: ConsolePluginOrgMount,\n /** The org document, from which an audience's consent group is resolved. */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n // Every site's messages, drawn by the plugin that sends; it is handed\n // the org mount so it can say which site each was sent as.\n return (\n <EmailMessagesSection\n hostId={null}\n orgMount={orgMount}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * One table over every site's templates. A template is a screen on one\n * site, so its own page stays that site's: each row links there, and\n * nothing beneath this section is an org route.\n */\n return <OrgEmailTemplatesCard />\n case 'audiences':\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard hostId={null} listId={detail[0]} basePath={basePath} />\n ) : (\n <ListDetailCard\n hostId={null}\n org={org}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={null} basePath={basePath} />\n )\n case 'topics':\n return detail[0] ? (\n <EmailTopicDetail\n hostId={null}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={null} basePath={basePath} />\n )\n case 'sending':\n /*\n * A domain is proved by the organization, so its page is an org route\n * here as it is under a site. What each SITE sends as is the other half,\n * and the section's own page lists it per site.\n */\n return detail[0] ? (\n <OrgSendingDomainDetail\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <OrgSendingCard basePath={basePath} />\n )\n case 'consent-groups':\n /*\n * The declaration itself, and under it the organization's one switch\n * over how a declared group treats a confirmation one of its sites is\n * waiting on — the switch belongs with the groups it governs rather\n * than with the topics that each set their own confirmation.\n */\n return <OrgConsentGroupsSection org={org} />\n case 'suppressions':\n /*\n * `…/suppressions` totals every site's list; `…/suppressions/{hostId}`\n * is one site's list itself, the same card a site's own page draws.\n */\n return detail[0] ? (\n <OrgSiteSuppressions hostId={detail[0]} />\n ) : (\n <OrgSuppressionsCard />\n )\n default:\n return null\n }\n}\n\n/**\n * Emails page (AGL-395): the console surface owned by the email plugin,\n * rendered by the shell's generic plugin route.\n *\n * Two of its sections are two different things a merchant calls \"an email\":\n * an EMAIL is one message that was or will be sent, and a TEMPLATE is the\n * reusable besigner document a message is built from. Keeping them apart is\n * what lets each carry its own report — a message's own numbers, and a\n * template's summed across every message sent from it. Audience lists, the\n * topic catalog, the sending identities and the suppression list complete the\n * surface.\n *\n * The CAMPAIGN that groups messages is not here. It is a container with a\n * window of dates, a set of lists, a topic and revenue attribution — a\n * marketing object that reaches people by email — so it is a section of the\n * Marketing console, and a message's page links out to the campaign it\n * belongs to.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `emails-console-read-cost.spec.tsx` was written\n * BEFORE the conversion precisely to hold that line, and reports the same\n * counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag\n * somebody has to remember on the next surface.\n *\n * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.\n * Handed no site, it publishes the org mount to its cards and renders\n * {@link orgSectionBody} instead: the same seven sections, over every site.\n */\nexport function EmailsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } =\n props\n\n /*==========================================\n * THE CONTROLLER THIS SURFACE IS BEING VIEWED AS.\n *\n * The declared group of sites that are one sender, or this site alone. Every\n * consent question asked below is asked FOR a controller and not for a site:\n * a grant is looked up under the asking site, a refusal is honored across\n * the whole group, and a grant held by a site outside it belongs to somebody\n * else. It is the same group `performCampaignSend` resolves, so the audience\n * table and the send agree about the same document by construction.\n *\n * Pure, from the org document the shell already passed, so it costs no read.\n * An absent org resolves to the group of one, which is the narrow answer.\n *\n * Only under a site. The organization's page has no site to resolve a group\n * FOR, and `consentGroupForHost` refuses to invent one — the audience page\n * there resolves it for the site the reader enrolls as, once one is chosen.\n *=========================================*/\n const consentGroup = useMemo(\n () =>\n hostId == null\n ? null\n : consentGroupForHost(org as Record<string, unknown>, hostId),\n [org, hostId],\n )\n\n /*\n * Nothing, deliberately, while the redirect is in flight. Rendering the\n * default section here would issue its listens on a URL about to be\n * replaced — on every arrival at `/emails`, which is every nav-tab click.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!orgMount) return null\n return (\n <EmailOrgMountProvider mount={orgMount} basePath={basePath}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as EmailsConsoleSectionId,\n detail,\n basePath,\n orgMount,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n </EmailOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as EmailsConsoleSectionId,\n hostId,\n detail,\n basePath,\n consentGroup as ConsentGroup,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n )\n}\nEmailsConsolePage.displayName = 'EmailsConsolePage'\n\nexport default EmailsConsolePage\n"],"names":["consentGroupForHost","useConsoleWidgetSlot","HubSections","Stack","useMemo","useConsentGroupAccess","ConsentGroupConfirmationCard","ConsentGroupsCard","EmailScreensCard","EmailTemplateDetail","EmailTopicDetail","EmailTopicsCard","ListDetailCard","ListEditCard","ListsCard","OrgEmailTemplatesCard","OrgSendingCard","OrgSendingDomainDetail","OrgSiteSuppressions","OrgSuppressionsCard","SendingDomainDetail","SendingDomainsCard","SiteConsentGroupCard","SuppressionsCard","EmailOrgMountProvider","useEmailOrgMount","EMAIL_MESSAGES_ZONE","EmailMessagesSection","props","Zone","slot","id","OrgConsentGroupsSection","orgId","access","undefined","spacing","org","sectionBody","section","hostId","detail","basePath","consentGroup","screenId","listId","topicId","domain","decodeURIComponent","orgSectionBody","orgMount","EmailsConsolePage","sections","segments","length","slice","mount","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/emails-console-page.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 consentGroupForHost,\n type ConsentGroup,\n type ConsolePluginOrgMount,\n type ConsolePluginPageProps,\n} from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport { useConsentGroupAccess } from './consent-group-access'\nimport ConsentGroupConfirmationCard from './consent-group-confirmation-card'\nimport ConsentGroupsCard from './consent-groups-card'\nimport EmailScreensCard from './email-screens-card'\nimport EmailTemplateDetail from './email-template-detail'\nimport EmailTopicDetail from './email-topic-detail'\nimport EmailTopicsCard from './email-topics-card'\nimport ListDetailCard from './list-detail-card'\nimport ListEditCard from './list-edit-card'\nimport ListsCard from './lists-card'\nimport OrgEmailTemplatesCard from './org-email-templates-card'\nimport OrgSendingCard from './org-sending-card'\nimport OrgSendingDomainDetail from './org-sending-domain-detail'\nimport OrgSiteSuppressions from './org-site-suppressions'\nimport OrgSuppressionsCard from './org-suppressions-card'\nimport SendingDomainDetail from './sending-domain-detail'\nimport SendingDomainsCard from './sending-domains-card'\nimport SiteConsentGroupCard from './site-consent-group-card'\nimport SuppressionsCard from './suppressions-card'\nimport type { EmailsConsoleSectionId } from './emails-console-sections'\nimport { EmailOrgMountProvider, useEmailOrgMount } from './email-org-mount'\nimport { EMAIL_MESSAGES_ZONE, type EmailMessagesZoneProps } from './email-zones'\n\n/**\n * The Messages section: a zone, drawn by whichever plugin owns campaigns.\n *\n * Its own component because a zone's renderer comes from a hook and\n * `sectionBody` is a plain function — and so that, like every other branch\n * there, it is constructed only while Messages is the section being read.\n */\nfunction EmailMessagesSection(props: EmailMessagesZoneProps) {\n const Zone = useConsoleWidgetSlot()\n return Zone ? <Zone slot={EMAIL_MESSAGES_ZONE.id} {...props} /> : null\n}\n\n/**\n * The organization's Consent groups section: the editor, and under it the\n * switch over how a group's sites treat each other's confirmations.\n *\n * Both ask whether the reader may change consent groups, and the two routes\n * behind them ask the same two permissions, so the section reads the\n * reader's membership once and hands the answer to both.\n */\nfunction OrgConsentGroupsSection(props: { org?: Record<string, unknown> }) {\n const orgId = useEmailOrgMount()?.orgId\n const access = useConsentGroupAccess(orgId || undefined)\n return (\n <Stack spacing={3}>\n <ConsentGroupsCard org={props.org} access={access} />\n <ConsentGroupConfirmationCard org={props.org} access={access} />\n </Stack>\n )\n}\n\n/**\n * The body of one emails section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and each card opens its\n * Firestore listens on mount — which is the entire cost this page exists to\n * stop paying. Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: EmailsConsoleSectionId,\n hostId: string,\n /**\n * The section's OWN segments — `segments[1]` onward, already sliced by the\n * caller. A section that owns deeper routes reads them here; one that does\n * not simply ignores them.\n */\n detail: readonly string[],\n basePath: string,\n /**\n * The controller the surface is being viewed as, resolved once by the page.\n *\n * Only the audience detail needs it today — it is what its membership table\n * reads consent FOR — but it is resolved at the page because the answer is a\n * property of the org and this is the one place holding the org document.\n */\n consentGroup: ConsentGroup,\n /**\n * The org document, from which the Consent groups section reads the org's\n * slug and any change in flight.\n */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n /*\n * `/emails/messages/{emailId}` is ONE MESSAGE — the thing that was or\n * will be sent, as against the campaign that groups messages and the\n * template they are built from. A ROUTE rather than an expanded row: it\n * is linkable, which is what a merchant wants to paste into a message\n * about last week's send, and its preview, link table and recipient\n * list are reads the list above it must not pay for.\n *\n * The page links OUT to the campaign this message belongs to, which is\n * a section of the Marketing console.\n */\n /*\n * `…/{emailId}/edit` WRITES the email and `…/{emailId}` reports on\n * it — two jobs with two shapes, and creating stays a drawer on the\n * list. Which of the three is drawn is decided from `detail` by the\n * widget, which builds only the one it is asked for: the composer's\n * listens are not paid for by somebody reading a report.\n */\n return (\n <EmailMessagesSection\n hostId={hostId}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * `/emails/templates/{screenId}` is one TEMPLATE's page.\n *\n * A route for the same reasons a message's page is one: it is\n * linkable, and the listing above it is a cheaper surface a reader who\n * came for one template should not have to mount. The preview, the\n * aggregate figures and the recipients table all hang off this branch,\n * so none of them is constructed while the list is what is being read.\n */\n return detail[0] ? (\n <EmailTemplateDetail\n hostId={hostId}\n screenId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailScreensCard\n hostId={hostId}\n orgId={typeof org?.['$id'] === 'string' ? org['$id'] : undefined}\n basePath={basePath}\n />\n )\n case 'audiences':\n /*\n * A list is a resource with its own pages, on the same terms a message\n * is: `/emails/audiences/{listId}` is one audience, and `…/edit` is its\n * settings.\n *\n * The membership used to unfold inside the audiences table. That made a\n * list unlinkable, put the back button one press from leaving the whole\n * surface, and asked the reader of a list of lists to hold the table\n * that lists them AND the table of one list's subscribers on the same\n * screen. The subscribers are also the expensive read here — one PII\n * document per person — so putting them behind a route is what stops\n * them being paid for by somebody who came to see which audiences exist.\n *\n * Ternaries rather than a lookup: only the branch taken is CONSTRUCTED,\n * which is the cost this whole function is shaped around.\n */\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard\n hostId={hostId}\n listId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <ListDetailCard\n hostId={hostId}\n consentGroup={consentGroup}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={hostId} basePath={basePath} />\n )\n case 'topics':\n // Create is a drawer on the list; EDIT is the topic's own route, which\n // is the section owning its own subtree exactly as `messages` does.\n return detail[0] ? (\n <EmailTopicDetail\n hostId={hostId}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={hostId} basePath={basePath} />\n )\n case 'sending':\n /*\n * `/emails/sending/{domain}` is one domain's page, and it is a route\n * for the reason every other detail here is: the DNS records, the DMARC\n * read and the verification button are what somebody came for, and a\n * link to them is what they paste to whoever actually edits the zone.\n *\n * The domain is the document id, so it is also the segment. It is\n * decoded because a URL carries it encoded and the record is keyed on\n * the bare name.\n */\n return detail[0] ? (\n <SendingDomainDetail\n hostId={hostId}\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <SendingDomainsCard hostId={hostId} basePath={basePath} />\n )\n case 'consent-groups':\n // Which sender this site is. Declared for the organization, so here it\n // is said and linked, never changed.\n return (\n <SiteConsentGroupCard\n hostId={hostId}\n consentGroup={consentGroup}\n org={org}\n />\n )\n case 'suppressions':\n return <SuppressionsCard hostId={hostId} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section, at `/[orgSlug]/emails`.\n *\n * The same seven sections as a site's, each answering over the organization.\n * Three of them are already the org's — an audience and a topic are shared by\n * every site, and a consent group is declared for several — so their cards\n * render with no site. The other four are site facts: a message is sent as\n * one site, a template is one site's screen, a sending identity and a\n * suppression list are one site's. Those sections read each site in turn, a\n * page of sites at a time, and link a row into the site that holds it.\n *\n * Built only when that section is the one being read, for the reason\n * {@link sectionBody} is: every card opens its reads on mount.\n */\nfunction orgSectionBody(\n section: EmailsConsoleSectionId,\n detail: readonly string[],\n basePath: string,\n orgMount: ConsolePluginOrgMount,\n /** The org document, from which an audience's consent group is resolved. */\n org: Record<string, unknown> | undefined,\n): ReactNode {\n switch (section) {\n case 'messages':\n // Every site's messages, drawn by the plugin that sends; it is handed\n // the org mount so it can say which site each was sent as.\n return (\n <EmailMessagesSection\n hostId={null}\n orgMount={orgMount}\n basePath={basePath}\n detail={detail}\n />\n )\n case 'templates':\n /*\n * One table over every site's templates. A template is a screen on one\n * site, so its own page stays that site's: each row links there, and\n * nothing beneath this section is an org route.\n */\n return <OrgEmailTemplatesCard />\n case 'audiences':\n return detail[0] ? (\n detail[1] === 'edit' ? (\n <ListEditCard hostId={null} listId={detail[0]} basePath={basePath} />\n ) : (\n <ListDetailCard\n hostId={null}\n org={org}\n listId={detail[0]}\n basePath={basePath}\n />\n )\n ) : (\n <ListsCard hostId={null} basePath={basePath} />\n )\n case 'topics':\n return detail[0] ? (\n <EmailTopicDetail\n hostId={null}\n topicId={detail[0]}\n basePath={basePath}\n />\n ) : (\n <EmailTopicsCard hostId={null} basePath={basePath} />\n )\n case 'sending':\n /*\n * A domain is proved by the organization, so its page is an org route\n * here as it is under a site. What each SITE sends as is the other half,\n * and the section's own page lists it per site.\n */\n return detail[0] ? (\n <OrgSendingDomainDetail\n domain={decodeURIComponent(detail[0])}\n basePath={basePath}\n />\n ) : (\n <OrgSendingCard basePath={basePath} />\n )\n case 'consent-groups':\n /*\n * The declaration itself, and under it the organization's one switch\n * over how a declared group treats a confirmation one of its sites is\n * waiting on — the switch belongs with the groups it governs rather\n * than with the topics that each set their own confirmation.\n */\n return <OrgConsentGroupsSection org={org} />\n case 'suppressions':\n /*\n * `…/suppressions` totals every site's list; `…/suppressions/{hostId}`\n * is one site's list itself, the same card a site's own page draws.\n */\n return detail[0] ? (\n <OrgSiteSuppressions hostId={detail[0]} />\n ) : (\n <OrgSuppressionsCard />\n )\n default:\n return null\n }\n}\n\n/**\n * Emails page (AGL-395): the console surface owned by the email plugin,\n * rendered by the shell's generic plugin route.\n *\n * Two of its sections are two different things a merchant calls \"an email\":\n * an EMAIL is one message that was or will be sent, and a TEMPLATE is the\n * reusable besigner document a message is built from. Keeping them apart is\n * what lets each carry its own report — a message's own numbers, and a\n * template's summed across every message sent from it. Audience lists, the\n * topic catalog, the sending identities and the suppression list complete the\n * surface.\n *\n * The CAMPAIGN that groups messages is not here. It is a container with a\n * window of dates, a set of lists, a topic and revenue attribution — a\n * marketing object that reaches people by email — so it is a section of the\n * Marketing console, and a message's page links out to the campaign it\n * belongs to.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `emails-console-read-cost.spec.tsx` was written\n * BEFORE the conversion precisely to hold that line, and reports the same\n * counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag\n * somebody has to remember on the next surface.\n *\n * The same page is the ORGANIZATION's Emails page at `/[orgSlug]/emails`.\n * Handed no site, it publishes the org mount to its cards and renders\n * {@link orgSectionBody} instead: the same seven sections, over every site.\n */\nexport function EmailsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, section, sections, basePath, segments } =\n props\n\n /*==========================================\n * THE CONTROLLER THIS SURFACE IS BEING VIEWED AS.\n *\n * The declared group of sites that are one sender, or this site alone. Every\n * consent question asked below is asked FOR a controller and not for a site:\n * a grant is looked up under the asking site, a refusal is honored across\n * the whole group, and a grant held by a site outside it belongs to somebody\n * else. It is the same group `performCampaignSend` resolves, so the audience\n * table and the send agree about the same document by construction.\n *\n * Pure, from the org document the shell already passed, so it costs no read.\n * An absent org resolves to the group of one, which is the narrow answer.\n *\n * Only under a site. The organization's page has no site to resolve a group\n * FOR, and `consentGroupForHost` refuses to invent one — the audience page\n * there resolves it for the site the reader enrolls as, once one is chosen.\n *=========================================*/\n const consentGroup = useMemo(\n () =>\n hostId == null\n ? null\n : consentGroupForHost(org as Record<string, unknown>, hostId),\n [org, hostId],\n )\n\n /*\n * Nothing, deliberately, while the redirect is in flight. Rendering the\n * default section here would issue its listens on a URL about to be\n * replaced — on every arrival at `/emails`, which is every nav-tab click.\n */\n if (!section || !sections?.length || !basePath) return null\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n const detail = (segments ?? []).slice(1)\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!orgMount) return null\n return (\n <EmailOrgMountProvider mount={orgMount} basePath={basePath}>\n <HubSections sections={sections}>\n {orgSectionBody(\n section as EmailsConsoleSectionId,\n detail,\n basePath,\n orgMount,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n </EmailOrgMountProvider>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as EmailsConsoleSectionId,\n hostId,\n detail,\n basePath,\n consentGroup as ConsentGroup,\n org as Record<string, unknown> | undefined,\n )}\n </HubSections>\n )\n}\nEmailsConsolePage.displayName = 'EmailsConsolePage'\n\nexport default EmailsConsolePage\n"],"names":["consentGroupForHost","useConsoleWidgetSlot","HubSections","Stack","useMemo","useConsentGroupAccess","ConsentGroupConfirmationCard","ConsentGroupsCard","EmailScreensCard","EmailTemplateDetail","EmailTopicDetail","EmailTopicsCard","ListDetailCard","ListEditCard","ListsCard","OrgEmailTemplatesCard","OrgSendingCard","OrgSendingDomainDetail","OrgSiteSuppressions","OrgSuppressionsCard","SendingDomainDetail","SendingDomainsCard","SiteConsentGroupCard","SuppressionsCard","EmailOrgMountProvider","useEmailOrgMount","EMAIL_MESSAGES_ZONE","EmailMessagesSection","props","Zone","slot","id","OrgConsentGroupsSection","orgId","access","undefined","spacing","org","sectionBody","section","hostId","detail","basePath","consentGroup","screenId","listId","topicId","domain","decodeURIComponent","orgSectionBody","orgMount","EmailsConsolePage","sections","segments","length","slice","mount","displayName"],"mappings":"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"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -18,7 +18,7 @@ import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/pl
|
|
|
18
18
|
import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
19
19
|
import { mdiAccountMultipleOutline, mdiEmailOffOutline, mdiEmailOutline } from "@aglyn/shared-data-mdi";
|
|
20
20
|
import { lazy } from "react";
|
|
21
|
-
import { EMAIL_MESSAGES_ZONE, EMAIL_TEMPLATE_RECIPIENTS_ZONE, EMAIL_TEMPLATE_REPORT_ZONE } from "./components/email-zones.js";
|
|
21
|
+
import { EMAIL_MESSAGES_ZONE, EMAIL_TEMPLATE_RECIPIENTS_ZONE, EMAIL_TEMPLATE_REPORT_ZONE, HOST_EMAIL_TEMPLATES_ZONE } from "./components/email-zones.js";
|
|
22
22
|
import { EMAILS_CONSOLE_SECTIONS } from "./components/emails-console-sections.js";
|
|
23
23
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
24
24
|
import { SITE_EMAIL_PACKAGE_KIND } from "./constants/site-package.js";
|
|
@@ -129,6 +129,16 @@ const ListConsentStep = lazy(()=>import("./transfer/list-import-steps.js").then(
|
|
|
129
129
|
}, {
|
|
130
130
|
pluginId: BUNDLE_ID
|
|
131
131
|
});
|
|
132
|
+
registerPluginZone({
|
|
133
|
+
zone: HOST_EMAIL_TEMPLATES_ZONE,
|
|
134
|
+
label: 'A site’s email templates',
|
|
135
|
+
surface: 'console',
|
|
136
|
+
// One item in the list's row of actions, beside New template.
|
|
137
|
+
layout: 'bare',
|
|
138
|
+
description: 'On a site’s Emails page, beside New template and in the empty list: another way to start an email design. A widget here is handed the site and its org and writes nothing through the page.'
|
|
139
|
+
}, {
|
|
140
|
+
pluginId: BUNDLE_ID
|
|
141
|
+
});
|
|
132
142
|
Aglyn.registerConsoleExtension({
|
|
133
143
|
pluginId: BUNDLE_ID,
|
|
134
144
|
displayName: 'Email',
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/plugin.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n mdiAccountMultipleOutline,\n mdiEmailOffOutline,\n mdiEmailOutline,\n} from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n EMAIL_MESSAGES_ZONE,\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './components/email-zones'\nimport { EMAILS_CONSOLE_SECTIONS } from './components/emails-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { SITE_EMAIL_PACKAGE_KIND } from './constants/site-package'\nimport { registerEmailRecordRoutes } from './model/email-record-routes'\nimport {\n LIST_CONSENT_STEP_ID,\n LIST_EXISTING_STEP_ID,\n LIST_MEMBERS_RESOURCE,\n SUPPRESSIONS_RESOURCE,\n} from './transfer/email-transfer-catalog'\n\n/** Code-split: the Emails console page only loads when opened. */\nconst EmailsConsolePage = lazy(() => import('./components/emails-console-page'))\n\n/*\n * What this plugin draws inside a campaign's own pages, each loaded only where\n * the campaign owner's zone is on screen.\n */\nconst CampaignTopicSelect = lazy(\n () => import('./components/campaign-topic-select'),\n)\nconst CampaignTopicOptionsWidget = lazy(\n () => import('./components/campaign-topic-options-widget'),\n)\nconst CampaignSenderEditorWidget = lazy(\n () => import('./components/campaign-sender-editor-widget'),\n)\nconst CampaignDesignCreateWidget = lazy(\n () => import('./components/campaign-design-create-widget'),\n)\nconst EmailDesignPreview = lazy(\n () => import('./components/email-design-preview'),\n)\nconst SitePackageEmailPreview = lazy(\n () => import('./components/site-package-email-preview'),\n)\n\n/* The list import's own wizard steps, loaded when the wizard reaches one. */\nconst ListExistingStep = lazy(() =>\n import('./transfer/list-import-steps').then((steps) => ({\n default: steps.ListExistingStep,\n })),\n)\nconst ListConsentStep = lazy(() =>\n import('./transfer/list-import-steps').then((steps) => ({\n default: steps.ListConsentStep,\n })),\n)\n\n/**\n * The client half of what this plugin imports and exports (AGL-3529,\n * AGL-3550): how the wizard and the hub name each resource, and the list import's two steps of its\n * own — whether people the workspace already holds may change, and the\n * statement of permission under what the dry run found. The server half\n * registers from the console declarations.\n */\nexport function registerEmailTransferResourceUis(): void {\n registerPluginTransferResourceUi(\n LIST_MEMBERS_RESOURCE,\n {\n label: 'list members',\n icon: { path: mdiAccountMultipleOutline.path },\n extraSteps: [\n {\n id: LIST_EXISTING_STEP_ID,\n label: 'People already on this list',\n after: 'matching',\n component: ListExistingStep,\n },\n {\n id: LIST_CONSENT_STEP_ID,\n label: 'Permission',\n after: 'conflicts',\n component: ListConsentStep,\n },\n ],\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginTransferResourceUi(\n SUPPRESSIONS_RESOURCE,\n { label: 'suppressions', icon: { path: mdiEmailOffOutline.path } },\n { pluginId: BUNDLE_ID },\n )\n // How the Import & export hub names topics in a workspace package (AGL-3550).\n registerPluginTransferResourceUi(\n 'email.topics',\n { label: 'Email topics' },\n { pluginId: BUNDLE_ID },\n )\n}\n\n/**\n * Console half (AGL-395): registers the Emails nav item + page in the\n * ConsoleExtension registry. Safe to call at console app load — the page is\n * lazy, so no besigner/canvas code loads. The shell renders the Emails nav\n * item and, via its generic plugin route, the page (the messages and their\n * composer, the templates, the audience lists, the topic catalog, the sending\n * identities and the suppression list) — with no edit to the console's own\n * nav or page files.\n */\nexport function registerEmailConsole(): void {\n // Where a message and a sending identity are read (AGL-3080), for the\n // surfaces that link to them without knowing this page's address.\n registerEmailRecordRoutes()\n registerEmailTransferResourceUis()\n /*\n * The two places this plugin's pages hand over to whichever plugin owns\n * campaigns. A message is one send of a campaign and every action on it is\n * that plugin's route, so the Messages section is a zone this page hosts\n * rather than pages this plugin imports; the same holds for the recipients\n * table under a template's report. Named, because a spec calls this\n * registrar without the loader.\n */\n registerPluginZone(\n {\n zone: EMAIL_MESSAGES_ZONE,\n label: 'The Emails page’s Messages section',\n surface: 'console',\n layout: 'bare',\n description:\n 'The whole body of `/emails/messages` and the routes under it, under a site and on the organization’s Emails page. A widget here is handed the site (or `null` and the org mount at the organization level), the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_REPORT_ZONE,\n label: 'What a template’s emails did',\n surface: 'console',\n description:\n 'On one template’s page, under its name. A widget here reports the messages sent from that template — their figures, the audiences they went to and the messages themselves — read from the plugin that sends them; it is handed the site, the template’s id and the Emails page’s base path.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n label: 'Who received a template’s emails',\n surface: 'console',\n description:\n 'On one template’s page, under its report. A widget here lists the recipients of every send built from that template; it is handed the site and the template’s id.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Email',\n /*\n * WHO may open the email console, declared so the shell enforces it.\n *\n * The audiences section reads `orgs/{orgId}/lists/{listId}/members`, and\n * those members are enrolled CONTACTS — an address, a name, and the\n * consent basis recording why the person may be mailed. That is the same\n * org-shared people data the CRM holds, reached from a different page.\n *\n * The rules gate those reads on `isOrgWideMember()` ALONE, with no role\n * condition, so org-wide membership of any role is enough to list every\n * audience the organization has and everybody on it. An org VIEWER — the\n * role that exists to read and change nothing — therefore reads the whole\n * marketing audience today. `data.manage` is what closes that: it\n * defaults to owner, admin and editor, so the population it admits is\n * exactly the one `server-list-gate.ts` accepts a list write from, and\n * the viewer it excludes is the reader the rules never excluded.\n *\n * `data.manage` rather than a key of this plugin's own for the reason the\n * catalog gives for refusing a `marketing.manage`: campaigns are written\n * client-direct against rules that gate on the HOST role, so a new\n * org-level key would name an action with no org-level boundary under it.\n * `data.manage` is not in that position — it already governs the\n * org-shared data this surface exposes, and the list gate already reads\n * the roles it defaults to.\n *\n * A SITE COLLABORATOR holding it opens the page, and the answer is NOT\n * the one Contacts reached. There the listener is scoped and the rules\n * prove the same predicate per document; here the audiences read demands\n * `isOrgWideMember()`, which a collaborator is not, so the org-shared\n * half is already refused beneath the console and the half that remains —\n * this site's own messages, templates and sending identities — is theirs.\n * Refusing the surface outright would take that away to close nothing.\n */\n permission: 'data.manage',\n /*\n * The mail a campaign rides on is this plugin's — the topic catalog, the\n * sending identities, the design document and its renderer — so each is\n * drawn here, in a zone the campaign owner's composer and message page\n * host. Every one reports through a callback; none writes a campaign.\n */\n widgets: [\n {\n slot: 'campaignTopicSelect',\n widgetId: 'email-campaign-topic-select',\n title: 'Topic',\n Component: CampaignTopicSelect,\n },\n {\n slot: 'campaignTopicOptions',\n widgetId: 'email-campaign-topic-options',\n title: 'Topics',\n Component: CampaignTopicOptionsWidget,\n },\n {\n slot: 'campaignSenderEditor',\n widgetId: 'email-campaign-sender-editor',\n title: 'Add a sender',\n Component: CampaignSenderEditorWidget,\n },\n {\n slot: 'campaignDesignCreate',\n widgetId: 'email-campaign-design-create',\n title: 'Design this email',\n Component: CampaignDesignCreateWidget,\n },\n {\n slot: 'campaignDesignPreview',\n widgetId: 'email-campaign-design-preview',\n title: 'Preview',\n Component: EmailDesignPreview,\n },\n {\n // A site email in a site package import, each side drawn by the\n // preview that renders through the send path's code (AGL-3545).\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,\n widgetId: 'email-site-package-preview',\n title: 'Email preview',\n itemKinds: [SITE_EMAIL_PACKAGE_KIND],\n Component: SitePackageEmailPreview,\n },\n ],\n navItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n // Sections as ROUTES (AGL-2501): `/emails/messages` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Emails page, at `/[orgSlug]/emails`: the same six\n * sections over every site. The audiences and the topics are the org's\n * already; the messages, the templates, the sending identities and the\n * suppression lists are read site by site and link into the site that\n * holds them. The page is the same component — handed no site, it renders\n * the org sections.\n *\n * No `navTabId`, the same as the site tab: this plugin's release flag\n * gates the plugin itself, so one flag already holds both levels.\n */\n orgNavItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginTransferResourceUi","registerPluginZone","mdiAccountMultipleOutline","mdiEmailOffOutline","mdiEmailOutline","lazy","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","EMAILS_CONSOLE_SECTIONS","BUNDLE_ID","SITE_EMAIL_PACKAGE_KIND","registerEmailRecordRoutes","LIST_CONSENT_STEP_ID","LIST_EXISTING_STEP_ID","LIST_MEMBERS_RESOURCE","SUPPRESSIONS_RESOURCE","EmailsConsolePage","CampaignTopicSelect","CampaignTopicOptionsWidget","CampaignSenderEditorWidget","CampaignDesignCreateWidget","EmailDesignPreview","SitePackageEmailPreview","ListExistingStep","then","steps","default","ListConsentStep","registerEmailTransferResourceUis","label","icon","path","extraSteps","id","after","component","pluginId","registerEmailConsole","zone","surface","layout","description","registerConsoleExtension","displayName","permission","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","sitePackageItemPreview","itemKinds","navItems","href","sections","header","docsTopic","orgNavItems"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/plugin.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n mdiAccountMultipleOutline,\n mdiEmailOffOutline,\n mdiEmailOutline,\n} from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n EMAIL_MESSAGES_ZONE,\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n HOST_EMAIL_TEMPLATES_ZONE,\n} from './components/email-zones'\nimport { EMAILS_CONSOLE_SECTIONS } from './components/emails-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { SITE_EMAIL_PACKAGE_KIND } from './constants/site-package'\nimport { registerEmailRecordRoutes } from './model/email-record-routes'\nimport {\n LIST_CONSENT_STEP_ID,\n LIST_EXISTING_STEP_ID,\n LIST_MEMBERS_RESOURCE,\n SUPPRESSIONS_RESOURCE,\n} from './transfer/email-transfer-catalog'\n\n/** Code-split: the Emails console page only loads when opened. */\nconst EmailsConsolePage = lazy(() => import('./components/emails-console-page'))\n\n/*\n * What this plugin draws inside a campaign's own pages, each loaded only where\n * the campaign owner's zone is on screen.\n */\nconst CampaignTopicSelect = lazy(\n () => import('./components/campaign-topic-select'),\n)\nconst CampaignTopicOptionsWidget = lazy(\n () => import('./components/campaign-topic-options-widget'),\n)\nconst CampaignSenderEditorWidget = lazy(\n () => import('./components/campaign-sender-editor-widget'),\n)\nconst CampaignDesignCreateWidget = lazy(\n () => import('./components/campaign-design-create-widget'),\n)\nconst EmailDesignPreview = lazy(\n () => import('./components/email-design-preview'),\n)\nconst SitePackageEmailPreview = lazy(\n () => import('./components/site-package-email-preview'),\n)\n\n/* The list import's own wizard steps, loaded when the wizard reaches one. */\nconst ListExistingStep = lazy(() =>\n import('./transfer/list-import-steps').then((steps) => ({\n default: steps.ListExistingStep,\n })),\n)\nconst ListConsentStep = lazy(() =>\n import('./transfer/list-import-steps').then((steps) => ({\n default: steps.ListConsentStep,\n })),\n)\n\n/**\n * The client half of what this plugin imports and exports (AGL-3529,\n * AGL-3550): how the wizard and the hub name each resource, and the list import's two steps of its\n * own — whether people the workspace already holds may change, and the\n * statement of permission under what the dry run found. The server half\n * registers from the console declarations.\n */\nexport function registerEmailTransferResourceUis(): void {\n registerPluginTransferResourceUi(\n LIST_MEMBERS_RESOURCE,\n {\n label: 'list members',\n icon: { path: mdiAccountMultipleOutline.path },\n extraSteps: [\n {\n id: LIST_EXISTING_STEP_ID,\n label: 'People already on this list',\n after: 'matching',\n component: ListExistingStep,\n },\n {\n id: LIST_CONSENT_STEP_ID,\n label: 'Permission',\n after: 'conflicts',\n component: ListConsentStep,\n },\n ],\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginTransferResourceUi(\n SUPPRESSIONS_RESOURCE,\n { label: 'suppressions', icon: { path: mdiEmailOffOutline.path } },\n { pluginId: BUNDLE_ID },\n )\n // How the Import & export hub names topics in a workspace package (AGL-3550).\n registerPluginTransferResourceUi(\n 'email.topics',\n { label: 'Email topics' },\n { pluginId: BUNDLE_ID },\n )\n}\n\n/**\n * Console half (AGL-395): registers the Emails nav item + page in the\n * ConsoleExtension registry. Safe to call at console app load — the page is\n * lazy, so no besigner/canvas code loads. The shell renders the Emails nav\n * item and, via its generic plugin route, the page (the messages and their\n * composer, the templates, the audience lists, the topic catalog, the sending\n * identities and the suppression list) — with no edit to the console's own\n * nav or page files.\n */\nexport function registerEmailConsole(): void {\n // Where a message and a sending identity are read (AGL-3080), for the\n // surfaces that link to them without knowing this page's address.\n registerEmailRecordRoutes()\n registerEmailTransferResourceUis()\n /*\n * The two places this plugin's pages hand over to whichever plugin owns\n * campaigns. A message is one send of a campaign and every action on it is\n * that plugin's route, so the Messages section is a zone this page hosts\n * rather than pages this plugin imports; the same holds for the recipients\n * table under a template's report. Named, because a spec calls this\n * registrar without the loader.\n */\n registerPluginZone(\n {\n zone: EMAIL_MESSAGES_ZONE,\n label: 'The Emails page’s Messages section',\n surface: 'console',\n layout: 'bare',\n description:\n 'The whole body of `/emails/messages` and the routes under it, under a site and on the organization’s Emails page. A widget here is handed the site (or `null` and the org mount at the organization level), the Emails page’s base path and the segments under `messages`, and draws the list, one message’s report or its composer.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_REPORT_ZONE,\n label: 'What a template’s emails did',\n surface: 'console',\n description:\n 'On one template’s page, under its name. A widget here reports the messages sent from that template — their figures, the audiences they went to and the messages themselves — read from the plugin that sends them; it is handed the site, the template’s id and the Emails page’s base path.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n label: 'Who received a template’s emails',\n surface: 'console',\n description:\n 'On one template’s page, under its report. A widget here lists the recipients of every send built from that template; it is handed the site and the template’s id.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: HOST_EMAIL_TEMPLATES_ZONE,\n label: 'A site’s email templates',\n surface: 'console',\n // One item in the list's row of actions, beside New template.\n layout: 'bare',\n description:\n 'On a site’s Emails page, beside New template and in the empty list: another way to start an email design. A widget here is handed the site and its org and writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Email',\n /*\n * WHO may open the email console, declared so the shell enforces it.\n *\n * The audiences section reads `orgs/{orgId}/lists/{listId}/members`, and\n * those members are enrolled CONTACTS — an address, a name, and the\n * consent basis recording why the person may be mailed. That is the same\n * org-shared people data the CRM holds, reached from a different page.\n *\n * The rules gate those reads on `isOrgWideMember()` ALONE, with no role\n * condition, so org-wide membership of any role is enough to list every\n * audience the organization has and everybody on it. An org VIEWER — the\n * role that exists to read and change nothing — therefore reads the whole\n * marketing audience today. `data.manage` is what closes that: it\n * defaults to owner, admin and editor, so the population it admits is\n * exactly the one `server-list-gate.ts` accepts a list write from, and\n * the viewer it excludes is the reader the rules never excluded.\n *\n * `data.manage` rather than a key of this plugin's own for the reason the\n * catalog gives for refusing a `marketing.manage`: campaigns are written\n * client-direct against rules that gate on the HOST role, so a new\n * org-level key would name an action with no org-level boundary under it.\n * `data.manage` is not in that position — it already governs the\n * org-shared data this surface exposes, and the list gate already reads\n * the roles it defaults to.\n *\n * A SITE COLLABORATOR holding it opens the page, and the answer is NOT\n * the one Contacts reached. There the listener is scoped and the rules\n * prove the same predicate per document; here the audiences read demands\n * `isOrgWideMember()`, which a collaborator is not, so the org-shared\n * half is already refused beneath the console and the half that remains —\n * this site's own messages, templates and sending identities — is theirs.\n * Refusing the surface outright would take that away to close nothing.\n */\n permission: 'data.manage',\n /*\n * The mail a campaign rides on is this plugin's — the topic catalog, the\n * sending identities, the design document and its renderer — so each is\n * drawn here, in a zone the campaign owner's composer and message page\n * host. Every one reports through a callback; none writes a campaign.\n */\n widgets: [\n {\n slot: 'campaignTopicSelect',\n widgetId: 'email-campaign-topic-select',\n title: 'Topic',\n Component: CampaignTopicSelect,\n },\n {\n slot: 'campaignTopicOptions',\n widgetId: 'email-campaign-topic-options',\n title: 'Topics',\n Component: CampaignTopicOptionsWidget,\n },\n {\n slot: 'campaignSenderEditor',\n widgetId: 'email-campaign-sender-editor',\n title: 'Add a sender',\n Component: CampaignSenderEditorWidget,\n },\n {\n slot: 'campaignDesignCreate',\n widgetId: 'email-campaign-design-create',\n title: 'Design this email',\n Component: CampaignDesignCreateWidget,\n },\n {\n slot: 'campaignDesignPreview',\n widgetId: 'email-campaign-design-preview',\n title: 'Preview',\n Component: EmailDesignPreview,\n },\n {\n // A site email in a site package import, each side drawn by the\n // preview that renders through the send path's code (AGL-3545).\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,\n widgetId: 'email-site-package-preview',\n title: 'Email preview',\n itemKinds: [SITE_EMAIL_PACKAGE_KIND],\n Component: SitePackageEmailPreview,\n },\n ],\n navItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n // Sections as ROUTES (AGL-2501): `/emails/messages` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Emails page, at `/[orgSlug]/emails`: the same six\n * sections over every site. The audiences and the topics are the org's\n * already; the messages, the templates, the sending identities and the\n * suppression lists are read site by site and link into the site that\n * holds them. The page is the same component — handed no site, it renders\n * the org sections.\n *\n * No `navTabId`, the same as the site tab: this plugin's release flag\n * gates the plugin itself, so one flag already holds both levels.\n */\n orgNavItems: [\n {\n label: 'Emails',\n href: '/emails',\n icon: { path: mdiEmailOutline.path },\n sections: EMAILS_CONSOLE_SECTIONS,\n header: {\n title: 'Emails',\n icon: { path: mdiEmailOutline.path },\n docsTopic: 'emailCampaigns',\n },\n Component: EmailsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginTransferResourceUi","registerPluginZone","mdiAccountMultipleOutline","mdiEmailOffOutline","mdiEmailOutline","lazy","EMAIL_MESSAGES_ZONE","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","HOST_EMAIL_TEMPLATES_ZONE","EMAILS_CONSOLE_SECTIONS","BUNDLE_ID","SITE_EMAIL_PACKAGE_KIND","registerEmailRecordRoutes","LIST_CONSENT_STEP_ID","LIST_EXISTING_STEP_ID","LIST_MEMBERS_RESOURCE","SUPPRESSIONS_RESOURCE","EmailsConsolePage","CampaignTopicSelect","CampaignTopicOptionsWidget","CampaignSenderEditorWidget","CampaignDesignCreateWidget","EmailDesignPreview","SitePackageEmailPreview","ListExistingStep","then","steps","default","ListConsentStep","registerEmailTransferResourceUis","label","icon","path","extraSteps","id","after","component","pluginId","registerEmailConsole","zone","surface","layout","description","registerConsoleExtension","displayName","permission","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","sitePackageItemPreview","itemKinds","navItems","href","sections","header","docsTopic","orgNavItems"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,yBAAyB,EACzBC,kBAAkB,EAClBC,eAAe,QACV,yBAAwB;AAC/B,SAASC,IAAI,QAAQ,QAAO;AAC5B,SACEC,mBAAmB,EACnBC,8BAA8B,EAC9BC,0BAA0B,EAC1BC,yBAAyB,QACpB,8BAA0B;AACjC,SAASC,uBAAuB,QAAQ,0CAAsC;AAC9E,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,uBAAuB,QAAQ,8BAA0B;AAClE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,qBAAqB,EACrBC,qBAAqB,QAChB,uCAAmC;AAE1C,gEAAgE,GAChE,MAAMC,oBAAoBb,KAAK,IAAM,MAAM,CAAC;AAE5C;;;CAGC,GACD,MAAMc,sBAAsBd,KAC1B,IAAM,MAAM,CAAC;AAEf,MAAMe,6BAA6Bf,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMgB,6BAA6BhB,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMiB,6BAA6BjB,KACjC,IAAM,MAAM,CAAC;AAEf,MAAMkB,qBAAqBlB,KACzB,IAAM,MAAM,CAAC;AAEf,MAAMmB,0BAA0BnB,KAC9B,IAAM,MAAM,CAAC;AAGf,2EAA2E,GAC3E,MAAMoB,mBAAmBpB,KAAK,IAC5B,MAAM,CAAC,mCAAgCqB,IAAI,CAAC,CAACC,QAAW,CAAA;YACtDC,SAASD,MAAMF,gBAAgB;QACjC,CAAA;AAEF,MAAMI,kBAAkBxB,KAAK,IAC3B,MAAM,CAAC,mCAAgCqB,IAAI,CAAC,CAACC,QAAW,CAAA;YACtDC,SAASD,MAAME,eAAe;QAChC,CAAA;AAGF;;;;;;CAMC,GACD,OAAO,SAASC;IACd9B,iCACEgB,uBACA;QACEe,OAAO;QACPC,MAAM;YAAEC,MAAM/B,0BAA0B+B,IAAI;QAAC;QAC7CC,YAAY;YACV;gBACEC,IAAIpB;gBACJgB,OAAO;gBACPK,OAAO;gBACPC,WAAWZ;YACb;YACA;gBACEU,IAAIrB;gBACJiB,OAAO;gBACPK,OAAO;gBACPC,WAAWR;YACb;SACD;IACH,GACA;QAAES,UAAU3B;IAAU;IAExBX,iCACEiB,uBACA;QAAEc,OAAO;QAAgBC,MAAM;YAAEC,MAAM9B,mBAAmB8B,IAAI;QAAC;IAAE,GACjE;QAAEK,UAAU3B;IAAU;IAExB,8EAA8E;IAC9EX,iCACE,gBACA;QAAE+B,OAAO;IAAe,GACxB;QAAEO,UAAU3B;IAAU;AAE1B;AAEA;;;;;;;;CAQC,GACD,OAAO,SAAS4B;IACd,sEAAsE;IACtE,kEAAkE;IAClE1B;IACAiB;IACA;;;;;;;GAOC,GACD7B,mBACE;QACEuC,MAAMlC;QACNyB,OAAO;QACPU,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACA;QAAEL,UAAU3B;IAAU;IAExBV,mBACE;QACEuC,MAAMhC;QACNuB,OAAO;QACPU,SAAS;QACTE,aACE;IACJ,GACA;QAAEL,UAAU3B;IAAU;IAExBV,mBACE;QACEuC,MAAMjC;QACNwB,OAAO;QACPU,SAAS;QACTE,aACE;IACJ,GACA;QAAEL,UAAU3B;IAAU;IAExBV,mBACE;QACEuC,MAAM/B;QACNsB,OAAO;QACPU,SAAS;QACT,8DAA8D;QAC9DC,QAAQ;QACRC,aACE;IACJ,GACA;QAAEL,UAAU3B;IAAU;IAExBZ,MAAM6C,wBAAwB,CAAC;QAC7BN,UAAU3B;QACVkC,aAAa;QACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCC,GACDC,YAAY;QACZ;;;;;KAKC,GACDC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWhC;YACb;YACA;gBACE6B,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW/B;YACb;YACA;gBACE4B,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW9B;YACb;YACA;gBACE2B,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW7B;YACb;YACA;gBACE0B,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAW5B;YACb;YACA;gBACE,gEAAgE;gBAChE,gEAAgE;gBAChEyB,MAAMjD,MAAMqD,oBAAoB,CAACC,sBAAsB;gBACvDJ,UAAU;gBACVC,OAAO;gBACPI,WAAW;oBAAC1C;iBAAwB;gBACpCuC,WAAW3B;YACb;SACD;QACD+B,UAAU;YACR;gBACExB,OAAO;gBACPyB,MAAM;gBACNxB,MAAM;oBAAEC,MAAM7B,gBAAgB6B,IAAI;gBAAC;gBACnC,oEAAoE;gBACpE,qEAAqE;gBACrE,6CAA6C;gBAC7CwB,UAAU/C;gBACVgD,QAAQ;oBACNR,OAAO;oBACPlB,MAAM;wBAAEC,MAAM7B,gBAAgB6B,IAAI;oBAAC;oBACnC0B,WAAW;gBACb;gBACAR,WAAWjC;YACb;SACD;QACD;;;;;;;;;;KAUC,GACD0C,aAAa;YACX;gBACE7B,OAAO;gBACPyB,MAAM;gBACNxB,MAAM;oBAAEC,MAAM7B,gBAAgB6B,IAAI;gBAAC;gBACnCwB,UAAU/C;gBACVgD,QAAQ;oBACNR,OAAO;oBACPlB,MAAM;wBAAEC,MAAM7B,gBAAgB6B,IAAI;oBAAC;oBACnC0B,WAAW;gBACb;gBACAR,WAAWjC;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
|