@aglyn/plugins-email 1.0.0-beta.179 → 1.0.0-beta.180

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.
Files changed (59) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/components/email-org-mount.d.ts +116 -0
  4. package/src/lib/components/email-org-mount.js +211 -0
  5. package/src/lib/components/email-org-mount.js.map +1 -0
  6. package/src/lib/components/email-topic-detail.d.ts +2 -1
  7. package/src/lib/components/email-topic-detail.js +7 -1
  8. package/src/lib/components/email-topic-detail.js.map +1 -1
  9. package/src/lib/components/email-topics-card.d.ts +2 -1
  10. package/src/lib/components/email-topics-card.js +7 -1
  11. package/src/lib/components/email-topics-card.js.map +1 -1
  12. package/src/lib/components/email-zones.d.ts +28 -1
  13. package/src/lib/components/email-zones.js.map +1 -1
  14. package/src/lib/components/emails-console-page.d.ts +4 -0
  15. package/src/lib/components/emails-console-page.js +107 -5
  16. package/src/lib/components/emails-console-page.js.map +1 -1
  17. package/src/lib/components/list-detail-card.d.ts +19 -3
  18. package/src/lib/components/list-detail-card.js +52 -13
  19. package/src/lib/components/list-detail-card.js.map +1 -1
  20. package/src/lib/components/list-edit-card.d.ts +2 -1
  21. package/src/lib/components/list-edit-card.js +56 -17
  22. package/src/lib/components/list-edit-card.js.map +1 -1
  23. package/src/lib/components/lists-card.d.ts +2 -1
  24. package/src/lib/components/lists-card.js +5 -5
  25. package/src/lib/components/lists-card.js.map +1 -1
  26. package/src/lib/components/org-email-templates-card.d.ts +34 -0
  27. package/src/lib/components/org-email-templates-card.js +470 -0
  28. package/src/lib/components/org-email-templates-card.js.map +1 -0
  29. package/src/lib/components/org-sending-card.d.ts +30 -0
  30. package/src/lib/components/org-sending-card.js +680 -0
  31. package/src/lib/components/org-sending-card.js.map +1 -0
  32. package/src/lib/components/org-sending-domain-detail.d.ts +28 -0
  33. package/src/lib/components/org-sending-domain-detail.js +497 -0
  34. package/src/lib/components/org-sending-domain-detail.js.map +1 -0
  35. package/src/lib/components/org-site-suppressions.d.ts +20 -0
  36. package/src/lib/components/org-site-suppressions.js +99 -0
  37. package/src/lib/components/org-site-suppressions.js.map +1 -0
  38. package/src/lib/components/org-suppressions-card.d.ts +22 -0
  39. package/src/lib/components/org-suppressions-card.js +196 -0
  40. package/src/lib/components/org-suppressions-card.js.map +1 -0
  41. package/src/lib/components/sending-domain-detail.d.ts +12 -0
  42. package/src/lib/components/sending-domain-detail.js +4 -1
  43. package/src/lib/components/sending-domain-detail.js.map +1 -1
  44. package/src/lib/components/suppression-totals.d.ts +63 -0
  45. package/src/lib/components/suppression-totals.js +105 -0
  46. package/src/lib/components/suppression-totals.js.map +1 -0
  47. package/src/lib/components/suppressions-card.js +7 -80
  48. package/src/lib/components/suppressions-card.js.map +1 -1
  49. package/src/lib/components/use-org-email-topics.d.ts +9 -1
  50. package/src/lib/components/use-org-email-topics.js +4 -3
  51. package/src/lib/components/use-org-email-topics.js.map +1 -1
  52. package/src/lib/components/use-org-sending-views.d.ts +39 -0
  53. package/src/lib/components/use-org-sending-views.js +124 -0
  54. package/src/lib/components/use-org-sending-views.js.map +1 -0
  55. package/src/lib/model/sending-domain-status.d.ts +36 -0
  56. package/src/lib/model/sending-domain-status.js +46 -0
  57. package/src/lib/model/sending-domain-status.js.map +1 -1
  58. package/src/lib/plugin.js +29 -1
  59. package/src/lib/plugin.js.map +1 -1
