@aglyn/plugins-email 1.0.0-beta.199 → 1.0.0-beta.200

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 (29) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/email-screens-card.d.ts +8 -9
  3. package/src/lib/components/email-screens-card.js +83 -100
  4. package/src/lib/components/email-screens-card.js.map +1 -1
  5. package/src/lib/components/email-template-detail.js +3 -2
  6. package/src/lib/components/email-template-detail.js.map +1 -1
  7. package/src/lib/components/list-edit-card.js +2 -2
  8. package/src/lib/components/list-edit-card.js.map +1 -1
  9. package/src/lib/components/list-members-panel.js +72 -93
  10. package/src/lib/components/list-members-panel.js.map +1 -1
  11. package/src/lib/components/lists-card.js +70 -77
  12. package/src/lib/components/lists-card.js.map +1 -1
  13. package/src/lib/components/org-email-templates-card.d.ts +10 -6
  14. package/src/lib/components/org-email-templates-card.js +145 -90
  15. package/src/lib/components/org-email-templates-card.js.map +1 -1
  16. package/src/lib/components/suppressions-card.js +73 -69
  17. package/src/lib/components/suppressions-card.js.map +1 -1
  18. package/src/lib/constants/list-queries.d.ts +34 -0
  19. package/src/lib/constants/list-queries.js +256 -0
  20. package/src/lib/constants/list-queries.js.map +1 -0
  21. package/src/lib/server-email-drafts.js +3 -4
  22. package/src/lib/server-email-drafts.js.map +1 -1
  23. package/src/lib/server-suppressions.js +3 -1
  24. package/src/lib/server-suppressions.js.map +1 -1
  25. package/src/lib/server.js +3 -1
  26. package/src/lib/server.js.map +1 -1
  27. package/src/lib/utils/email-template-soft-delete.d.ts +39 -0
  28. package/src/lib/utils/email-template-soft-delete.js +41 -0
  29. package/src/lib/utils/email-template-soft-delete.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-email",
3
- "version": "1.0.0-beta.199",
3
+ "version": "1.0.0-beta.200",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.199",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.199",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.199",
31
- "@aglyn/shared-data-types": "1.0.0-beta.199",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.199",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.199",
34
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.199",
35
- "@aglyn/shared-ui-next": "1.0.0-beta.199",
36
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.199",
37
- "@aglyn/shared-ui-theme": "1.0.0-beta.199",
38
- "@aglyn/shared-util-email": "1.0.0-beta.199",
39
- "@aglyn/shared-util-http": "1.0.0-beta.199",
40
- "@aglyn/shared-util-timestamp": "1.0.0-beta.199",
41
- "@aglyn/shared-util-tools": "1.0.0-beta.199",
42
- "@aglyn/tenant-data-admin": "1.0.0-beta.199",
43
- "@aglyn/tenant-feature-instance": "1.0.0-beta.199",
28
+ "@aglyn/aglyn": "1.0.0-beta.200",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.200",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.200",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.200",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.200",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.200",
34
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.200",
35
+ "@aglyn/shared-ui-next": "1.0.0-beta.200",
36
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
37
+ "@aglyn/shared-ui-theme": "1.0.0-beta.200",
38
+ "@aglyn/shared-util-email": "1.0.0-beta.200",
39
+ "@aglyn/shared-util-http": "1.0.0-beta.200",
40
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.200",
41
+ "@aglyn/shared-util-tools": "1.0.0-beta.200",
42
+ "@aglyn/tenant-data-admin": "1.0.0-beta.200",
43
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.200",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -20,16 +20,15 @@
20
20
  * audiences table reads by. Delete in particular: it sat inline, one mis-click
21
21
  * from the name beside it.
22
22
  *
23
- * ## Why the read is a CEILING with a probe
23
+ * ## The list IS its query
24
24
  *
