@aglyn/plugins-email 1.0.0-beta.196 → 1.0.0-beta.198

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 (50) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/consent-group-access.d.ts +33 -0
  3. package/src/lib/components/consent-group-access.js +73 -0
  4. package/src/lib/components/consent-group-access.js.map +1 -0
  5. package/src/lib/components/consent-group-change-progress.d.ts +55 -0
  6. package/src/lib/components/consent-group-change-progress.js +282 -0
  7. package/src/lib/components/consent-group-change-progress.js.map +1 -0
  8. package/src/lib/components/consent-group-confirmation-card.d.ts +6 -16
  9. package/src/lib/components/consent-group-confirmation-card.js +7 -50
  10. package/src/lib/components/consent-group-confirmation-card.js.map +1 -1
  11. package/src/lib/components/consent-group-dialog.d.ts +53 -0
  12. package/src/lib/components/consent-group-dialog.js +495 -0
  13. package/src/lib/components/consent-group-dialog.js.map +1 -0
  14. package/src/lib/components/consent-group-editing.d.ts +127 -0
  15. package/src/lib/components/consent-group-editing.js +225 -0
  16. package/src/lib/components/consent-group-editing.js.map +1 -0
  17. package/src/lib/components/consent-group-review.d.ts +62 -0
  18. package/src/lib/components/consent-group-review.js +353 -0
  19. package/src/lib/components/consent-group-review.js.map +1 -0
  20. package/src/lib/components/consent-group-site-picker.d.ts +35 -0
  21. package/src/lib/components/consent-group-site-picker.js +190 -0
  22. package/src/lib/components/consent-group-site-picker.js.map +1 -0
  23. package/src/lib/components/consent-groups-api.d.ts +158 -0
  24. package/src/lib/components/consent-groups-api.js +250 -0
  25. package/src/lib/components/consent-groups-api.js.map +1 -0
  26. package/src/lib/components/consent-groups-card.d.ts +47 -0
  27. package/src/lib/components/consent-groups-card.js +351 -0
  28. package/src/lib/components/consent-groups-card.js.map +1 -0
  29. package/src/lib/components/email-screens-card.js +1 -1
  30. package/src/lib/components/email-screens-card.js.map +1 -1
  31. package/src/lib/components/emails-console-page.d.ts +1 -1
  32. package/src/lib/components/emails-console-page.js +62 -27
  33. package/src/lib/components/emails-console-page.js.map +1 -1
  34. package/src/lib/components/emails-console-sections.d.ts +1 -1
  35. package/src/lib/components/emails-console-sections.js +14 -1
  36. package/src/lib/components/emails-console-sections.js.map +1 -1
  37. package/src/lib/components/org-email-templates-card.js +1 -1
  38. package/src/lib/components/org-email-templates-card.js.map +1 -1
  39. package/src/lib/components/site-consent-group-card.d.ts +23 -0
  40. package/src/lib/components/site-consent-group-card.js +80 -0
  41. package/src/lib/components/site-consent-group-card.js.map +1 -0
  42. package/src/lib/components/suppressions-card.js +28 -7
  43. package/src/lib/components/suppressions-card.js.map +1 -1
  44. package/src/lib/server-console.js +4 -2
  45. package/src/lib/server-console.js.map +1 -1
  46. package/src/lib/server-list-gate.d.ts +9 -0
  47. package/src/lib/server-list-gate.js +23 -9
  48. package/src/lib/server-list-gate.js.map +1 -1
  49. package/src/lib/server-list-import.js +5 -2
  50. package/src/lib/server-list-import.js.map +1 -1
@@ -15,60 +15,15 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
- import { consentGroupsAwaitConfirmation, orgPermissionLabel, pluginDocsHelp, readConsentGroups, resolveOrgPermissions } from "@aglyn/aglyn";
18
+ import { consentGroupsAwaitConfirmation, pluginDocsHelp, readConsentGroups } from "@aglyn/aglyn";
19
19
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
20
20
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
21
21
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
22
- import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
22
+ import { useUser } from "@aglyn/tenant-feature-instance";
23
23
  import { FormControlLabel, FormHelperText, Stack, Switch, Typography } from "@mui/material";
24
- import { doc } from "firebase/firestore";
25
24
  import { useEffect, useState } from "react";