@@ -0,0 +1,470 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
20
+ import { mdiDeleteOutline, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
21
+ import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
+ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
24
+ import { TABLE_PAGE_SIZE_DEFAULT, TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
25
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
+ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
27
+ import { useFirestore, useFirestoreCollection, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
28
+ import { Alert, Button, Chip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
29
+ import { collection, doc, query, Timestamp, updateDoc, where } from "firebase/firestore";
30
+ import { useRouter } from "next/navigation";
31
+ import { useCallback, useEffect, useMemo, useState } from "react";
32
+ import { templateProvenance } from "../model/template-provenance.js";
33
+ import { createEmailScreen } from "../utils/create-email-screen.js";
34
+ import { OrgSiteSelect, orgSiteEmailsPath, orgSiteName, orgSitePageLabel, useEmailOrgMount, useOrgSitePage } from "./email-org-mount.js";
35
+ /**
36
+ * How many of one site's templates the organization's list reads.
37
+ *
38
+ * Lower than the site's own list, because this one reads a page of sites at
39
+ * once: a site with more than this is named under the table, with a link to
40
+ * its own Templates page, which reads its whole set.
41
+ */ export const ORG_TEMPLATE_CEILING = 50;
42
+ /**
43
+ * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.
44
+ *
45
+ * A component rather than a hook in a loop, because the number of sites
46
+ * changes and a hook's call count may not: each mounted reader is one
47
+ * listener, and a page of sites mounts one per site on it.
48
+ *
49
+ * The equality filter is on the QUERY, so a site with a thousand pages reads
50
+ * its emails and nothing else. `collectionCeiling` orders on the document
51
+ * name, which an equality predicate composes onto without a composite index.
52
+ */ function SiteTemplatesReader(props) {
53
+ const { hostId, onRead } = props;
54
+ const firestore = useFirestore();
55
+ const { data, status } = useFirestoreCollection(()=>collectionCeiling(query(collection(firestore, 'hosts', hostId, 'screens'), where('kind', '==', 'email')), ORG_TEMPLATE_CEILING), [
56
+ firestore,
57
+ hostId
58
+ ], {
59
+ idField: '$id'
60
+ });
61
+ // One window per snapshot, so a re-render that brings no new snapshot hands
62
+ // the table the same rows it already holds.
63
+ const window = useMemo(()=>ceilingedWindow(data, ORG_TEMPLATE_CEILING), [
64
+ data
65
+ ]);
66
+ useEffect(()=>{
67
+ onRead(hostId, {
68
+ rows: window.rows,
69
+ truncated: window.truncated,
70
+ settled: status !== 'loading'
71
+ });
72
+ }, [
73
+ window,
74
+ status,
75
+ hostId,
76
+ onRead
77
+ ]);
78
+ return null;
79
+ }
80
+ SiteTemplatesReader.displayName = 'SiteTemplatesReader';
81
+ /**
82
+ * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one
83
+ * table.
84
+ *
85
+ * A template is a screen on ONE site — its besigner, its assets and the
86
+ * messages built from it are that site's — so this table reads each site in
87
+ * turn and names the site on every row. A row opens the template's page on
88
+ * its own site, where its report and its recipients are; nothing here is an
89
+ * organization-level template page, because there is no such thing.
90
+ *
91
+ * The reads are one listener per site, over one page of sites at a time —
92
+ * ordered by name — each capped at {@link ORG_TEMPLATE_CEILING} templates.
93
+ * The shared footer pages the templates in hand, as a site's own list does;
94
+ * an organization with more sites than one page chooses which page of sites
95
+ * is read with the Sites filter above the table, and only that page's sites
96
+ * are read. Both caps say so on screen when they bite, so a missing template
97
+ * is never mistaken for one that does not exist.
98
+ *
99
+ * "New template" asks which site the design is made on — the besigner opens
100
+ * on a site — and skips the question when the org has one site.
101
+ */ export function OrgEmailTemplatesCard() {
102
+ const mount = useEmailOrgMount();
103
+ return mount ? /*#__PURE__*/ _jsx(OrgEmailTemplatesTable, {
104
+ mount: mount
105
+ }) : null;
106
+ }
107
+ OrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard';
108
+ function OrgEmailTemplatesTable(props) {
109
+ const { mount } = props;
110
+ const firestore = useFirestore();
111
+ const router = useRouter();
112
+ const { enqueueSnackbar } = useSnackbar();
113
+ const { confirm } = useConfirmationContext();
114
+ const createHostResource = useHostResourceApi();
115
+ const createHostVersion = useHostVersionApi();
116
+ const sitePage = useOrgSitePage(mount);
117
+ const { sites } = sitePage;
118
+ const [reads, setReads] = useState({});
119
+ const onRead = useCallback((hostId, read)=>setReads((current)=>{
120
+ const previous = current[hostId];
121
+ // An answer the table already holds changes nothing to draw.
122
+ if (previous && previous.rows === read.rows && previous.truncated === read.truncated && previous.settled === read.settled) {
123
+ return current;
124
+ }
125
+ return _extends({}, current, {
126
+ [hostId]: read
127
+ });
128
+ }), []);
129
+ /*
130
+ * One row per template, named by site. The row id joins the site to the
131
+ * screen id: two sites can hold a screen with the same id, and a grid that
132
+ * keyed on the screen alone would draw one of them twice.
133
+ */ const rows = useMemo(()=>sites.flatMap((site)=>{
134
+ var _ref;
135
+ var _reads_site_id;
136
+ return ((_ref = (_reads_site_id = reads[site.id]) == null ? void 0 : _reads_site_id.rows) != null ? _ref : []).filter((screen)=>!screen['deletedAt']).map((screen)=>_extends({}, screen, {
137
+ $id: `${site.id}:${screen['$id']}`,
138
+ screenId: String(screen['$id']),
139
+ hostId: site.id
140
+ }));
141
+ }).sort((a, b)=>{
142
+ var _a_displayName, _b_displayName;
143
+ return String((_a_displayName = a['displayName']) != null ? _a_displayName : '').localeCompare(String((_b_displayName = b['displayName']) != null ? _b_displayName : '')) || orgSiteName(mount, a.hostId).localeCompare(orgSiteName(mount, b.hostId));
144
+ }), [
145
+ sites,
146
+ reads,
147
+ mount
148
+ ]);
149
+ const settled = sites.every((site)=>{
150
+ var _reads_site_id;
151
+ return (_reads_site_id = reads[site.id]) == null ? void 0 : _reads_site_id.settled;
152
+ });
153
+ const crowded = sites.filter((site)=>{
154
+ var _reads_site_id;
155
+ return (_reads_site_id = reads[site.id]) == null ? void 0 : _reads_site_id.truncated;
156
+ });
157
+ /*
158
+ * The templates in hand, a page at a time on the shared footer. Choosing
159
+ * another page of sites starts the templates from their first page: page
160
+ * four of one set of sites is not a position in another.
161
+ */ const [page, setPage] = useState(0);
162
+ const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
163
+ useEffect(()=>setPage(0), [
164
+ sitePage.page
165
+ ]);
166
+ const shown = useMemo(()=>rows.slice(page * pageSize, page * pageSize + pageSize), [
167
+ rows,
168
+ page,
169
+ pageSize
170
+ ]);
171
+ const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize);
172
+ const templateName = (row)=>{
173
+ var _row_displayName;
174
+ return String((_row_displayName = row['displayName']) != null ? _row_displayName : 'Untitled template');
175
+ };
176
+ /** The template's own page, on the site that holds it. */ const templateHref = (row)=>orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`);
177
+ const siteSubdomain = (hostId)=>{
178
+ var _ref;
179
+ var _mount_hosts_find;
180
+ return (_ref = (_mount_hosts_find = mount.hosts.find((site)=>site.id === hostId)) == null ? void 0 : _mount_hosts_find.subdomain) != null ? _ref : null;
181
+ };
182
+ const besignerHref = (hostId, screenId, versionId)=>{
183
+ const subdomain = siteSubdomain(hostId);
184
+ return subdomain && versionId ? buildRoute(Route.SCREEN_BESIGNER, {
185
+ orgSlug: mount.orgSlug,
186
+ host: subdomain,
187
+ screenId,
188
+ versionId: String(versionId)
189
+ }) : null;
190
+ };
191
+ const handleDelete = async (row)=>{
192
+ const confirmed = await confirm({
193
+ title: 'Delete this template?',
194
+ description: `"${templateName(row)}" will be removed from ` + `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` + 'keep their reports.',
195
+ confirmationText: 'Delete',
196
+ confirmationButtonProps: {
197
+ color: 'error'
198
+ }
199
+ }).then(()=>true).catch(()=>false);
200
+ if (!confirmed) return;
201
+ await updateDoc(doc(firestore, 'hosts', row['hostId'], 'screens', row['screenId']), {
202
+ deletedAt: Timestamp.now()
203
+ });
204
+ };
205
+ const rowActions = (row)=>{
206
+ const page = templateHref(row);
207
+ const editor = besignerHref(row['hostId'], row['screenId'], row['versionId']);
208
+ const unlinkedReason = 'This site has no console address yet, so it cannot be linked to.';
209
+ return [
210
+ {
211
+ key: 'details',
212
+ label: 'Open details',
213
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
214
+ path: mdiEyeOutline.path,
215
+ size: 0.8
216
+ }),
217
+ href: page != null ? page : undefined,
218
+ disabled: !page,
219
+ disabledReason: unlinkedReason
220
+ },
221
+ {
222
+ key: 'besigner',
223
+ label: 'Edit in besigner',
224
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
225
+ path: mdiPencilOutline.path,
226
+ size: 0.8
227
+ }),
228
+ href: editor != null ? editor : undefined,
229
+ disabled: !editor,
230
+ disabledReason: row['versionId'] ? unlinkedReason : 'This template has never been saved in the besigner. Open it from its page.'
231
+ },
232
+ {
233
+ key: 'delete',
234
+ label: 'Delete',
235
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
236
+ path: mdiDeleteOutline.path,
237
+ size: 0.8
238
+ }),
239
+ destructive: true,
240
+ onClick: ()=>void handleDelete(row)
241
+ }
242
+ ];
243
+ };
244
+ const columns = [
245
+ {
246
+ field: 'displayName',
247
+ headerName: 'Template',
248
+ flex: 1,
249
+ minWidth: 220,
250
+ valueGetter: (_value, row)=>templateName(row),
251
+ renderCell: ({ row, value })=>{
252
+ const href = templateHref(row);
253
+ return href ? /*#__PURE__*/ _jsx(AppLink, {
254
+ href: href,
255
+ // The row's own handler would fire too and push the same route
256
+ // twice — one history entry per back press.
257
+ onClick: (event)=>event.stopPropagation(),
258
+ children: value
259
+ }) : value;
260
+ }
261
+ },
262
+ {
263
+ field: 'site',
264
+ headerName: 'Site',
265
+ width: 170,
266
+ valueGetter: (_value, row)=>orgSiteName(mount, row.hostId)
267
+ },
268
+ {
269
+ // Whose template this is, as on a site's own list.
270
+ field: 'origin',
271
+ headerName: 'Origin',
272
+ width: 130,
273
+ valueGetter: (_value, row)=>templateProvenance(row).origin === 'installed' ? 'Installed' : 'Yours',
274
+ renderCell: ({ value })=>value === 'Installed' ? /*#__PURE__*/ _jsx(Chip, {
275
+ size: "small",
276
+ label: "Installed"
277
+ }) : /*#__PURE__*/ _jsx(Typography, {
278
+ variant: "body2",
279
+ color: "text.secondary",
280
+ children: 'Yours'
281
+ })
282
+ },
283
+ listActionsColumn((row)=>/*#__PURE__*/ _jsx(ListRowActions, {
284
+ label: templateName(row),
285
+ items: rowActions(row)
286
+ }), {
287
+ width: 72
288
+ })
289
+ ];
290
+ /*==========================================
291
+ * CREATE, ON A SITE.
292
+ *
293
+ * The same document a site's own list creates — `createEmailScreen`
294
+ * through the quota-enforcing resources route — made as the chosen site,
295
+ * then straight into that site's besigner. One site answers the question
296
+ * itself; otherwise the dialog asks, defaulting to the session's pick.
297
+ *=========================================*/ const [choosing, setChoosing] = useState(false);
298
+ const [createHostId, setCreateHostId] = useState('');
299
+ const [creating, setCreating] = useState(false);
300
+ const create = async (hostId)=>{
301
+ if (creating || !hostId) return;
302
+ setCreating(true);
303
+ try {
304
+ const { screenId, versionId } = await createEmailScreen(hostId, createHostResource, createHostVersion);
305
+ setChoosing(false);
306
+ const editor = besignerHref(hostId, screenId, versionId);
307
+ if (editor) {
308
+ void router.push(editor);
309
+ } else {
310
+ enqueueSnackbar(`Template created on ${orgSiteName(mount, hostId)}. Open it from ` + 'that site’s Emails page.', {
311
+ variant: 'success'
312
+ });
313
+ }
314
+ } catch (error) {
315
+ var _ref;
316
+ console.error(error);
317
+ enqueueSnackbar((_ref = error == null ? void 0 : error.message) != null ? _ref : 'Creating the template failed', {
318
+ variant: 'error'
319
+ });
320
+ } finally{
321
+ setCreating(false);
322
+ }
323
+ };
324
+ const handleNew = ()=>{
325
+ var _mount_pickedHostId;
326
+ if (mount.hosts.length === 1) return void create(mount.hosts[0].id);
327
+ setCreateHostId((_mount_pickedHostId = mount.pickedHostId) != null ? _mount_pickedHostId : '');
328
+ setChoosing(true);
329
+ };
330
+ const noSites = mount.hostsReady && mount.hosts.length === 0;
331
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
332
+ header: 'Templates',
333
+ help: pluginDocsHelp('designedEmails', {
334
+ anchor: '#create-a-template'
335
+ }),
336
+ contentGutterX: true,
337
+ contentGutterY: true,
338
+ HeaderProps: {
339
+ action: /*#__PURE__*/ _jsx(Button, {
340
+ size: "small",
341
+ variant: "contained",
342
+ disabled: noSites || creating,
343
+ onClick: handleNew,
344
+ children: creating ? 'Creating…' : 'New template'
345
+ })
346
+ },
347
+ children: [
348
+ sites.map((site)=>/*#__PURE__*/ _jsx(SiteTemplatesReader, {
349
+ hostId: site.id,
350
+ onRead: onRead
351
+ }, site.id)),
352
+ /*#__PURE__*/ _jsxs(Stack, {
353
+ spacing: 1.5,
354
+ children: [
355
+ sitePageCount > 1 ? /*#__PURE__*/ _jsx(TextField, {
356
+ select: true,
357
+ size: "small",
358
+ label: "Sites",
359
+ value: sitePage.page,
360
+ onChange: (event)=>sitePage.setPage(Number(event.target.value)),
361
+ sx: {
362
+ maxWidth: 260
363
+ },
364
+ children: Array.from({
365
+ length: sitePageCount
366
+ }, (_, index)=>/*#__PURE__*/ _jsx(MenuItem, {
367
+ value: index,
368
+ children: orgSitePageLabel({
369
+ from: index * sitePage.pageSize + 1,
370
+ to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),
371
+ count: sitePage.count
372
+ })
373
+ }, index))
374
+ }) : null,
375
+ noSites ? /*#__PURE__*/ _jsx(Typography, {
376
+ variant: "body2",
377
+ color: "text.secondary",
378
+ children: 'This organization has no sites yet. A template is designed on ' + 'a site, in its besigner, so it appears here once there is one.'
379
+ }) : rows.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
380
+ variant: "body2",
381
+ color: "text.secondary",
382
+ children: settled ? 'None of these sites has an email template yet. Design one ' + 'here, then send it from a campaign — a new template opens in ' + 'the besigner with email-safe components only.' : 'Loading templates…'
383
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
384
+ children: [
385
+ /*#__PURE__*/ _jsx(ListTable, {
386
+ "aria-label": "Email templates",
387
+ rows: shown,
388
+ columns: columns,
389
+ rowHeight: TABLE_ROW_HEIGHT,
390
+ // Paged by the footer below, so the grid must not also slice.
391
+ hideFooter: true,
392
+ onOpen: (_id, row)=>{
393
+ const href = templateHref(row);
394
+ if (href) router.push(href);
395
+ }
396
+ }),
397
+ /*#__PURE__*/ _jsx(ListPagination, {
398
+ page: page,
399
+ pageSize: pageSize,
400
+ rowCount: shown.length,
401
+ count: rows.length,
402
+ onPageChange: setPage,
403
+ onPageSizeChange: setPageSize
404
+ })
405
+ ]
406
+ }),
407
+ crowded.map((site)=>{
408
+ const href = orgSiteEmailsPath(mount, site.id, 'templates');
409
+ return /*#__PURE__*/ _jsxs(Alert, {
410
+ severity: "info",
411
+ children: [
412
+ `${orgSiteName(mount, site.id)} has more than ` + `${ORG_TEMPLATE_CEILING} templates, and only the first of them ` + 'are listed here. ',
413
+ href ? /*#__PURE__*/ _jsx(AppLink, {
414
+ href: href,
415
+ children: 'See all of its templates'
416
+ }) : null
417
+ ]
418
+ }, site.id);
419
+ })
420
+ ]
421
+ }),
422
+ /*#__PURE__*/ _jsxs(Dialog, {
423
+ open: choosing,
424
+ onClose: creating ? undefined : ()=>setChoosing(false),
425
+ fullWidth: true,
426
+ maxWidth: "xs",
427
+ children: [
428
+ /*#__PURE__*/ _jsx(DialogTitle, {
429
+ children: 'New template'
430
+ }),
431
+ /*#__PURE__*/ _jsxs(DialogContent, {
432
+ children: [
433
+ /*#__PURE__*/ _jsx(DialogContentText, {
434
+ sx: {
435
+ mb: 2
436
+ },
437
+ children: 'A template is designed on one site, in its besigner, and is ' + 'sent from that site’s campaigns.'
438
+ }),
439
+ /*#__PURE__*/ _jsx(OrgSiteSelect, {
440
+ mount: mount,
441
+ label: "Site",
442
+ value: createHostId,
443
+ onChange: setCreateHostId
444
+ })
445
+ ]
446
+ }),
447
+ /*#__PURE__*/ _jsxs(DialogActions, {
448
+ children: [
449
+ /*#__PURE__*/ _jsx(Button, {
450
+ onClick: ()=>setChoosing(false),
451
+ disabled: creating,
452
+ children: 'Cancel'
453
+ }),
454
+ /*#__PURE__*/ _jsx(Button, {
455
+ variant: "contained",
456
+ disabled: !createHostId || creating,
457
+ onClick: ()=>void create(createHostId),
458
+ children: creating ? 'Creating…' : 'Create and open'
459
+ })
460
+ ]
461
+ })
462
+ ]
463
+ })
464
+ ]
465
+ });
466
+ }
467
+ OrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable';
468
+ export default OrgEmailTemplatesCard;
469
+
470
+ //# sourceMappingURL=org-email-templates-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-email-templates-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_ROW_HEIGHT,\n} from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n query,\n Timestamp,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport {\n OrgSiteSelect,\n orgSiteEmailsPath,\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\n\n/**\n * How many of one site's templates the organization's list reads.\n *\n * Lower than the site's own list, because this one reads a page of sites at\n * once: a site with more than this is named under the table, with a link to\n * its own Templates page, which reads its whole set.\n */\nexport const ORG_TEMPLATE_CEILING = 50\n\n/** One site's read, as the table assembles it. */\ninterface SiteRead {\n rows: Array<Record<string, any>>\n /** The site holds more templates than {@link ORG_TEMPLATE_CEILING}. */\n truncated: boolean\n /** The read has answered — rows or none. */\n settled: boolean\n}\n\n/**\n * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.\n *\n * A component rather than a hook in a loop, because the number of sites\n * changes and a hook's call count may not: each mounted reader is one\n * listener, and a page of sites mounts one per site on it.\n *\n * The equality filter is on the QUERY, so a site with a thousand pages reads\n * its emails and nothing else. `collectionCeiling` orders on the document\n * name, which an equality predicate composes onto without a composite index.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, onRead } = props\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n collectionCeiling(\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n where('kind', '==', 'email'),\n ),\n ORG_TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n // One window per snapshot, so a re-render that brings no new snapshot hands\n // the table the same rows it already holds.\n const window = useMemo(\n () => ceilingedWindow(data, ORG_TEMPLATE_CEILING),\n [data],\n )\n useEffect(() => {\n onRead(hostId, {\n rows: window.rows,\n truncated: window.truncated,\n settled: status !== 'loading',\n })\n }, [window, status, hostId, onRead])\n return null\n}\nSiteTemplatesReader.displayName = 'SiteTemplatesReader'\n\n/**\n * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one\n * table.\n *\n * A template is a screen on ONE site — its besigner, its assets and the\n * messages built from it are that site's — so this table reads each site in\n * turn and names the site on every row. A row opens the template's page on\n * its own site, where its report and its recipients are; nothing here is an\n * organization-level template page, because there is no such thing.\n *\n * The reads are one listener per site, over one page of sites at a time —\n * ordered by name — each capped at {@link ORG_TEMPLATE_CEILING} templates.\n * The shared footer pages the templates in hand, as a site's own list does;\n * an organization with more sites than one page chooses which page of sites\n * is read with the Sites filter above the table, and only that page's sites\n * are read. Both caps say so on screen when they bite, so a missing template\n * is never mistaken for one that does not exist.\n *\n * \"New template\" asks which site the design is made on — the besigner opens\n * on a site — and skips the question when the org has one site.\n */\nexport function OrgEmailTemplatesCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgEmailTemplatesTable mount={mount} /> : null\n}\nOrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard'\n\nfunction OrgEmailTemplatesTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const sitePage = useOrgSitePage(mount)\n const { sites } = sitePage\n\n const [reads, setReads] = useState<Record<string, SiteRead>>({})\n const onRead = useCallback(\n (hostId: string, read: SiteRead) =>\n setReads((current) => {\n const previous = current[hostId]\n // An answer the table already holds changes nothing to draw.\n if (\n previous &&\n previous.rows === read.rows &&\n previous.truncated === read.truncated &&\n previous.settled === read.settled\n ) {\n return current\n }\n return { ...current, [hostId]: read }\n }),\n [],\n )\n\n /*\n * One row per template, named by site. The row id joins the site to the\n * screen id: two sites can hold a screen with the same id, and a grid that\n * keyed on the screen alone would draw one of them twice.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? [])\n .filter((screen) => !screen['deletedAt'])\n .map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n })),\n )\n .sort(\n (a, b) =>\n String(a['displayName'] ?? '').localeCompare(\n String(b['displayName'] ?? ''),\n ) ||\n orgSiteName(mount, a.hostId).localeCompare(\n orgSiteName(mount, b.hostId),\n ),\n ),\n [sites, reads, mount],\n )\n const settled = sites.every((site) => reads[site.id]?.settled)\n const crowded = sites.filter((site) => reads[site.id]?.truncated)\n\n /*\n * The templates in hand, a page at a time on the shared footer. Choosing\n * another page of sites starts the templates from their first page: page\n * four of one set of sites is not a position in another.\n */\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n useEffect(() => setPage(0), [sitePage.page])\n const shown = useMemo(\n () => rows.slice(page * pageSize, page * pageSize + pageSize),\n [rows, page, pageSize],\n )\n const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize)\n\n const templateName = (row: Record<string, any>) =>\n String(row['displayName'] ?? 'Untitled template')\n /** The template's own page, on the site that holds it. */\n const templateHref = (row: Record<string, any>) =>\n orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`)\n const siteSubdomain = (hostId: string) =>\n mount.hosts.find((site) => site.id === hostId)?.subdomain ?? null\n const besignerHref = (\n hostId: string,\n screenId: string,\n versionId: unknown,\n ) => {\n const subdomain = siteSubdomain(hostId)\n return subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug: mount.orgSlug,\n host: subdomain,\n screenId,\n versionId: String(versionId),\n })\n : null\n }\n\n const handleDelete = async (row: Record<string, any>) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${templateName(row)}\" will be removed from ` +\n `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` +\n 'keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', row['hostId'], 'screens', row['screenId']),\n { deletedAt: Timestamp.now() },\n )\n }\n\n const rowActions = (row: Record<string, any>): RowActionsMenuItem[] => {\n const page = templateHref(row)\n const editor = besignerHref(\n row['hostId'],\n row['screenId'],\n row['versionId'],\n )\n const unlinkedReason =\n 'This site has no console address yet, so it cannot be linked to.'\n return [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: page ?? undefined,\n disabled: !page,\n disabledReason: unlinkedReason,\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: editor ?? undefined,\n disabled: !editor,\n disabledReason: row['versionId']\n ? unlinkedReason\n : 'This template has never been saved in the besigner. Open it from its page.',\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(row),\n },\n ]\n }\n\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => {\n const href = templateHref(row)\n return href ? (\n <AppLink\n href={href}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ) : (\n value\n )\n },\n },\n {\n field: 'site',\n headerName: 'Site',\n width: 170,\n valueGetter: (_value, row) => orgSiteName(mount, row.hostId),\n },\n {\n // Whose template this is, as on a site's own list.\n field: 'origin',\n headerName: 'Origin',\n width: 130,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'Installed' : 'Yours',\n renderCell: ({ value }) =>\n value === 'Installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n /*==========================================\n * CREATE, ON A SITE.\n *\n * The same document a site's own list creates — `createEmailScreen`\n * through the quota-enforcing resources route — made as the chosen site,\n * then straight into that site's besigner. One site answers the question\n * itself; otherwise the dialog asks, defaulting to the session's pick.\n *=========================================*/\n const [choosing, setChoosing] = useState(false)\n const [createHostId, setCreateHostId] = useState('')\n const [creating, setCreating] = useState(false)\n\n const create = async (hostId: string) => {\n if (creating || !hostId) return\n setCreating(true)\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n setChoosing(false)\n const editor = besignerHref(hostId, screenId, versionId)\n if (editor) {\n void router.push(editor)\n } else {\n enqueueSnackbar(\n `Template created on ${orgSiteName(mount, hostId)}. Open it from ` +\n 'that site’s Emails page.',\n { variant: 'success' },\n )\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n } finally {\n setCreating(false)\n }\n }\n\n const handleNew = () => {\n if (mount.hosts.length === 1) return void create(mount.hosts[0].id)\n setCreateHostId(mount.pickedHostId ?? '')\n setChoosing(true)\n }\n\n const noSites = mount.hostsReady && mount.hosts.length === 0\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#create-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={noSites || creating}\n onClick={handleNew}\n >\n {creating ? 'Creating…' : 'New template'}\n </Button>\n ),\n }}\n >\n {sites.map((site) => (\n <SiteTemplatesReader key={site.id} hostId={site.id} onRead={onRead} />\n ))}\n <Stack spacing={1.5}>\n {/*\n WHICH SITES' TEMPLATES ARE LISTED, when there are more sites than\n one page holds. Only the chosen page's sites are read; a template on\n a site of another page is one choice away, not missing.\n */}\n {sitePageCount > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Sites\"\n value={sitePage.page}\n onChange={(event) => sitePage.setPage(Number(event.target.value))}\n sx={{ maxWidth: 260 }}\n >\n {Array.from({ length: sitePageCount }, (_, index) => (\n <MenuItem key={index} value={index}>\n {orgSitePageLabel({\n from: index * sitePage.pageSize + 1,\n to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),\n count: sitePage.count,\n })}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A template is designed on ' +\n 'a site, in its besigner, so it appears here once there is one.'}\n </Typography>\n ) : rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'None of these sites has an email template yet. Design one ' +\n 'here, then send it from a campaign — a new template opens in ' +\n 'the besigner with email-safe components only.'\n : 'Loading templates…'}\n </Typography>\n ) : (\n <>\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={columns}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n onOpen={(_id, row) => {\n const href = templateHref(row)\n if (href) router.push(href)\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {crowded.map((site) => {\n const href = orgSiteEmailsPath(mount, site.id, 'templates')\n return (\n <Alert key={site.id} severity=\"info\">\n {`${orgSiteName(mount, site.id)} has more than ` +\n `${ORG_TEMPLATE_CEILING} templates, and only the first of them ` +\n 'are listed here. '}\n {href ? (\n <AppLink href={href}>{'See all of its templates'}</AppLink>\n ) : null}\n </Alert>\n )\n })}\n </Stack>\n\n <Dialog\n open={choosing}\n onClose={creating ? undefined : () => setChoosing(false)}\n fullWidth\n maxWidth=\"xs\"\n >\n <DialogTitle>{'New template'}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'A template is designed on one site, in its besigner, and is ' +\n 'sent from that site’s campaigns.'}\n </DialogContentText>\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={createHostId}\n onChange={setCreateHostId}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setChoosing(false)} disabled={creating}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!createHostId || creating}\n onClick={() => void create(createHostId)}\n >\n {creating ? 'Creating…' : 'Create and open'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable'\n\nexport default OrgEmailTemplatesCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","collection","doc","query","Timestamp","updateDoc","where","useRouter","useCallback","useEffect","useMemo","useState","templateProvenance","createEmailScreen","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","SiteTemplatesReader","props","hostId","onRead","firestore","data","status","idField","window","rows","truncated","settled","displayName","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","sites","reads","setReads","read","current","previous","flatMap","site","id","filter","screen","map","$id","screenId","String","sort","a","b","localeCompare","every","crowded","page","setPage","pageSize","setPageSize","shown","slice","sitePageCount","Math","ceil","count","templateName","row","templateHref","siteSubdomain","hosts","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","host","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","deletedAt","now","rowActions","editor","unlinkedReason","key","label","icon","path","size","href","undefined","disabled","disabledReason","destructive","onClick","columns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","value","event","stopPropagation","width","origin","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","length","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","aria-label","rowHeight","hideFooter","onOpen","_id","rowCount","onPageChange","onPageSizeChange","severity","open","onClose","fullWidth","mb"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export interface OrgSendingCardProps {
2
+ /** The organization's Emails page, which the domain pages hang beneath. */
3
+ basePath: string;
4
+ }
5
+ /**
6
+ * WHO EVERY SITE'S MAIL COMES FROM, AND THE DOMAINS THE ORGANIZATION PROVED.
7
+ *
8
+ * The two halves of a site's Sending section, over the organization. A domain
9
+ * is proved once by the org — publishing its records is a statement about a
10
+ * zone — and each site then chooses which proved domain it sends as, so the
11
+ * page is two tables: the sites, each with what it sends as right now and its
12
+ * default sender, and the domains, each with the sites sending as it.
13
+ *
14
+ * Every site row is read through the same route the site's own page reads,
15
+ * so what it says is what a campaign sent as that site would get. The reads
16
+ * are one per site over one page of sites, and the org's domains come back on
17
+ * each of them, so there is no separate read for the domain table. What the
18
+ * domain table says about which sites send as a domain is what the pages read
19
+ * so far say, and the removal confirmation counts the sites not yet read.
20
+ *
21
+ * Managing — adding a domain, checking it, choosing what a site sends as, or
22
+ * editing a sender — needs the organization admin role, exactly as on a
23
+ * site's page, and the page says so to anybody else rather than offering
24
+ * controls that would refuse.
25
+ */
26
+ export declare function OrgSendingCard(props: OrgSendingCardProps): import("react").JSX.Element;
27
+ export declare namespace OrgSendingCard {
28
+ var displayName: string;
29
+ }
30
+ export default OrgSendingCard;