25
- * `screens` holds every kind of screen the site has and the email ones are
26
- * picked out here, so the cap cannot be a page: a page of the collection is
27
- * not a page of this list. `collectionCeiling` orders on the document name,
28
- * which is the one ordering that cannot drop a screen — `orderBy('displayName')`
29
- * matches only documents that HAVE the field, so a screen created without a
30
- * name would vanish rather than sort oddly. It reads one past the ceiling so
31
- * the card can SAY when the site holds more than it drew, and the footer under
32
- * the table pages the window the card already has.
25
+ * `kind == 'email'`, by name, paged by the query — and every Filters clause
26
+ * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so
27
+ * a page is a page of the templates that match and none is matched in the
28
+ * browser. The order is `nameLower`, which every screen writer stamps from
29
+ * `displayName`; a deleted template has its name keys cleared with the
30
+ * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than
31
+ * the card dropping it after the read.
33
32
  */
34
33
  export declare function EmailScreensCard(props: {
35
34
  hostId: string;
@@ -14,54 +14,28 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { _ as _extends } from "@swc/helpers/_/_extends";
18
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
18
  import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
20
19
  import { mdiDeleteOutline, mdiContentCopy, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
21
20
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
21
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
+ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
23
24
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
24
- import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
26
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
26
- import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
27
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
27
28
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
28
- import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
29
- import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useFirestoreCollection, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
30
- import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
31
- import { collection, doc, Timestamp, updateDoc } from "firebase/firestore";
29
+ import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
30
+ import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
31
+ import { Button, Chip, Stack, Typography } from "@mui/material";
32
+ import { collection, doc, updateDoc } from "firebase/firestore";
32
33
  import { useRouter } from "next/navigation";
33
34
  import { useMemo } from "react";
35
+ import { EMAIL_TEMPLATE_BASE, EMAIL_TEMPLATE_FILTER_HEADERS, EMAIL_TEMPLATE_QUERY } from "../constants/list-queries.js";
34
36
  import { templateProvenance } from "../model/template-provenance.js";
35
37
  import { createEmailScreen } from "../utils/create-email-screen.js";
36
- /** How many of the site's screens one read of this list covers. */ const TEMPLATE_CEILING = 200;
37
- /*
38
- * What the templates grid's Filters panel offers (AGL-3317). The card holds
39
- * the templates its capped window read, so it answers the panel over all of
40
- * them; the notice under the table says when the cap truncated the window.
41
- * Origin reads `originKey`, the provenance the column draws.
42
- */ const TEMPLATE_FILTER_FIELDS = [
43
- inMemoryListField('displayName', 'text', 'templateName'),
44
- inMemoryListField('origin', 'select', 'originKey')
45
- ];
46
- const TEMPLATE_FILTER_HEADERS = {
47
- displayName: 'Template',
48
- origin: 'Origin'
49
- };
50
- const TEMPLATE_FILTER_OPTIONS = {
51
- origin: [
52
- {
53
- value: 'local',
54
- label: 'Yours'
55
- },
56
- {
57
- value: 'installed',
58
- label: 'Installed'
59
- }
60
- ]
61
- };
62
- const TEMPLATE_SEARCH_FIELDS = [
63
- 'templateName'
64
- ];
38
+ import { emailTemplateSoftDelete } from "../utils/email-template-soft-delete.js";
65
39
  // The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/
66
40
  // versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the
67
41
  // pre-AGL-621/622 shape — so every "Edit"/"Design" jump out of the Emails
@@ -95,16 +69,15 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
95
69
  * audiences table reads by. Delete in particular: it sat inline, one mis-click
96
70
  * from the name beside it.
97
71
  *
98
- * ## Why the read is a CEILING with a probe
72
+ * ## The list IS its query
99
73
  *
100
- * `screens` holds every kind of screen the site has and the email ones are
101
- * picked out here, so the cap cannot be a page: a page of the collection is
102
- * not a page of this list. `collectionCeiling` orders on the document name,
103
- * which is the one ordering that cannot drop a screen — `orderBy('displayName')`
104
- * matches only documents that HAVE the field, so a screen created without a
105
- * name would vanish rather than sort oddly. It reads one past the ceiling so
106
- * the card can SAY when the site holds more than it drew, and the footer under
107
- * the table pages the window the card already has.
74
+ * `kind == 'email'`, by name, paged by the query — and every Filters clause
75
+ * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so
76
+ * a page is a page of the templates that match and none is matched in the
77
+ * browser. The order is `nameLower`, which every screen writer stamps from
78
+ * `displayName`; a deleted template has its name keys cleared with the
79
+ * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than
80
+ * the card dropping it after the read.
108
81
  */ export function EmailScreensCard(props) {
109
82
  const { hostId, basePath } = props;
110
83
  const { orgSlug, subdomain } = useConsoleHostRoute(hostId);
@@ -122,34 +95,28 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
122
95
  })
123
96
  });
