@aglyn/plugins-email 1.0.0-beta.189 → 1.0.0-beta.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-email",
3
- "version": "1.0.0-beta.189",
3
+ "version": "1.0.0-beta.191",
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.189",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.189",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.189",
31
- "@aglyn/shared-data-types": "1.0.0-beta.189",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.189",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.189",
34
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.189",
35
- "@aglyn/shared-ui-next": "1.0.0-beta.189",
36
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.189",
37
- "@aglyn/shared-ui-theme": "1.0.0-beta.189",
38
- "@aglyn/shared-util-email": "1.0.0-beta.189",
39
- "@aglyn/shared-util-http": "1.0.0-beta.189",
40
- "@aglyn/shared-util-timestamp": "1.0.0-beta.189",
41
- "@aglyn/shared-util-tools": "1.0.0-beta.189",
42
- "@aglyn/tenant-data-admin": "1.0.0-beta.189",
43
- "@aglyn/tenant-feature-instance": "1.0.0-beta.189",
28
+ "@aglyn/aglyn": "1.0.0-beta.191",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.191",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.191",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.191",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.191",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.191",
34
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.191",
35
+ "@aglyn/shared-ui-next": "1.0.0-beta.191",
36
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.191",
37
+ "@aglyn/shared-ui-theme": "1.0.0-beta.191",
38
+ "@aglyn/shared-util-email": "1.0.0-beta.191",
39
+ "@aglyn/shared-util-http": "1.0.0-beta.191",
40
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.191",
41
+ "@aglyn/shared-util-tools": "1.0.0-beta.191",
42
+ "@aglyn/tenant-data-admin": "1.0.0-beta.191",
43
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.191",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Which permission the signed-in member lacks for this switch — its label, or
3
+ * `null` when they hold both — and whether that is known yet.
4
+ *
5
+ * Read off the member's own membership document, and their custom role when
6
+ * they hold one, both of which the rules let a member read. The shell's
7
+ * permission map carries the legacy keys and the plugin-declared ones, not
8
+ * these two, and the CRM's org settings cards answer the same question the
9
+ * same way. `ready` separates "no" from "not yet", so the switch disables
10
+ * with a reason instead of hiding until the read lands. A membership that
11
+ * cannot be read resolves no permission at all; the route decides either way.
12
+ */
13
+ export declare function useConsentGroupConfirmationAccess(orgId: string | undefined): {
14
+ missing: string | null;
15
+ ready: boolean;
16
+ };
17
+ export interface ConsentGroupConfirmationCardProps {
18
+ /** The org document the shell passes, kept live by its listener. */
19
+ org?: Record<string, unknown> | null;
20
+ }
21
+ /**
22
+ * "Wait for confirmation across a consent group" — the org's one switch over
23
+ * whether a confirmation click one site asked for holds the rest of the
24
+ * group's mail (AGL-3316).
25
+ *
26
+ * ## What it decides
27
+ *
28
+ * A site that asks new subscribers to confirm holds its own mail on that
29
+ * topic until they click. Off — the default — the other sites of a declared
30
+ * consent group go on mailing them. On, every site of the group waits for the
31
+ * click, on every send path that already reads a sibling's unsubscribe. A site
32
+ * in no group has nobody to wait for, so an org that declared none changes
33
+ * nothing by turning it on, and the card says so.
34
+ *
35
+ * ## Written through the route, read from the org document
36
+ *
37
+ * The field is server-owned — the rules deny it, and `consentGroups`, to every
38
+ * client — so the switch posts `set-consent-group-confirmation` and never
39
+ * writes Firestore. The shell's org listener delivers the stored value back,
40
+ * which is what the switch shows; a local copy holds the click only until
41
+ * then, and snaps back when the route refuses.
42
+ */
43
+ export declare function ConsentGroupConfirmationCard(props: ConsentGroupConfirmationCardProps): import("react").JSX.Element;
44
+ export declare namespace ConsentGroupConfirmationCard {
45
+ var displayName: string;
46
+ }
47
+ export default ConsentGroupConfirmationCard;
@@ -0,0 +1,188 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { consentGroupsAwaitConfirmation, orgPermissionLabel, pluginDocsHelp, readConsentGroups, resolveOrgPermissions } from "@aglyn/aglyn";
19
+ import { CardDisplay } from "@aglyn/shared-ui-jsx";
20
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
21
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
22
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
23
+ import { FormControlLabel, FormHelperText, Stack, Switch, Typography } from "@mui/material";
24
+ import { doc } from "firebase/firestore";
25
+ import { useEffect, useState } from "react";
26
+ import { useEmailOrgMount } from "./email-org-mount.js";
27
+ /**
28
+ * What `/api/orgs/settings` asks of a member for this switch: the route's own
29
+ * gate, and the permission that opens this console.
30
+ */ const REQUIRED_PERMISSIONS = [
31
+ 'org.settings',
32
+ 'data.manage'
33
+ ];
34
+ /**
35
+ * Which permission the signed-in member lacks for this switch — its label, or
36
+ * `null` when they hold both — and whether that is known yet.
37
+ *
38
+ * Read off the member's own membership document, and their custom role when
39
+ * they hold one, both of which the rules let a member read. The shell's
40
+ * permission map carries the legacy keys and the plugin-declared ones, not
41
+ * these two, and the CRM's org settings cards answer the same question the
42
+ * same way. `ready` separates "no" from "not yet", so the switch disables
43
+ * with a reason instead of hiding until the read lands. A membership that
44
+ * cannot be read resolves no permission at all; the route decides either way.
45
+ */ export function useConsentGroupConfirmationAccess(orgId) {
46
+ var _ref, _roleRead_data;
47
+ const firestore = useFirestore();
48
+ const { data: user } = useUser();
49
+ const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : '';
50
+ const memberRead = useFirestoreDoc(()=>orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null, [
51
+ firestore,
52
+ orgId,
53
+ uid
54
+ ]);
55
+ const member = memberRead.status === 'success' ? memberRead.data : undefined;
56
+ const roleId = typeof (member == null ? void 0 : member.roleId) === 'string' ? member.roleId : '';
57
+ const roleRead = useFirestoreDoc(()=>orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null, [
58
+ firestore,
59
+ orgId,
60
+ roleId
61
+ ]);
62
+ // A reference that is never built stays `loading`, so a member with no
63
+ // custom role has nothing left to wait for.
64
+ const ready = Boolean(orgId && uid) && memberRead.status !== 'loading' && (!roleId || roleRead.status !== 'loading');
65
+ const granted = resolveOrgPermissions(member != null ? member : null, roleId && roleRead.status === 'success' ? (_roleRead_data = roleRead.data) != null ? _roleRead_data : null : null);
66
+ const lacking = REQUIRED_PERMISSIONS.find((key)=>granted[key] !== true);
67
+ return {
68
+ missing: lacking ? orgPermissionLabel(lacking) : null,
69
+ ready
70
+ };
71
+ }
72
+ /**
73
+ * "Wait for confirmation across a consent group" — the org's one switch over
74
+ * whether a confirmation click one site asked for holds the rest of the
75
+ * group's mail (AGL-3316).
76
+ *
77
+ * ## What it decides
78
+ *
79
+ * A site that asks new subscribers to confirm holds its own mail on that
80
+ * topic until they click. Off — the default — the other sites of a declared
81
+ * consent group go on mailing them. On, every site of the group waits for the
82
+ * click, on every send path that already reads a sibling's unsubscribe. A site
83
+ * in no group has nobody to wait for, so an org that declared none changes
84
+ * nothing by turning it on, and the card says so.
85
+ *
86
+ * ## Written through the route, read from the org document
87
+ *
88
+ * The field is server-owned — the rules deny it, and `consentGroups`, to every
89
+ * client — so the switch posts `set-consent-group-confirmation` and never
90
+ * writes Firestore. The shell's org listener delivers the stored value back,
91
+ * which is what the switch shows; a local copy holds the click only until
92
+ * then, and snaps back when the route refuses.
93
+ */ export function ConsentGroupConfirmationCard(props) {
94
+ var _useEmailOrgMount;
95
+ const { org } = props;
96
+ const orgId = (_useEmailOrgMount = useEmailOrgMount()) == null ? void 0 : _useEmailOrgMount.orgId;
97
+ const { data: user } = useUser();
98
+ const { enqueueSnackbar } = useSnackbar();
99
+ const { missing, ready } = useConsentGroupConfirmationAccess(orgId);
100
+ const stored = consentGroupsAwaitConfirmation(org);
101
+ const declared = Object.keys(readConsentGroups(org)).length > 0;
102
+ const [checked, setChecked] = useState(stored);
103
+ const [busy, setBusy] = useState(false);
104
+ // The stored value wins whenever it changes: the click is optimistic, and
105
+ // the org document is what every send path reads.
106
+ useEffect(()=>setChecked(stored), [
107
+ stored
108
+ ]);
109
+ const handleChange = async (next)=>{
110
+ // Disabled for a member who may not move it; guarded anyway, because a
111
+ // disabled control can still deliver a change event and the route would
112
+ // refuse it.
113
+ if (!orgId || !user || !ready || missing || busy) return;
114
+ setChecked(next);
115
+ setBusy(true);
116
+ try {
117
+ const response = await authorizedFetch(user, '/api/orgs/settings', {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json'
121
+ },
122
+ body: JSON.stringify({
123
+ orgId,
124
+ action: 'set-consent-group-confirmation',
125
+ awaitConfirmation: next
126
+ })
127
+ });
128
+ const result = await response.json().catch(()=>({}));
129
+ if (!response.ok) {
130
+ var _ref;
131
+ throw new Error((_ref = result == null ? void 0 : result.error) != null ? _ref : 'The setting could not be saved');
132
+ }
133
+ enqueueSnackbar(next ? 'The sites in a consent group now wait for the confirmation click' : 'The sites in a consent group no longer wait for each other', {
134
+ variant: 'success',
135
+ persist: false
136
+ });
137
+ } catch (error) {
138
+ var _ref1;
139
+ console.error(error);
140
+ setChecked(stored);
141
+ enqueueSnackbar((_ref1 = error == null ? void 0 : error.message) != null ? _ref1 : 'The setting could not be saved', {
142
+ variant: 'error',
143
+ allowDuplicate: true
144
+ });
145
+ } finally{
146
+ setBusy(false);
147
+ }
148
+ };
149
+ return /*#__PURE__*/ _jsx(CardDisplay, {
150
+ header: 'Confirmation across consent groups',
151
+ help: pluginDocsHelp('emailCampaigns', {
152
+ anchor: '#consent-group-confirmation'
153
+ }),
154
+ contentGutterX: true,
155
+ contentGutterY: true,
156
+ children: /*#__PURE__*/ _jsxs(Stack, {
157
+ spacing: 1,
158
+ children: [
159
+ /*#__PURE__*/ _jsx(FormControlLabel, {
160
+ control: /*#__PURE__*/ _jsx(Switch, {
161
+ size: "small",
162
+ checked: checked,
163
+ disabled: !ready || !!missing || busy || !orgId,
164
+ onChange: (event)=>void handleChange(event.target.checked)
165
+ }),
166
+ label: "Wait for confirmation across a consent group"
167
+ }),
168
+ /*#__PURE__*/ _jsx(FormHelperText, {
169
+ children: 'When one site in a consent group asks someone to confirm by ' + 'email, the group’s other sites wait for the click too, and send ' + 'them nothing on that topic until they confirm. This applies only ' + 'to sites your organization has declared one sender. It is off ' + 'unless you turn it on.'
170
+ }),
171
+ !declared ? /*#__PURE__*/ _jsx(Typography, {
172
+ variant: "caption",
173
+ color: "text.secondary",
174
+ children: 'Your organization has not declared a consent group, so this ' + 'changes nothing until it does.'
175
+ }) : null,
176
+ ready && missing ? /*#__PURE__*/ _jsx(Typography, {
177
+ variant: "caption",
178
+ color: "text.secondary",
179
+ children: `You need the ${missing} permission to change this.`
180
+ }) : null
181
+ ]
182
+ })
183
+ });
184
+ }
185
+ ConsentGroupConfirmationCard.displayName = 'ConsentGroupConfirmationCard';
186
+ export default ConsentGroupConfirmationCard;
187
+
188
+ //# sourceMappingURL=consent-group-confirmation-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-confirmation-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 {\n type AglynOrgCustomRole,\n consentGroupsAwaitConfirmation,\n type OrgPermission,\n orgPermissionLabel,\n pluginDocsHelp,\n readConsentGroups,\n resolveOrgPermissions,\n} from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n FormControlLabel,\n FormHelperText,\n Stack,\n Switch,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useEmailOrgMount } from './email-org-mount'\n\n/**\n * What `/api/orgs/settings` asks of a member for this switch: the route's own\n * gate, and the permission that opens this console.\n */\nconst REQUIRED_PERMISSIONS: readonly OrgPermission[] = [\n 'org.settings',\n 'data.manage',\n]\n\n/** A membership document, as the permission resolver reads one. */\ntype MemberRecord = NonNullable<Parameters<typeof resolveOrgPermissions>[0]>\n\n/**\n * Which permission the signed-in member lacks for this switch — its label, or\n * `null` when they hold both — and whether that is known yet.\n *\n * Read off the member's own membership document, and their custom role when\n * they hold one, both of which the rules let a member read. The shell's\n * permission map carries the legacy keys and the plugin-declared ones, not\n * these two, and the CRM's org settings cards answer the same question the\n * same way. `ready` separates \"no\" from \"not yet\", so the switch disables\n * with a reason instead of hiding until the read lands. A membership that\n * cannot be read resolves no permission at all; the route decides either way.\n */\nexport function useConsentGroupConfirmationAccess(orgId: string | undefined): {\n missing: string | null\n ready: boolean\n} {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const memberRead = useFirestoreDoc<MemberRecord>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n const member = memberRead.status === 'success' ? memberRead.data : undefined\n const roleId = typeof member?.roleId === 'string' ? member.roleId : ''\n const roleRead = useFirestoreDoc<AglynOrgCustomRole>(\n () => (orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null),\n [firestore, orgId, roleId],\n )\n // A reference that is never built stays `loading`, so a member with no\n // custom role has nothing left to wait for.\n const ready =\n Boolean(orgId && uid) &&\n memberRead.status !== 'loading' &&\n (!roleId || roleRead.status !== 'loading')\n const granted = resolveOrgPermissions(\n member ?? null,\n roleId && roleRead.status === 'success' ? (roleRead.data ?? null) : null,\n )\n const lacking = REQUIRED_PERMISSIONS.find((key) => granted[key] !== true)\n return { missing: lacking ? orgPermissionLabel(lacking) : null, ready }\n}\n\nexport interface ConsentGroupConfirmationCardProps {\n /** The org document the shell passes, kept live by its listener. */\n org?: Record<string, unknown> | null\n}\n\n/**\n * \"Wait for confirmation across a consent group\" — the org's one switch over\n * whether a confirmation click one site asked for holds the rest of the\n * group's mail (AGL-3316).\n *\n * ## What it decides\n *\n * A site that asks new subscribers to confirm holds its own mail on that\n * topic until they click. Off — the default — the other sites of a declared\n * consent group go on mailing them. On, every site of the group waits for the\n * click, on every send path that already reads a sibling's unsubscribe. A site\n * in no group has nobody to wait for, so an org that declared none changes\n * nothing by turning it on, and the card says so.\n *\n * ## Written through the route, read from the org document\n *\n * The field is server-owned — the rules deny it, and `consentGroups`, to every\n * client — so the switch posts `set-consent-group-confirmation` and never\n * writes Firestore. The shell's org listener delivers the stored value back,\n * which is what the switch shows; a local copy holds the click only until\n * then, and snaps back when the route refuses.\n */\nexport function ConsentGroupConfirmationCard(\n props: ConsentGroupConfirmationCardProps,\n) {\n const { org } = props\n const orgId = useEmailOrgMount()?.orgId\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { missing, ready } = useConsentGroupConfirmationAccess(orgId)\n\n const stored = consentGroupsAwaitConfirmation(org)\n const declared = Object.keys(readConsentGroups(org)).length > 0\n const [checked, setChecked] = useState(stored)\n const [busy, setBusy] = useState(false)\n // The stored value wins whenever it changes: the click is optimistic, and\n // the org document is what every send path reads.\n useEffect(() => setChecked(stored), [stored])\n\n const handleChange = async (next: boolean) => {\n // Disabled for a member who may not move it; guarded anyway, because a\n // disabled control can still deliver a change event and the route would\n // refuse it.\n if (!orgId || !user || !ready || missing || busy) return\n setChecked(next)\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/orgs/settings', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n action: 'set-consent-group-confirmation',\n awaitConfirmation: next,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) {\n throw new Error(result?.error ?? 'The setting could not be saved')\n }\n enqueueSnackbar(\n next\n ? 'The sites in a consent group now wait for the confirmation click'\n : 'The sites in a consent group no longer wait for each other',\n { variant: 'success', persist: false },\n )\n } catch (error: any) {\n console.error(error)\n setChecked(stored)\n enqueueSnackbar(error?.message ?? 'The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <CardDisplay\n header={'Confirmation across consent groups'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#consent-group-confirmation',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={checked}\n disabled={!ready || !!missing || busy || !orgId}\n onChange={(event) => void handleChange(event.target.checked)}\n />\n }\n label=\"Wait for confirmation across a consent group\"\n />\n <FormHelperText>\n {'When one site in a consent group asks someone to confirm by ' +\n 'email, the group’s other sites wait for the click too, and send ' +\n 'them nothing on that topic until they confirm. This applies only ' +\n 'to sites your organization has declared one sender. It is off ' +\n 'unless you turn it on.'}\n </FormHelperText>\n {!declared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Your organization has not declared a consent group, so this ' +\n 'changes nothing until it does.'}\n </Typography>\n ) : null}\n {ready && missing ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`You need the ${missing} permission to change this.`}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nConsentGroupConfirmationCard.displayName = 'ConsentGroupConfirmationCard'\n\nexport default ConsentGroupConfirmationCard\n"],"names":["consentGroupsAwaitConfirmation","orgPermissionLabel","pluginDocsHelp","readConsentGroups","resolveOrgPermissions","CardDisplay","useSnackbar","authorizedFetch","useFirestore","useFirestoreDoc","useUser","FormControlLabel","FormHelperText","Stack","Switch","Typography","doc","useEffect","useState","useEmailOrgMount","REQUIRED_PERMISSIONS","useConsentGroupConfirmationAccess","orgId","roleRead","firestore","data","user","uid","memberRead","member","status","undefined","roleId","ready","Boolean","granted","lacking","find","key","missing","ConsentGroupConfirmationCard","props","org","enqueueSnackbar","stored","declared","Object","keys","length","checked","setChecked","busy","setBusy","handleChange","next","response","method","headers","body","JSON","stringify","action","awaitConfirmation","result","json","catch","ok","Error","error","variant","persist","console","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","control","size","disabled","onChange","event","target","label","color","displayName"],"mappings":"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"}
@@ -14,12 +14,16 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
19
  import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