25
+ import { useConsentGroupAccess } from "./consent-group-access.js";
26
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
27
  /**
73
28
  * "Wait for confirmation across a consent group" — the org's one switch over
74
29
  * whether a confirmation click one site asked for holds the rest of the
@@ -92,11 +47,13 @@ import { useEmailOrgMount } from "./email-org-mount.js";
92
47
  * then, and snaps back when the route refuses.
93
48
  */ export function ConsentGroupConfirmationCard(props) {
94
49
  var _useEmailOrgMount;
95
- const { org } = props;
50
+ const { org, access } = props;
96
51
  const orgId = (_useEmailOrgMount = useEmailOrgMount()) == null ? void 0 : _useEmailOrgMount.orgId;
97
52
  const { data: user } = useUser();
98
53
  const { enqueueSnackbar } = useSnackbar();
99
- const { missing, ready } = useConsentGroupConfirmationAccess(orgId);
54
+ // Handed the answer, the card opens no read of its own for it.
55
+ const own = useConsentGroupAccess(access ? undefined : orgId);
56
+ const { missing, ready } = access != null ? access : own;
100
57
  const stored = consentGroupsAwaitConfirmation(org);
101
58
  const declared = Object.keys(readConsentGroups(org)).length > 0;
102
59
  const [checked, setChecked] = useState(stored);
@@ -1 +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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"}
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 consentGroupsAwaitConfirmation,\n pluginDocsHelp,\n readConsentGroups,\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 { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n FormControlLabel,\n FormHelperText,\n Stack,\n Switch,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { type ConsentGroupAccess, useConsentGroupAccess } from './consent-group-access'\nimport { useEmailOrgMount } from './email-org-mount'\n\nexport interface ConsentGroupConfirmationCardProps {\n /** The org document the shell passes, kept live by its listener. */\n org?: Record<string, unknown> | null\n /**\n * Whether the reader may move the switch, when the section already asked.\n * Absent, the card reads the reader's membership itself.\n */\n access?: ConsentGroupAccess\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, access } = props\n const orgId = useEmailOrgMount()?.orgId\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // Handed the answer, the card opens no read of its own for it.\n const own = useConsentGroupAccess(access ? undefined : orgId)\n const { missing, ready } = access ?? own\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","pluginDocsHelp","readConsentGroups","CardDisplay","useSnackbar","authorizedFetch","useUser","FormControlLabel","FormHelperText","Stack","Switch","Typography","useEffect","useState","useConsentGroupAccess","useEmailOrgMount","ConsentGroupConfirmationCard","props","org","access","orgId","data","user","enqueueSnackbar","own","undefined","missing","ready","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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"}
@@ -0,0 +1,53 @@
1
+ import type { ConsolePluginOrgHost } from '@aglyn/aglyn';
2
+ export type ConsentGroupDialogMode = 'create' | 'edit' | 'dissolve';
3
+ export interface ConsentGroupDialogProps {
4
+ open: boolean;
5
+ mode: ConsentGroupDialogMode;
6
+ /** The group to edit or dissolve; ignored when creating. */
7
+ groupId: string | null;
8
+ /** The org document, kept live by the shell. */
9
+ org: Record<string, unknown> | null | undefined;
10
+ orgId: string;
11
+ /** Every site the organization has. */
12
+ sites: readonly ConsolePluginOrgHost[];
13
+ onClose: () => void;
14
+ /**
15
+ * The route accepted the change. `done` when it already finished — a
16
+ * rename moves no data, so it can complete inside the request.
17
+ */
18
+ onApplied: (result: {
19
+ changeId: string;
20
+ done: boolean;
21
+ }) => void;
22
+ }
23
+ /**
24
+ * Declare, change or dissolve one consent group (AGL-3320).
25
+ *
26
+ * ## Two steps, because the second one is the consequence
27
+ *
28
+ * The EDIT step is the declaration: a name, which every signup form on the
29
+ * group's sites will show, and the sites. The REVIEW step is what that
30
+ * declaration does to people — who stops getting mail, who can still be
31
+ * mailed, what happens to the CRM's records — counted by the route before
32
+ * anything is written. Nothing changes until the review's button is pressed,
33
+ * and that button is named for what it does.
34
+ *
35
+ * ## The whole declaration is sent, against the one that was read
36
+ *
37
+ * The route takes the org's complete next declaration and the raw one the
38
+ * dialog was opened against, so a change somebody else made meanwhile is
39
+ * refused rather than overwritten. The dialog snapshots that declaration
40
+ * when it opens and edits against the snapshot; when the route says it is
41
+ * stale, the dialog reloads the current one and asks again.
42
+ *
43
+ * ## Dissolving is where a group with too few sites ends up
44
+ *
45
+ * A group of one site is not a group, so unticking all but one site of an
46
+ * existing group does not produce an invalid form — it becomes the dissolve,
47
+ * and the dialog says so before anything is reviewed.
48
+ */
49
+ export declare function ConsentGroupDialog(props: ConsentGroupDialogProps): import("react").JSX.Element;
50
+ export declare namespace ConsentGroupDialog {
51
+ var displayName: string;
52
+ }
53
+ export default ConsentGroupDialog;