124
97
  const { confirm } = useConfirmationContext();
125
- const { data: screenDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'screens'), TEMPLATE_CEILING), [
126
- firestore,
127
- hostId
128
- ], {
98
+ const gridFilter = useListGridFilter();
99
+ const { rows: emailScreens, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
100
+ collection: collection(firestore, 'hosts', hostId, 'screens'),
101
+ declaration: EMAIL_TEMPLATE_QUERY,
102
+ request: {
103
+ clauses: gridFilter.clauses,
104
+ search: gridFilter.searchWords,
105
+ base: EMAIL_TEMPLATE_BASE
106
+ },
107
+ deps: [
108
+ firestore,
109
+ hostId
110
+ ],
129
111
  idField: '$id'
130
112
  });
131
- const { rows: readScreens, truncated } = ceilingedWindow(screenDocs, TEMPLATE_CEILING);
132
- const emailScreens = useMemo(()=>[
133
- ...readScreens
134
- ].filter((screen)=>!screen.deletedAt && screen.kind === 'email').sort((a, b)=>{
135
- var _a_displayName, _b_displayName;
136
- return String((_a_displayName = a.displayName) != null ? _a_displayName : '').localeCompare(String((_b_displayName = b.displayName) != null ? _b_displayName : ''));
137
- }).map((screen)=>{
138
- var _screen_displayName;
139
- return _extends({}, screen, {
140
- templateName: String((_screen_displayName = screen.displayName) != null ? _screen_displayName : 'Untitled template'),
141
- originKey: templateProvenance(screen).origin === 'installed' ? 'installed' : 'local'
142
- });
113
+ const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
114
+ const refusals = useMemo(()=>listQueryRefusals(plan.refused, {
115
+ fields: EMAIL_TEMPLATE_QUERY.fields,
116
+ headers: EMAIL_TEMPLATE_FILTER_HEADERS
143
117
  }), [
144
- readScreens
118
+ plan.refused
145
119
  ]);
146
- const templateFilter = useListRowsFilter({
147
- rows: emailScreens,
148
- fields: TEMPLATE_FILTER_FIELDS,
149
- options: TEMPLATE_FILTER_OPTIONS,
150
- headers: TEMPLATE_FILTER_HEADERS,
151
- search: TEMPLATE_SEARCH_FIELDS
152
- });
153
120
  const handleCreate = async ()=>{
154
121
  try {
155
122
  const { screenId, versionId } = await createEmailScreen(hostId, createHostResource, createHostVersion);
@@ -175,9 +142,7 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
175
142
  }
176
143
  }).then(()=>true).catch(()=>false);
177
144
  if (!confirmed) return;
178
- await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {
179
- deletedAt: Timestamp.now()
180
- });
145
+ await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screen.$id), emailTemplateSoftDelete());
181
146
  };
182
147
  const templateHref = (screen)=>`${basePath}/templates/${screen.$id}`;
183
148
  const templateName = (screen)=>{
@@ -234,8 +199,7 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
234
199
  }
235
200
  ];