19
20
  import { mdiDeleteOutline, mdiContentCopy, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
20
21
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
21
23
  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";
22
25
  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";
23
27
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
28
  import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
25
29
  import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useFirestoreCollection, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
@@ -30,6 +34,34 @@ import { useMemo } from "react";
30
34
  import { templateProvenance } from "../model/template-provenance.js";
31
35
  import { createEmailScreen } from "../utils/create-email-screen.js";
32
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
+ ];
33
65
  // The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/
34
66
  // versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the
35
67
  // pre-AGL-621/622 shape — so every "Edit"/"Design" jump out of the Emails
@@ -102,9 +134,22 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
102
134
  ].filter((screen)=>!screen.deletedAt && screen.kind === 'email').sort((a, b)=>{
103
135
  var _a_displayName, _b_displayName;
104
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
+ });
105
143
  }), [
106
144
  readScreens
107
145
  ]);
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
+ });
108
153
  const handleCreate = async ()=>{
109
154
  try {
110
155
  const { screenId, versionId } = await createEmailScreen(hostId, createHostResource, createHostVersion);
@@ -214,8 +259,8 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
214
259
  */ field: 'origin',
215
260
  headerName: 'Origin',
216
261
  width: 140,
217
- valueGetter: (_value, row)=>templateProvenance(row).origin === 'installed' ? 'Installed' : 'Yours',
218
- renderCell: ({ value })=>value === 'Installed' ? /*#__PURE__*/ _jsx(Chip, {
262
+ valueGetter: (_value, row)=>row.originKey,
263
+ renderCell: ({ value })=>value === 'installed' ? /*#__PURE__*/ _jsx(Chip, {
219
264
  size: "small",
220
265
  label: "Installed"
221
266
  }) : /*#__PURE__*/ _jsx(Typography, {
@@ -255,12 +300,19 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
255
300
  variant: "body2",
256
301
  color: "text.secondary",
257
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.'
258
- }) : /*#__PURE__*/ _jsx(ListTable, {
259
- "aria-label": "Email templates",
260
- rows: emailScreens,
261
- columns: columns,
262
- rowHeight: TABLE_ROW_HEIGHT,
263
- onOpen: (_id, row)=>router.push(templateHref(row))
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
+ ]
264
316
  }),
265
317
  truncated ? /*#__PURE__*/ _jsx(Alert, {
266
318
  severity: "info",
@@ -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 {\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 { TABLE_ROW_HEIGHT } 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 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// 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 [readScreens],\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) =>\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 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 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 <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={columns}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\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","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","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","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","displayName","localeCompare","handleCreate","push","error","console","message","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","now","templateHref","templateName","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","request","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","length","aria-label","rowHeight","onOpen","_id","severity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\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: '#create-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":"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"}
@@ -15,11 +15,13 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx } from "react/jsx-runtime";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { consentGroupForHost } from "@aglyn/aglyn";
20
20
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
21
21
  import { HubSections } from "@aglyn/shared-ui-next";
22
+ import { Stack } from "@mui/material";
22
23
  import { useMemo } from "react";
24
+ import ConsentGroupConfirmationCard from "./consent-group-confirmation-card.js";
23
25
  import EmailScreensCard from "./email-screens-card.js";
24
26
  import EmailTemplateDetail from "./email-template-detail.js";
25
27
  import EmailTopicDetail from "./email-topic-detail.js";
@@ -223,9 +225,22 @@ import { EMAIL_MESSAGES_ZONE } from "./email-zones.js";
223
225
  hostId: null,
224
226
  topicId: detail[0],
225
227
  basePath: basePath
226
- }) : /*#__PURE__*/ _jsx(EmailTopicsCard, {
227
- hostId: null,
228
- basePath: basePath
228
+ }) : /*
229
+ * The catalog, whose topics each set their own confirmation, and under
230
+ * it the organization's one switch over how a declared consent group
231
+ * treats a confirmation one of its sites is waiting on. Only here: the
232
+ * switch is the organization's, so a site's Topics page has none.
233
+ */ /*#__PURE__*/ _jsxs(Stack, {
234
+ spacing: 3,
235
+ children: [
236
+ /*#__PURE__*/ _jsx(EmailTopicsCard, {
237
+ hostId: null,
238
+ basePath: basePath
239
+ }),
240
+ /*#__PURE__*/ _jsx(ConsentGroupConfirmationCard, {
241
+ org: org
242
+ })
243
+ ]
229
244
  });
230
245
  case 'sending':
231
246
  /*