236
201
  /*
237
- * The templates are a window this card already holds, so the grid pages
238
- * it. The name is a link AND the row opens the template.
202
+ * The name is a link AND the row opens the template.
239
203
  */ const columns = [
240
204
  {
241
205
  field: 'displayName',
@@ -259,7 +223,7 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
259
223
  */ field: 'origin',
260
224
  headerName: 'Origin',
261
225
  width: 140,
262
- valueGetter: (_value, row)=>row.originKey,
226
+ valueGetter: (_value, row)=>templateProvenance(row).origin === 'installed' ? 'installed' : 'local',
263
227
  renderCell: ({ value })=>value === 'installed' ? /*#__PURE__*/ _jsx(Chip, {
264
228
  size: "small",
265
229
  label: "Installed"
@@ -293,32 +257,51 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
293
257
  },
294
258
  children: [
295
259
  duplicate.dialog,
296
- /*#__PURE__*/ _jsxs(Stack, {
260
+ /*#__PURE__*/ _jsx(Stack, {
297
261
  spacing: 1.5,
298
- children: [
299
- emailScreens.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
300
- variant: "body2",
301
- color: "text.secondary",
302
- children: 'Design a reusable email here, then send it from a campaign. A ' + 'new template opens in the besigner with email-safe components ' + 'only.'
303
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
304
- children: [
305
- /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, templateFilter.chipsProps)),
306
- /*#__PURE__*/ _jsx(ListTable, _extends({
307
- "aria-label": "Email templates",
308
- rows: templateFilter.rows,
309
- columns: templateFilter.filterColumns(columns),
310
- rowHeight: TABLE_ROW_HEIGHT,
311
- onOpen: (_id, row)=>router.push(templateHref(row))
312
- }, templateFilter.gridProps, {
313
- noRowsLabel: "No templates match these filters"
314
- }))
315
- ]
316
- }),
317
- truncated ? /*#__PURE__*/ _jsx(Alert, {
318
- severity: "info",
319
- children: `This site holds more than ${TEMPLATE_CEILING} screens, and the ` + 'templates listed are drawn from the first of them. Any beyond ' + 'that are not in this list.'
320
- }) : null
321
- ]
262
+ children: emailScreens.length === 0 && !hasMore && !filtering ? /*#__PURE__*/ _jsx(Typography, {
263
+ variant: "body2",
264
+ color: "text.secondary",
265
+ children: 'Design a reusable email here, then send it from a campaign. A ' + 'new template opens in the besigner with email-safe components ' + 'only.'
266
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
267
+ children: [
268
+ /*#__PURE__*/ _jsx(ListFilterChips, {
269
+ fields: EMAIL_TEMPLATE_QUERY.fields,
270
+ headers: EMAIL_TEMPLATE_FILTER_HEADERS,
271
+ clauses: gridFilter.clauses,
272
+ onChange: gridFilter.setClauses
273
+ }),
274
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
275
+ refused: refusals,
276
+ notices: plan.notices
277
+ }),
278
+ /*#__PURE__*/ _jsx(ListTable, {
279
+ "aria-label": "Email templates",
280
+ rows: emailScreens,
281
+ columns: listFilterGridColumns(columns, EMAIL_TEMPLATE_QUERY.fields, {}, EMAIL_TEMPLATE_FILTER_HEADERS),
282
+ rowHeight: TABLE_ROW_HEIGHT,
283
+ onOpen: (_id, row)=>router.push(templateHref(row)),
284
+ // Paged by the footer below, so the grid must not also slice.
285
+ hideFooter: true,
286
+ // The panel and the search go to the query; the grid neither
287
+ // filters nor sorts the page it holds.
288
+ filterMode: "server",
289
+ filterModel: gridFilter.filterModel,
290
+ onFilterModelChange: gridFilter.onFilterModelChange,
291
+ quickFilter: true,
292
+ disableColumnSorting: true,
293
+ noRowsLabel: "No templates match these filters"
294
+ }),
295
+ /*#__PURE__*/ _jsx(ListPagination, {
296
+ page: page,
297
+ pageSize: pageSize,
298
+ rowCount: emailScreens.length,
299
+ hasMore: hasMore,
300
+ onPageChange: setPage,
301
+ onPageSizeChange: setPageSize
302
+ })
303
+ ]
304
+ })
322
305
  })
323
306
  ]
324
307
  });
@@ -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 {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, Timestamp, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\n\n/** How many of the site's screens one read of this list covers. */\nconst TEMPLATE_CEILING = 200\n\n/*\n * What the templates grid's Filters panel offers (AGL-3317). The card holds\n * the templates its capped window read, so it answers the panel over all of\n * them; the notice under the table says when the cap truncated the window.\n * Origin reads `originKey`, the provenance the column draws.\n */\nconst TEMPLATE_FILTER_FIELDS = [\n inMemoryListField('displayName', 'text', 'templateName'),\n inMemoryListField('origin', 'select', 'originKey'),\n]\nconst TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Template',\n origin: 'Origin',\n}\nconst TEMPLATE_FILTER_OPTIONS = {\n origin: [\n { value: 'local', label: 'Yours' },\n { value: 'installed', label: 'Installed' },\n ],\n}\nconst TEMPLATE_SEARCH_FIELDS = ['templateName'] as const\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 * ## Why the read is a CEILING with a probe\n *\n * `screens` holds every kind of screen the site has and the email ones are\n * picked out here, so the cap cannot be a page: a page of the collection is\n * not a page of this list. `collectionCeiling` orders on the document name,\n * which is the one ordering that cannot drop a screen — `orderBy('displayName')`\n * matches only documents that HAVE the field, so a screen created without a\n * name would vanish rather than sort oddly. It reads one past the ceiling so\n * the card can SAY when the site holds more than it drew, and the footer under\n * the table pages the window the card already has.\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 { data: screenDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'screens'),\n TEMPLATE_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { rows: readScreens, truncated } = ceilingedWindow<any>(\n screenDocs,\n TEMPLATE_CEILING,\n )\n const emailScreens = useMemo(\n () =>\n [...readScreens]\n .filter((screen: any) => !screen.deletedAt && screen.kind === 'email')\n .sort((a: any, b: any) =>\n String(a.displayName ?? '').localeCompare(\n String(b.displayName ?? ''),\n ),\n )\n .map((screen: any) => ({\n ...screen,\n templateName: String(screen.displayName ?? 'Untitled template'),\n originKey:\n templateProvenance(screen).origin === 'installed' ? 'installed' : 'local',\n })),\n [readScreens],\n )\n const templateFilter = useListRowsFilter<any>({\n rows: emailScreens,\n fields: TEMPLATE_FILTER_FIELDS,\n options: TEMPLATE_FILTER_OPTIONS,\n headers: TEMPLATE_FILTER_HEADERS,\n search: TEMPLATE_SEARCH_FIELDS,\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(doc(firestore, 'hosts', hostId, 'screens', screen.$id), {\n deletedAt: Timestamp.now(),\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 templates are a window this card already holds, so the grid pages\n * it. 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) => row.originKey,\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n 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 ? (\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 {...templateFilter.chipsProps} />\n <ListTable\n aria-label=\"Email templates\"\n rows={templateFilter.rows}\n columns={templateFilter.filterColumns(columns)}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // The panel and the search are the grid's; the card answers\n // them over every template its window read.\n {...templateFilter.gridProps}\n noRowsLabel=\"No templates match these filters\"\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`This site holds more than ${TEMPLATE_CEILING} screens, and the ` +\n 'templates listed are drawn from the first of them. Any beyond ' +\n 'that are not in this list.'}\n </Alert>\n ) : null}\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","ListRowActions","ListTable","listActionsColumn","inMemoryListField","TABLE_ROW_HEIGHT","useListRowsFilter","useSnackbar","ceilingedWindow","collectionCeiling","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Stack","Typography","collection","doc","Timestamp","updateDoc","useRouter","useMemo","templateProvenance","createEmailScreen","TEMPLATE_CEILING","TEMPLATE_FILTER_FIELDS","TEMPLATE_FILTER_HEADERS","displayName","origin","TEMPLATE_FILTER_OPTIONS","value","label","TEMPLATE_SEARCH_FIELDS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","basePath","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","data","screenDocs","idField","rows","readScreens","truncated","emailScreens","filter","screen","deletedAt","kind","sort","a","b","String","localeCompare","map","templateName","originKey","templateFilter","fields","options","headers","search","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","rowActions","key","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","id","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","dialog","spacing","length","chipsProps","aria-label","filterColumns","rowHeight","onOpen","_id","gridProps","noRowsLabel","severity"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,UAAU,EAAEC,cAAc,EAAEC,KAAK,QAAQ,eAAc;AAChE,SACEC,gBAAgB,EAChBC,cAAc,EACdC,aAAa,EACbC,gBAAgB,QACX,yBAAwB;AAC/B,SACEC,OAAO,EACPC,WAAW,EACXC,OAAO,EACPC,sBAAsB,QACjB,uBAAsB;AAC7B,OAAOC,qBAAqB,8DAA6D;AACzF,SACEC,cAAc,EACdC,SAAS,EACTC,iBAAiB,QACZ,uDAAsD;AAE7D,SAASC,iBAAiB,QAAQ,8CAA6C;AAC/E,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,eAAe,EACfC,iBAAiB,QACZ,+DAA8D;AACrE,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,oBAAoB,EACpBC,YAAY,EACZC,sBAAsB,EACtBC,kBAAkB,EAClBC,iBAAiB,QACZ,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,UAAU,QACL,gBAAe;AAEtB,SAASC,UAAU,EAAEC,GAAG,EAAEC,SAAS,EAAEC,SAAS,QAAQ,qBAAoB;AAC1E,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,kBAAkB,QAAQ,kCAA8B;AACjE,SAASC,iBAAiB,QAAQ,kCAA8B;AAEhE,iEAAiE,GACjE,MAAMC,mBAAmB;AAEzB;;;;;CAKC,GACD,MAAMC,yBAAyB;IAC7B3B,kBAAkB,eAAe,QAAQ;IACzCA,kBAAkB,UAAU,UAAU;CACvC;AACD,MAAM4B,0BAA4D;IAChEC,aAAa;IACbC,QAAQ;AACV;AACA,MAAMC,0BAA0B;IAC9BD,QAAQ;QACN;YAAEE,OAAO;YAASC,OAAO;QAAQ;QACjC;YAAED,OAAO;YAAaC,OAAO;QAAY;KAC1C;AACH;AACA,MAAMC,yBAAyB;IAAC;CAAe;AAE/C,qEAAqE;AACrE,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,MAAMC,eAAe,CACnBC,SACAC,MACAC,UACAC,YAEAtD,WAAWE,MAAMqD,eAAe,EAAE;QAAEJ;QAASC;QAAMC;QAAUC;IAAU;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,SAASE,iBAAiBC,KAIhC;IACC,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,MAAM,EAAEN,OAAO,EAAES,SAAS,EAAE,GAAGtC,oBAAoBoC;IACnD,MAAMG,YAAYrC;IAClB,MAAMsC,qBAAqBpC;IAC3B,MAAMqC,oBAAoBpC;IAC1B,MAAMqC,SAAS3B;IACf,MAAM,EAAE4B,eAAe,EAAE,GAAG/C;IAC5B,yEAAyE;IACzE,6BAA6B;IAC7B,MAAMgD,YAAY3C,qBAAqB;QACrCmC;QACAS,cAAc,CAACC,OAAOC,OACpBJ,gBAAgB,CAAC,eAAe,EAAEI,KAAKC,IAAI,CAAC,CAAC,CAAC,EAAE;gBAAEC,SAAS;YAAU;IACzE;IACA,MAAM,EAAEC,OAAO,EAAE,GAAG9D;IAEpB,MAAM,EAAE+D,MAAMC,UAAU,EAAE,GAAGjD,uBAC3B,IACEL,kBACEa,WAAW4B,WAAW,SAASH,QAAQ,YACvCjB,mBAEJ;QAACoB;QAAWH;KAAO,EACnB;QAAEiB,SAAS;IAAM;IAEnB,MAAM,EAAEC,MAAMC,WAAW,EAAEC,SAAS,EAAE,GAAG3D,gBACvCuD,YACAjC;IAEF,MAAMsC,eAAezC,QACnB,IACE;eAAIuC;SAAY,CACbG,MAAM,CAAC,CAACC,SAAgB,CAACA,OAAOC,SAAS,IAAID,OAAOE,IAAI,KAAK,SAC7DC,IAAI,CAAC,CAACC,GAAQC;gBACND,gBACEC;mBADTC,QAAOF,iBAAAA,EAAEzC,WAAW,YAAbyC,iBAAiB,IAAIG,aAAa,CACvCD,QAAOD,iBAAAA,EAAE1C,WAAW,YAAb0C,iBAAiB;WAG3BG,GAAG,CAAC,CAACR;gBAEiBA;mBAFA,aAClBA;gBACHS,cAAcH,QAAON,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB;gBAC3CU,WACEpD,mBAAmB0C,QAAQpC,MAAM,KAAK,cAAc,cAAc;;YAE1E;QAACgC;KAAY;IAEf,MAAMe,iBAAiB3E,kBAAuB;QAC5C2D,MAAMG;QACNc,QAAQnD;QACRoD,SAAShD;QACTiD,SAASpD;QACTqD,QAAQ/C;IACV;IAEA,MAAMgD,eAAe;QACnB,IAAI;YACF,MAAM,EAAE5C,QAAQ,EAAEC,SAAS,EAAE,GAAG,MAAMd,kBACpCkB,QACAI,oBACAC;YAEF,IAAIZ,WAAWS,WAAW;gBACxB,KAAKI,OAAOkC,IAAI,CAAChD,aAAaC,SAASS,WAAWP,UAAUC;YAC9D;QACF,EAAE,OAAO6C,OAAY;;YACnBC,QAAQD,KAAK,CAACA;YACdlC,wBAAgBkC,yBAAAA,MAAOE,OAAO,mBAAI,gCAAgC;gBAChE9B,SAAS;YACX;QACF;IACF;IAEA,MAAM+B,eAAe,OAAOrB;YAIlBA;QAHR,MAAMsB,YAAY,MAAM/B,QAAQ;YAC9BgC,OAAO;YACPC,aACE,CAAC,CAAC,GAAExB,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB,oBAAoB,mBAAmB,CAAC,GAClE;YACFyB,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,WAAW;QAChB,MAAMnE,UAAUF,IAAI2B,WAAW,SAASH,QAAQ,WAAWuB,OAAO8B,GAAG,GAAG;YACtE7B,WAAW/C,UAAU6E,GAAG;QAC1B;IACF;IAEA,MAAMC,eAAe,CAAChC,SAAgB,GAAGtB,SAAS,WAAW,EAAEsB,OAAO8B,GAAG,EAAE;IAE3E,MAAMrB,eAAe,CAACT;YACbA;eAAPM,QAAON,sBAAAA,OAAOrC,WAAW,YAAlBqC,sBAAsB;;IAE/B,MAAMiC,aAAa,CAACjC,SAAsC;YACxD;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAMhH,cAAcgH,IAAI;oBAAEC,MAAM;;gBAC/CC,MAAMN,aAAahC;YACrB;YACA;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAM/G,iBAAiB+G,IAAI;oBAAEC,MAAM;;gBAClD;;;;;OAKC,GACDC,MACEpE,WAAWS,YACPV,aAAaC,SAASS,WAAWqB,OAAO8B,GAAG,EAAE9B,OAAO3B,SAAS,IAC7DkE;gBACNC,UAAU,CAACtE,WAAW,CAACS;gBACvB8D,gBAAgB;YAClB;YACA;gBACEP,KAAK;gBACLnE,OAAO3B;gBACP+F,oBAAM,KAAC3G;oBAAQ4G,MAAMjH,eAAeiH,IAAI;oBAAEC,MAAM;;gBAChDK,SAAS,IACPzD,UAAU0D,OAAO,CAAC,eAAe;wBAC/BC,IAAI5C,OAAO8B,GAAG;wBACdzC,MAAMoB,aAAaT;oBACrB;YACJ;YACA;gBACEkC,KAAK;gBACLnE,OAAO;gBACPoE,oBAAM,KAAC3G;oBAAQ4G,MAAMlH,iBAAiBkH,IAAI;oBAAEC,MAAM;;gBAClDQ,aAAa;gBACbH,SAAS,IAAM,KAAKrB,aAAarB;YACnC;SACD;IAED;;;GAGC,GACD,MAAM8C,UAAwB;QAC5B;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,aAAa,CAACC,QAAQC,MAAQ5C,aAAa4C;YAC3CC,YAAY,CAAC,EAAED,GAAG,EAAEvF,KAAK,EAAE,iBACzB,KAACxC;oBACCgH,MAAMN,aAAaqB;oBACnB,+DAA+D;oBAC/D,4CAA4C;oBAC5CX,SAAS,CAACa,QACRA,MAAMC,eAAe;8BAGtB1F;;QAGP;QACA;YACE;;;;OAIC,GACDiF,OAAO;YACPC,YAAY;YACZS,OAAO;YACPN,aAAa,CAACC,QAAQC,MAAQA,IAAI3C,SAAS;YAC3C4C,YAAY,CAAC,EAAExF,KAAK,EAAE,GACpBA,UAAU,4BACR,KAACjB;oBAAKwF,MAAK;oBAAQtE,OAAM;mCAEzB,KAAChB;oBAAWuC,SAAQ;oBAAQqC,OAAM;8BAC/B;;QAGT;QACA9F,kBACE,CAACwH,oBACC,KAAC1H;gBAAeoC,OAAO0C,aAAa4C;gBAAMK,OAAOzB,WAAWoB;gBAE9D;YAAEI,OAAO;QAAG;KAEf;IAED,qBACE,MAAClI;QACCoI,QAAQ;QACRC,MAAM5I,eAAe,kBAAkB;YAAE6I,QAAQ;QAAmB;QACpEC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACrH;gBACCyF,MAAK;gBACL/C,SAAQ;gBACRoD,SAAS,IAAM,KAAK1B;0BAEnB;;QAGP;;YAEC/B,UAAUiF,MAAM;0BACjB,MAACpH;gBAAMqH,SAAS;;oBACbrE,aAAasE,MAAM,KAAK,kBACvB,KAACrH;wBAAWuC,SAAQ;wBAAQqC,OAAM;kCAC/B,mEACC,mEACA;uCAGJ;;0CACE,KAACjG,8BAAoBiF,eAAe0D,UAAU;0CAC9C,KAACzI;gCACC0I,cAAW;gCACX3E,MAAMgB,eAAehB,IAAI;gCACzBmD,SAASnC,eAAe4D,aAAa,CAACzB;gCACtC0B,WAAWzI;gCACX0I,QAAQ,CAACC,KAAKrB,MAAQtE,OAAOkC,IAAI,CAACe,aAAaqB;+BAG3C1C,eAAegE,SAAS;gCAC5BC,aAAY;;;;oBAIjB/E,0BACC,KAAClD;wBAAMkI,UAAS;kCACb,CAAC,0BAA0B,EAAErH,iBAAiB,kBAAkB,CAAC,GAChE,mEACA;yBAEF;;;;;AAIZ;AACAe,iBAAiBZ,WAAW,GAAG;AAE/B,eAAeY,iBAAgB"}
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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"}
@@ -17,6 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { buildRoute, PageHeaderRecord, pluginDocsHelp, Route } from "@aglyn/aglyn";
20
+ import { artifactRenameListKeys } from "@aglyn/aglyn/app-utils/artifact-list-keys";
20
21
  import { ICON_VARIANT_BESIGNER } from "@aglyn/shared-data-enums";
21
22
  import { mdiBullhornOutline, mdiEyeOutline } from "@aglyn/shared-data-mdi";
22
23
  import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
@@ -115,9 +116,9 @@ const templateDocsHelp = pluginDocsHelp('designedEmails', {
115
116
  if (!next || renaming) return;
116
117
  setRenaming(true);
117
118
  try {
118
- await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {
119
+ await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), _extends({
119
120
  displayName: next
120
- });
121
+ }, artifactRenameListKeys('screens', next)));
121
122
  setNameDraft(null);
122
123
  enqueueSnackbar('Design renamed', {
123
124
  variant: 'success',