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

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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-groups-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The console's client of `POST /api/orgs/consent-groups` — the one door\n * through which a consent group is declared, edited or dissolved (AGL-3320).\n *\n * ## Why the editor never writes Firestore\n *\n * `consentGroups` decides whose opt-outs hold a send and who a grant covers,\n * so the rules deny it to every client. A change is also not one write: the\n * route carries every refusal the change would otherwise lose onto the sites\n * that stop reading each other, flips the declaration, and then re-homes the\n * CRM records keyed by the old group — a job of several invocations with its\n * own lock. The console only asks: `preview` what a change would do, `apply`\n * it, and drive it with `continue` while somebody is watching. Everything\n * else is the server's.\n *\n * ## The wire shapes are the route's own\n *\n * Every request and response type below is the planner module's\n * (`@aglyn/aglyn/app-utils/consent-group-change`), imported as a type and\n * erased from the bundle, so a field the route renames fails this plugin's\n * type check instead of rendering a sentence with a hole in it. The constants\n * come from the same module; it is pure, with no read and no server import.\n *\n * Nothing the server says is trusted to be well formed all the same: every\n * reader below accepts a missing or mistyped field and answers the narrow\n * value, because a progress banner that throws on a field it did not expect\n * would hide the one change an org cannot afford to lose track of.\n */\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_NAME_MAX,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n CONSENT_GROUPS_CHANGE_FIELD,\n type ConsentGroupChangeEstimate,\n type ConsentGroupChangePreview,\n type ConsentGroupChangePreviewLine,\n type ConsentGroupDraft,\n type ConsentGroupsApiChangeRequest,\n type ConsentGroupsApiChangeResponse,\n type ConsentGroupsApiDeclarationRequest,\n type ConsentGroupsApiPreviewResponse,\n type ConsentGroupsApiRequest,\n type ConsentGroupsChangeMarker,\n type ConsentGroupsChangePhase,\n type ConsentGroupValidationCode,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport { MAX_CONSENT_GROUP_HOSTS } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\n/** The route every call below posts to. */\nexport const CONSENT_GROUPS_ROUTE = '/api/orgs/consent-groups'\n\nexport {\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_NAME_MAX,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n CONSENT_GROUPS_CHANGE_FIELD,\n MAX_CONSENT_GROUP_HOSTS,\n}\n\n/** The fewest sites a group may name; one site is already its own sender. */\nexport const CONSENT_GROUP_MIN_HOSTS = 2\n\nexport type { ConsentGroupChangeEstimate, ConsentGroupChangePreview }\n\n/** One group of the complete next declaration; a new group has no id. */\nexport type ConsentGroupDeclarationEntry = ConsentGroupDraft\n\n/**\n * `preview` or `apply` a declaration.\n *\n * `groups` is the WHOLE next declaration, not a patch: a group left out is a\n * group dissolved. `expected` is the raw `org.consentGroups` the editor was\n * opened against, or `null` when the org had none, so a change made by\n * somebody else in the meantime is refused rather than overwritten.\n */\nexport type ConsentGroupsDeclareRequest = ConsentGroupsApiDeclarationRequest\n\n/** Drive, stop or read one change that is already running. */\nexport type ConsentGroupsChangeRequest = ConsentGroupsApiChangeRequest\n\nexport type ConsentGroupsRequest = ConsentGroupsApiRequest\n\n/**\n * Where a running change is.\n *\n * - `carry` — refusals are being copied; the old declaration still holds.\n * - `rehome` — the new declaration is in force; CRM records are moving.\n * - `sweep` — a last pass for anything written while it changed.\n */\nexport type ConsentGroupChangePhase = ConsentGroupsChangePhase\n\n/** `org.consentGroupsChange`, present exactly while a change runs. */\nexport type ConsentGroupChangeMarker = ConsentGroupsChangeMarker\n\n/**\n * A plugin's own account of what the change does to its data, in its words.\n * The editor prints these as given; `count` is `null` when it was not\n * counted.\n */\nexport type ConsentGroupParticipantLine = ConsentGroupChangePreviewLine\n\nexport type ConsentGroupsPreviewResponse = ConsentGroupsApiPreviewResponse\n\n/** `apply`, `continue`, `cancel` and `status` all answer the change's state. */\nexport type ConsentGroupsProgressResponse = ConsentGroupsApiChangeResponse\n\n/** One refusal from the route's validation, pointing at what it refused. */\nexport interface ConsentGroupValidationIssue {\n code: string\n /** Index into the `groups` that were posted. */\n groupIndex?: number\n hostId?: string\n}\n\n/** Why a call did not succeed, sorted into what the editor does about it. */\nexport type ConsentGroupsFailure =\n | {\n /** 400: the declaration itself; `errors` point at what to fix. */\n kind: 'invalid'\n message: string\n errors: ConsentGroupValidationIssue[]\n }\n | {\n /** 409: somebody else changed the declaration first. */\n kind: 'stale'\n message: string\n /** The raw declaration as it now stands. */\n current: Record<string, unknown> | null\n }\n | {\n /** 409: another change is still running, sweep included. */\n kind: 'in-flight'\n message: string\n changeId: string | null\n }\n | {\n /** 409 on `cancel`: the declaration already flipped. */\n kind: 'took-effect'\n message: string\n }\n | { kind: 'locked'; message: string }\n | { kind: 'refused'; status: number; message: string }\n\n/**\n * A call's outcome. Each branch names the other's field as absent, so a\n * caller can read `failure` after testing `ok` — this workspace compiles\n * without `strictNullChecks`, under which a boolean discriminant alone does\n * not narrow.\n */\nexport type ConsentGroupsCallResult<T> =\n | { ok: true; body: T; failure?: undefined }\n | { ok: false; body?: undefined; failure: ConsentGroupsFailure }\n\n/*==========================================\n * VALIDATION CODES\n *=========================================*/\n\n/**\n * Every refusal the route's validation can name, and what to tell the admin.\n *\n * The route is authoritative — the editor checks only what it can check\n * without asking (a name, a site count) and shows the rest when the route\n * answers. The table is keyed by the route's own codes, so a code added there\n * without a sentence here fails the type check; one it still does not know —\n * a server newer than this bundle — gets a general sentence rather than none.\n */\nexport const CONSENT_GROUP_ISSUE_MESSAGES: Readonly<\n Record<ConsentGroupValidationCode, string>\n> = {\n malformed: 'This change couldn’t be read. Reload the page and try again.',\n 'name-empty':\n 'Give the group a name. Signup forms show it, so it can’t be blank.',\n 'name-too-long': `Keep the name to ${CONSENT_GROUP_NAME_MAX} characters or fewer.`,\n 'name-duplicate': 'Another consent group already has this name.',\n 'too-few-sites': 'A consent group needs at least two sites.',\n 'too-many-sites': `A consent group can include at most ${MAX_CONSENT_GROUP_HOSTS} sites.`,\n 'unknown-site': 'One of these sites is no longer part of your organization.',\n 'site-in-two-groups': 'A site can be in only one consent group.',\n 'unknown-group':\n 'This consent group no longer exists. Close this and start again.',\n 'group-replaced':\n 'Every site in this group would be replaced. Create a new group for these sites instead.',\n 'duplicate-group':\n 'The same consent group appears twice. Reload the page and try again.',\n 'no-change': 'Nothing has changed yet.',\n}\n\n/** The sentence for one validation refusal, naming the site when it can. */\nexport function consentGroupIssueMessage(\n issue: ConsentGroupValidationIssue,\n siteName?: (hostId: string) => string,\n): string {\n const code = issue.code\n const site = issue.hostId && siteName ? siteName(issue.hostId) : ''\n if (site && code === 'unknown-site') {\n return `${site} is no longer part of your organization.`\n }\n if (site && code === 'site-in-two-groups') {\n return `${site} can be in only one consent group.`\n }\n return (\n CONSENT_GROUP_ISSUE_MESSAGES[code as ConsentGroupValidationCode] ??\n 'This change can’t be made as it stands. Check the name and sites and try again.'\n )\n}\n\n/*==========================================\n * CALLING THE ROUTE\n *=========================================*/\n\n/** A token source `authorizedFetch` accepts: the signed-in user. */\ntype ConsentGroupsCaller = Parameters<typeof authorizedFetch>[0]\n\nconst STALE_MESSAGE =\n 'Someone else changed consent groups while you were editing.'\nconst IN_FLIGHT_MESSAGE =\n 'Finishing the last change. You can make another once it’s done.'\nconst TOOK_EFFECT_MESSAGE =\n 'This change already took effect, so it can’t be stopped now. It will finish on its own.'\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n\n/**\n * Sorts a refused answer into what the editor does next.\n *\n * Exported for the specs, which assert that each status the route documents\n * lands in the branch the dialog handles.\n */\nexport function classifyConsentGroupsFailure(\n status: number,\n payload: unknown,\n action: ConsentGroupsRequest['action'],\n): ConsentGroupsFailure {\n const body = asRecord(payload)\n const said = typeof body['error'] === 'string' ? body['error'] : ''\n if (status === 400) {\n const errors = Array.isArray(body['errors'])\n ? (body['errors'] as unknown[])\n .map(asRecord)\n .filter((issue) => typeof issue['code'] === 'string')\n .map((issue) => ({\n code: String(issue['code']),\n ...(typeof issue['groupIndex'] === 'number'\n ? { groupIndex: issue['groupIndex'] as number }\n : {}),\n ...(typeof issue['hostId'] === 'string'\n ? { hostId: issue['hostId'] as string }\n : {}),\n }))\n : []\n return {\n kind: 'invalid',\n message: errors.length\n ? consentGroupIssueMessage(errors[0])\n : said || 'This change can’t be made as it stands.',\n errors,\n }\n }\n if (status === 409) {\n if (action === 'cancel') {\n return { kind: 'took-effect', message: TOOK_EFFECT_MESSAGE }\n }\n if ('current' in body) {\n const current = body['current']\n return {\n kind: 'stale',\n message: STALE_MESSAGE,\n current:\n current && typeof current === 'object' && !Array.isArray(current)\n ? (current as Record<string, unknown>)\n : null,\n }\n }\n return {\n kind: 'in-flight',\n message: IN_FLIGHT_MESSAGE,\n changeId: typeof body['changeId'] === 'string' ? body['changeId'] : null,\n }\n }\n if (status === 423) {\n const locked = parseLockdownRefusal(status, payload)\n return {\n kind: 'locked',\n message: locked\n ? lockdownRefusalText(locked)\n : 'Changes are paused right now. Try again later.',\n }\n }\n if (status === 401) {\n return {\n kind: 'refused',\n status,\n message: 'Your session has ended. Sign in again to change consent groups.',\n }\n }\n if (status === 403) {\n return {\n kind: 'refused',\n status,\n message:\n body['reason'] === 'email-unverified'\n ? 'Verify your email address to change consent groups.'\n : said || 'You don’t have permission to change consent groups.',\n }\n }\n if (status === 404) {\n return {\n kind: 'refused',\n status,\n message: said || 'This organization could not be found.',\n }\n }\n return {\n kind: 'refused',\n status,\n message: said || 'Something went wrong. Try again in a moment.',\n }\n}\n\n/**\n * Posts one action and sorts the answer. Never throws: a network failure is\n * a refusal with a sentence, so every caller has exactly two branches.\n */\nexport async function postConsentGroups<T>(\n user: ConsentGroupsCaller,\n request: ConsentGroupsRequest,\n): Promise<ConsentGroupsCallResult<T>> {\n let response: Response\n try {\n response = await authorizedFetch(user, CONSENT_GROUPS_ROUTE, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n } catch {\n return {\n ok: false,\n failure: {\n kind: 'refused',\n status: 0,\n message:\n 'The server couldn’t be reached. Check your connection and try again.',\n },\n }\n }\n const payload = await response.json().catch(() => null)\n if (response.ok && asRecord(payload)['ok'] === true) {\n return { ok: true, body: payload as T }\n }\n return {\n ok: false,\n failure: classifyConsentGroupsFailure(\n response.ok ? 500 : response.status,\n payload,\n request.action,\n ),\n }\n}\n\n/*==========================================\n * READING WHAT THE ORG DOCUMENT AND THE JOB SAY\n *=========================================*/\n\nconst PHASES: readonly ConsentGroupChangePhase[] = ['carry', 'rehome', 'sweep']\n\nconst finiteNumber = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\n\n/**\n * The org's in-flight change, or `null` when none runs.\n *\n * A marker without a change id is not a change anybody can drive or stop,\n * so it reads as absent; an unknown phase reads as `carry`, the phase in\n * which nothing has taken effect, because that is the claim a banner can\n * make without being wrong.\n */\nexport function readConsentGroupChangeMarker(\n org: Record<string, unknown> | null | undefined,\n): ConsentGroupChangeMarker | null {\n const raw = asRecord(asRecord(org)[CONSENT_GROUPS_CHANGE_FIELD])\n const changeId = typeof raw['changeId'] === 'string' ? raw['changeId'] : ''\n if (!changeId) return null\n const phase = PHASES.includes(raw['phase'] as ConsentGroupChangePhase)\n ? (raw['phase'] as ConsentGroupChangePhase)\n : 'carry'\n const declaredAtMs = finiteNumber(raw['declaredAtMs'])\n return {\n changeId,\n phase,\n hostIds: Array.isArray(raw['hostIds'])\n ? (raw['hostIds'] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && !!id,\n )\n : [],\n startedAtMs: finiteNumber(raw['startedAtMs']) ?? 0,\n ...(declaredAtMs != null ? { declaredAtMs } : {}),\n }\n}\n\n/** What the progress banner needs from a change's job document. */\nexport interface ConsentGroupChangeJobView {\n /** The job's own phase, when it records one. */\n phase: string | null\n /** When another runner's claim on the job lapses; `null` when unclaimed. */\n leaseUntilMs: number | null\n stalled: boolean\n lastError: string | null\n failures: number\n /** The change finished, or was stopped before it took effect. */\n finished: boolean\n}\n\n/** Reads a job document defensively. `undefined` reads as a fresh job. */\nexport function readConsentGroupChangeJob(\n data: Record<string, unknown> | null | undefined,\n): ConsentGroupChangeJobView {\n const job = asRecord(data)\n const lease = asRecord(job['lease'])\n const lastError = job['lastError']\n const phase = typeof job['phase'] === 'string' ? job['phase'] : null\n return {\n phase,\n leaseUntilMs: finiteNumber(lease['untilMs']),\n stalled: job['stalled'] === true,\n lastError:\n typeof lastError === 'string'\n ? lastError\n : typeof asRecord(lastError)['message'] === 'string'\n ? String(asRecord(lastError)['message'])\n : null,\n failures: finiteNumber(job['failures']) ?? 0,\n finished:\n job['done'] === true ||\n finiteNumber(job['finishedAtMs']) != null ||\n finiteNumber(job['canceledAtMs']) != null ||\n phase === 'done' ||\n phase === 'canceled',\n }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_NAME_MAX","CONSENT_GROUP_SWEEP_DELAY_MS","CONSENT_GROUPS_CHANGE_FIELD","MAX_CONSENT_GROUP_HOSTS","authorizedFetch","CONSENT_GROUPS_ROUTE","CONSENT_GROUP_MIN_HOSTS","CONSENT_GROUP_ISSUE_MESSAGES","malformed","consentGroupIssueMessage","issue","siteName","code","site","hostId","STALE_MESSAGE","IN_FLIGHT_MESSAGE","TOOK_EFFECT_MESSAGE","asRecord","value","Array","isArray","classifyConsentGroupsFailure","status","payload","action","body","said","errors","map","filter","String","groupIndex","kind","message","length","current","changeId","locked","postConsentGroups","user","request","response","method","headers","JSON","stringify","ok","failure","json","catch","PHASES","finiteNumber","Number","isFinite","readConsentGroupChangeMarker","org","raw","phase","includes","declaredAtMs","hostIds","id","startedAtMs","readConsentGroupChangeJob","data","job","lease","lastError","leaseUntilMs","stalled","failures","finished"],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { type ConsentGroupAccess } from './consent-group-access';
2
+ export interface ConsentGroupsCardProps {
3
+ /** The org document the shell passes, kept live by its listener. */
4
+ org?: Record<string, unknown> | null;
5
+ /**
6
+ * Whether the reader may change consent groups, when the section already
7
+ * asked. Absent, the card reads the reader's membership itself.
8
+ */
9
+ access?: ConsentGroupAccess;
10
+ }
11
+ /**
12
+ * Consent groups — the sites this organization declares ONE SENDER
13
+ * (AGL-3320).
14
+ *
15
+ * ## What a group is, in the terms the admin is held to
16
+ *
17
+ * Undeclared, every site is its own sender: a signup on one site reaches only
18
+ * that site, and an unsubscribe applies to that site alone. A group names the
19
+ * sites that are one sender. Its signup forms say so by name, a signup made
20
+ * on one of those forms may be mailed by every site in it, and an opt-out on
21
+ * any of them stops marketing email from all of them. This card lists the
22
+ * groups and is the only way to change them.
23
+ *
24
+ * ## Lists here, edits in a dialog
25
+ *
26
+ * The console's standing shape for a list surface: **New group** sits in the
27
+ * header and opens a dialog, and each row's menu edits or dissolves that
28
+ * group in the same dialog. No form sits above the table.
29
+ *
30
+ * ## One change at a time, shown until it is done
31
+ *
32
+ * A change runs as a job — refusals copied, the declaration flipped, CRM
33
+ * records moved — and the org document carries its marker until it
34
+ * finishes. While it does, the banner above the table shows where it is and
35
+ * the controls say "Finishing the last change", because the route refuses a
36
+ * second change until the first is swept.
37
+ *
38
+ * Reads nothing of its own beyond the reader's membership: the groups and
39
+ * the marker are fields of the org document the shell already holds, and the
40
+ * job document is read only by the banner, only while a change runs, and
41
+ * only for a reader who may drive it.
42
+ */
43
+ export declare function ConsentGroupsCard(props: ConsentGroupsCardProps): import("react").JSX.Element;
44
+ export declare namespace ConsentGroupsCard {
45
+ var displayName: string;
46
+ }
47
+ export default ConsentGroupsCard;
@@ -0,0 +1,351 @@
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 { pluginDocsHelp } from "@aglyn/aglyn";
19
+ import { mdiLinkVariantOff, mdiPencilOutline } from "@aglyn/shared-data-mdi";
20
+ import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
21
+ import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
22
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
23
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
+ import { Button, Chip, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
+ import { useConsentGroupAccess } from "./consent-group-access.js";
27
+ import ConsentGroupChangeProgress from "./consent-group-change-progress.js";
28
+ import ConsentGroupDialog from "./consent-group-dialog.js";
29
+ import { draftDisclosure, listConsentGroups, unusableConsentGroupCount } from "./consent-group-editing.js";
30
+ import { CONSENT_GROUP_MIN_HOSTS, readConsentGroupChangeMarker } from "./consent-groups-api.js";
31
+ import { orgSiteName, useEmailOrgMount } from "./email-org-mount.js";
32
+ /** Why the editor's controls are off, in the order a reader would fix them. */ function blockedReason(input) {
33
+ const { access, siteCount, sitesReady, changing } = input;
34
+ if (!access.ready) return null;
35
+ if (access.missing) {
36
+ return `You need the ${access.missing} permission to change consent groups.`;
37
+ }
38
+ if (changing) return 'Finishing the last change.';
39
+ if (sitesReady && siteCount < CONSENT_GROUP_MIN_HOSTS) {
40
+ return 'A consent group needs at least two sites.';
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Consent groups — the sites this organization declares ONE SENDER
46
+ * (AGL-3320).
47
+ *
48
+ * ## What a group is, in the terms the admin is held to
49
+ *
50
+ * Undeclared, every site is its own sender: a signup on one site reaches only
51
+ * that site, and an unsubscribe applies to that site alone. A group names the
52
+ * sites that are one sender. Its signup forms say so by name, a signup made
53
+ * on one of those forms may be mailed by every site in it, and an opt-out on
54
+ * any of them stops marketing email from all of them. This card lists the
55
+ * groups and is the only way to change them.
56
+ *
57
+ * ## Lists here, edits in a dialog
58
+ *
59
+ * The console's standing shape for a list surface: **New group** sits in the
60
+ * header and opens a dialog, and each row's menu edits or dissolves that
61
+ * group in the same dialog. No form sits above the table.
62
+ *
63
+ * ## One change at a time, shown until it is done
64
+ *
65
+ * A change runs as a job — refusals copied, the declaration flipped, CRM
66
+ * records moved — and the org document carries its marker until it
67
+ * finishes. While it does, the banner above the table shows where it is and
68
+ * the controls say "Finishing the last change", because the route refuses a
69
+ * second change until the first is swept.
70
+ *
71
+ * Reads nothing of its own beyond the reader's membership: the groups and
72
+ * the marker are fields of the org document the shell already holds, and the
73
+ * job document is read only by the banner, only while a change runs, and
74
+ * only for a reader who may drive it.
75
+ */ export function ConsentGroupsCard(props) {
76
+ var _ref, _ref1, _ref2, _ref3, _ref4;
77
+ const { org, access } = props;
78
+ const mount = useEmailOrgMount();
79
+ const orgId = (_ref = mount == null ? void 0 : mount.orgId) != null ? _ref : '';
80
+ const { enqueueSnackbar } = useSnackbar();
81
+ // Handed the answer, the card opens no read of its own for it.
82
+ const own = useConsentGroupAccess(access ? undefined : orgId || undefined);
83
+ const allowed = access != null ? access : own;
84
+ const groups = useMemo(()=>listConsentGroups(org), [
85
+ org
86
+ ]);
87
+ const unusable = unusableConsentGroupCount(org);
88
+ const marker = readConsentGroupChangeMarker(org);
89
+ const sites = (_ref1 = mount == null ? void 0 : mount.hosts) != null ? _ref1 : [];
90
+ const sitesReady = (_ref2 = mount == null ? void 0 : mount.hostsReady) != null ? _ref2 : false;
91
+ /*
92
+ * The change this page just started, held until the org document's marker
93
+ * catches up — the listener lands a moment after the route answers, and
94
+ * the banner should not blink out in between.
95
+ */ const [started, setStarted] = useState(null);
96
+ useEffect(()=>{
97
+ if (started && (marker == null ? void 0 : marker.changeId) === started) setStarted(null);
98
+ }, [
99
+ started,
100
+ marker == null ? void 0 : marker.changeId
101
+ ]);
102
+ const changeId = (_ref3 = marker == null ? void 0 : marker.changeId) != null ? _ref3 : started;
103
+ /*
104
+ * Announced once per change, whichever way its end is noticed: the banner's
105
+ * own answer, the job document, or the marker leaving the org document.
106
+ */ const announced = useRef(new Set());
107
+ const announce = useCallback((id, outcome)=>{
108
+ if (announced.current.has(id)) return;
109
+ announced.current.add(id);
110
+ if (outcome === 'done') {
111
+ enqueueSnackbar('Consent groups updated', {
112
+ variant: 'success',
113
+ persist: false
114
+ });
115
+ }
116
+ }, [
117
+ enqueueSnackbar
118
+ ]);
119
+ const seenMarker = useRef(null);
120
+ useEffect(()=>{
121
+ var _ref;
122
+ const previous = seenMarker.current;
123
+ seenMarker.current = (_ref = marker == null ? void 0 : marker.changeId) != null ? _ref : null;
124
+ if (previous && !marker) announce(previous, 'done');
125
+ }, [
126
+ marker,
127
+ announce
128
+ ]);
129
+ const [dialog, setDialog] = useState(null);
130
+ const blocked = blockedReason({
131
+ access: allowed,
132
+ siteCount: sites.length,
133
+ sitesReady,
134
+ changing: Boolean(changeId)
135
+ });
136
+ const canEdit = allowed.ready && !blocked && Boolean(orgId);
137
+ const rowBlocked = allowed.ready ? blocked != null ? blocked : null : 'Checking your permissions';
138
+ const changingHosts = new Set((_ref4 = marker == null ? void 0 : marker.hostIds) != null ? _ref4 : []);
139
+ const rowActions = (groupId)=>[
140
+ {
141
+ key: 'edit',
142
+ label: 'Edit group',
143
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
144
+ path: mdiPencilOutline.path,
145
+ size: 0.8
146
+ }),
147
+ disabled: !canEdit,
148
+ disabledReason: canEdit ? undefined : rowBlocked != null ? rowBlocked : undefined,
149
+ onClick: ()=>setDialog({
150
+ mode: 'edit',
151
+ groupId
152
+ })
153
+ },
154
+ {
155
+ key: 'dissolve',
156
+ label: 'Dissolve group',
157
+ icon: /*#__PURE__*/ _jsx(MdiIcon, {
158
+ path: mdiLinkVariantOff.path,
159
+ size: 0.8
160
+ }),
161
+ destructive: true,
162
+ disabled: !canEdit,
163
+ disabledReason: canEdit ? undefined : rowBlocked != null ? rowBlocked : undefined,
164
+ onClick: ()=>setDialog({
165
+ mode: 'dissolve',
166
+ groupId
167
+ })
168
+ }
169
+ ];
170
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
171
+ header: "Consent groups",
172
+ help: pluginDocsHelp('emailCampaigns', {
173
+ anchor: '#consent-groups'
174
+ }),
175
+ contentGutterX: true,
176
+ contentGutterY: true,
177
+ HeaderProps: {
178
+ action: /*#__PURE__*/ _jsx(Button, {
179
+ variant: "contained",
180
+ disabled: !canEdit,
181
+ onClick: ()=>setDialog({
182
+ mode: 'create',
183
+ groupId: null
184
+ }),
185
+ children: 'New group'
186
+ })
187
+ },
188
+ children: [
189
+ /*#__PURE__*/ _jsxs(Stack, {
190
+ spacing: 2,
191
+ children: [
192
+ /*#__PURE__*/ _jsx(Typography, {
193
+ variant: "body2",
194
+ color: "text.secondary",
195
+ children: 'Sites in a consent group send as one. Their signup forms name the ' + 'group, someone who signs up on one of those forms can hear from ' + 'every site in it, and an unsubscribe or opt-out on any of them ' + 'stops marketing email from all of them. A site in no group sends ' + 'on its own.'
196
+ }),
197
+ blocked ? /*#__PURE__*/ _jsx(Typography, {
198
+ variant: "body2",
199
+ color: "text.secondary",
200
+ children: blocked
201
+ }) : null,
202
+ changeId && orgId ? /*#__PURE__*/ _jsx(ConsentGroupChangeProgress, {
203
+ orgId: orgId,
204
+ changeId: changeId,
205
+ marker: marker,
206
+ canDrive: allowed.ready && !allowed.missing,
207
+ onFinished: (outcome)=>{
208
+ announce(changeId, outcome);
209
+ setStarted(null);
210
+ }
211
+ }, changeId) : null,
212
+ groups.length ? /*#__PURE__*/ _jsxs(ScrollTable, {
213
+ size: "small",
214
+ children: [
215
+ /*#__PURE__*/ _jsx(TableHead, {
216
+ children: /*#__PURE__*/ _jsxs(TableRow, {
217
+ children: [
218
+ /*#__PURE__*/ _jsx(TableCell, {
219
+ children: 'Group'
220
+ }),
221
+ /*#__PURE__*/ _jsx(TableCell, {
222
+ children: 'Sites'
223
+ }),
224
+ /*#__PURE__*/ _jsx(TableCell, {
225
+ children: 'Signup forms say'
226
+ }),
227
+ /*#__PURE__*/ _jsx(TableCell, {
228
+ align: "right"
229
+ })
230
+ ]
231
+ })
232
+ }),
233
+ /*#__PURE__*/ _jsx(TableBody, {
234
+ children: groups.map((group)=>{
235
+ var _draftDisclosure;
236
+ return /*#__PURE__*/ _jsxs(TableRow, {
237
+ children: [
238
+ /*#__PURE__*/ _jsx(TableCell, {
239
+ sx: {
240
+ verticalAlign: 'top'
241
+ },
242
+ children: /*#__PURE__*/ _jsxs(Stack, {
243
+ spacing: 0.5,
244
+ sx: {
245
+ alignItems: 'flex-start'
246
+ },
247
+ children: [
248
+ /*#__PURE__*/ _jsx(Typography, {
249
+ variant: "body2",
250
+ sx: {
251
+ fontWeight: 500
252
+ },
253
+ children: group.name
254
+ }),
255
+ /*#__PURE__*/ _jsx(Typography, {
256
+ variant: "caption",
257
+ color: "text.secondary",
258
+ sx: {
259
+ fontFamily: 'monospace'
260
+ },
261
+ children: group.id
262
+ }),
263
+ group.hostIds.some((hostId)=>changingHosts.has(hostId)) ? /*#__PURE__*/ _jsx(Chip, {
264
+ size: "small",
265
+ color: "info",
266
+ variant: "outlined",
267
+ label: "Changing"
268
+ }) : null
269
+ ]
270
+ })
271
+ }),
272
+ /*#__PURE__*/ _jsx(TableCell, {
273
+ sx: {
274
+ verticalAlign: 'top'
275
+ },
276
+ children: /*#__PURE__*/ _jsx(Stack, {
277
+ direction: "row",
278
+ spacing: 0.5,
279
+ useFlexGap: true,
280
+ sx: {
281
+ flexWrap: 'wrap'
282
+ },
283
+ children: group.hostIds.map((hostId)=>/*#__PURE__*/ _jsx(Chip, {
284
+ size: "small",
285
+ variant: "outlined",
286
+ label: orgSiteName(mount, hostId)
287
+ }, hostId))
288
+ })
289
+ }),
290
+ /*#__PURE__*/ _jsx(TableCell, {
291
+ sx: {
292
+ verticalAlign: 'top'
293
+ },
294
+ children: /*#__PURE__*/ _jsx(Typography, {
295
+ variant: "body2",
296
+ color: "text.secondary",
297
+ children: (_draftDisclosure = draftDisclosure(group.name, group.hostIds, group.id)) != null ? _draftDisclosure : '—'
298
+ })
299
+ }),
300
+ /*#__PURE__*/ _jsx(TableCell, {
301
+ align: "right",
302
+ sx: {
303
+ width: 56,
304
+ verticalAlign: 'top'
305
+ },
306
+ children: /*#__PURE__*/ _jsx(RowActionsMenu, {
307
+ label: group.name,
308
+ items: rowActions(group.id)
309
+ })
310
+ })
311
+ ]
312
+ }, group.id);
313
+ })
314
+ })
315
+ ]
316
+ }) : /*#__PURE__*/ _jsx(Typography, {
317
+ variant: "body2",
318
+ color: "text.secondary",
319
+ children: 'No consent groups yet. Every site is its own sender: someone who ' + 'signs up on one site hears only from that site, and an ' + 'unsubscribe applies to that site alone.'
320
+ }),
321
+ unusable > 0 ? /*#__PURE__*/ _jsx(Typography, {
322
+ variant: "caption",
323
+ color: "text.secondary",
324
+ children: `${unusable === 1 ? 'One saved consent group' : `${unusable} saved consent groups`} ` + 'can’t be used — no name, too few or too many sites, or a site ' + `another group also claims — so nothing honors ${unusable === 1 ? 'it' : 'them'}. ` + `The next change you make removes ${unusable === 1 ? 'it' : 'them'}.`
325
+ }) : null
326
+ ]
327
+ }),
328
+ dialog && orgId ? /*#__PURE__*/ _jsx(ConsentGroupDialog, {
329
+ open: true,
330
+ mode: dialog.mode,
331
+ groupId: dialog.groupId,
332
+ org: org,
333
+ orgId: orgId,
334
+ sites: sites,
335
+ onClose: ()=>setDialog(null),
336
+ onApplied: ({ changeId: applied, done })=>{
337
+ setDialog(null);
338
+ if (done) {
339
+ announce(applied, 'done');
340
+ return;
341
+ }
342
+ setStarted(applied);
343
+ }
344
+ }) : null
345
+ ]
346
+ });
347
+ }
348
+ ConsentGroupsCard.displayName = 'ConsentGroupsCard';
349
+ export default ConsentGroupsCard;
350
+
351
+ //# sourceMappingURL=consent-groups-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-groups-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiLinkVariantOff, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { type ConsentGroupAccess, useConsentGroupAccess } from './consent-group-access'\nimport ConsentGroupChangeProgress from './consent-group-change-progress'\nimport ConsentGroupDialog, { type ConsentGroupDialogMode } from './consent-group-dialog'\nimport {\n draftDisclosure,\n listConsentGroups,\n unusableConsentGroupCount,\n} from './consent-group-editing'\nimport {\n CONSENT_GROUP_MIN_HOSTS,\n readConsentGroupChangeMarker,\n} from './consent-groups-api'\nimport { orgSiteName, useEmailOrgMount } from './email-org-mount'\n\nexport interface ConsentGroupsCardProps {\n /** The org document the shell passes, kept live by its listener. */\n org?: Record<string, unknown> | null\n /**\n * Whether the reader may change consent groups, when the section already\n * asked. Absent, the card reads the reader's membership itself.\n */\n access?: ConsentGroupAccess\n}\n\n/** Why the editor's controls are off, in the order a reader would fix them. */\nfunction blockedReason(input: {\n access: ConsentGroupAccess\n siteCount: number\n sitesReady: boolean\n changing: boolean\n}): string | null {\n const { access, siteCount, sitesReady, changing } = input\n if (!access.ready) return null\n if (access.missing) {\n return `You need the ${access.missing} permission to change consent groups.`\n }\n if (changing) return 'Finishing the last change.'\n if (sitesReady && siteCount < CONSENT_GROUP_MIN_HOSTS) {\n return 'A consent group needs at least two sites.'\n }\n return null\n}\n\n/**\n * Consent groups — the sites this organization declares ONE SENDER\n * (AGL-3320).\n *\n * ## What a group is, in the terms the admin is held to\n *\n * Undeclared, every site is its own sender: a signup on one site reaches only\n * that site, and an unsubscribe applies to that site alone. A group names the\n * sites that are one sender. Its signup forms say so by name, a signup made\n * on one of those forms may be mailed by every site in it, and an opt-out on\n * any of them stops marketing email from all of them. This card lists the\n * groups and is the only way to change them.\n *\n * ## Lists here, edits in a dialog\n *\n * The console's standing shape for a list surface: **New group** sits in the\n * header and opens a dialog, and each row's menu edits or dissolves that\n * group in the same dialog. No form sits above the table.\n *\n * ## One change at a time, shown until it is done\n *\n * A change runs as a job — refusals copied, the declaration flipped, CRM\n * records moved — and the org document carries its marker until it\n * finishes. While it does, the banner above the table shows where it is and\n * the controls say \"Finishing the last change\", because the route refuses a\n * second change until the first is swept.\n *\n * Reads nothing of its own beyond the reader's membership: the groups and\n * the marker are fields of the org document the shell already holds, and the\n * job document is read only by the banner, only while a change runs, and\n * only for a reader who may drive it.\n */\nexport function ConsentGroupsCard(props: ConsentGroupsCardProps) {\n const { org, access } = props\n const mount = useEmailOrgMount()\n const orgId = mount?.orgId ?? ''\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 || undefined)\n const allowed = access ?? own\n\n const groups = useMemo(() => listConsentGroups(org), [org])\n const unusable = unusableConsentGroupCount(org)\n const marker = readConsentGroupChangeMarker(org)\n const sites = mount?.hosts ?? []\n const sitesReady = mount?.hostsReady ?? false\n\n /*\n * The change this page just started, held until the org document's marker\n * catches up — the listener lands a moment after the route answers, and\n * the banner should not blink out in between.\n */\n const [started, setStarted] = useState<string | null>(null)\n useEffect(() => {\n if (started && marker?.changeId === started) setStarted(null)\n }, [started, marker?.changeId])\n const changeId = marker?.changeId ?? started\n\n /*\n * Announced once per change, whichever way its end is noticed: the banner's\n * own answer, the job document, or the marker leaving the org document.\n */\n const announced = useRef(new Set<string>())\n const announce = useCallback(\n (id: string, outcome: 'done' | 'canceled') => {\n if (announced.current.has(id)) return\n announced.current.add(id)\n if (outcome === 'done') {\n enqueueSnackbar('Consent groups updated', { variant: 'success', persist: false })\n }\n },\n [enqueueSnackbar],\n )\n const seenMarker = useRef<string | null>(null)\n useEffect(() => {\n const previous = seenMarker.current\n seenMarker.current = marker?.changeId ?? null\n if (previous && !marker) announce(previous, 'done')\n }, [marker, announce])\n\n const [dialog, setDialog] = useState<{\n mode: ConsentGroupDialogMode\n groupId: string | null\n } | null>(null)\n\n const blocked = blockedReason({\n access: allowed,\n siteCount: sites.length,\n sitesReady,\n changing: Boolean(changeId),\n })\n const canEdit = allowed.ready && !blocked && Boolean(orgId)\n const rowBlocked = allowed.ready ? (blocked ?? null) : 'Checking your permissions'\n const changingHosts = new Set(marker?.hostIds ?? [])\n\n const rowActions = (groupId: string): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit group',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n disabled: !canEdit,\n disabledReason: canEdit ? undefined : (rowBlocked ?? undefined),\n onClick: () => setDialog({ mode: 'edit', groupId }),\n },\n {\n key: 'dissolve',\n label: 'Dissolve group',\n icon: <MdiIcon path={mdiLinkVariantOff.path} size={0.8} />,\n destructive: true,\n disabled: !canEdit,\n disabledReason: canEdit ? undefined : (rowBlocked ?? undefined),\n onClick: () => setDialog({ mode: 'dissolve', groupId }),\n },\n ]\n\n return (\n <CardDisplay\n header=\"Consent groups\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#consent-groups' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n disabled={!canEdit}\n onClick={() => setDialog({ mode: 'create', groupId: null })}\n >\n {'New group'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sites in a consent group send as one. Their signup forms name the ' +\n 'group, someone who signs up on one of those forms can hear from ' +\n 'every site in it, and an unsubscribe or opt-out on any of them ' +\n 'stops marketing email from all of them. A site in no group sends ' +\n 'on its own.'}\n </Typography>\n {blocked ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {blocked}\n </Typography>\n ) : null}\n {changeId && orgId ? (\n <ConsentGroupChangeProgress\n key={changeId}\n orgId={orgId}\n changeId={changeId}\n marker={marker}\n canDrive={allowed.ready && !allowed.missing}\n onFinished={(outcome) => {\n announce(changeId, outcome)\n setStarted(null)\n }}\n />\n ) : null}\n {groups.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Group'}</TableCell>\n <TableCell>{'Sites'}</TableCell>\n <TableCell>{'Signup forms say'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {groups.map((group) => (\n <TableRow key={group.id}>\n <TableCell sx={{ verticalAlign: 'top' }}>\n <Stack spacing={0.5} sx={{ alignItems: 'flex-start' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 500 }}>\n {group.name}\n </Typography>\n {/*\n The id an integration sends as `consentGroupId` to opt\n someone in for the whole group; nothing else shows it.\n */}\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ fontFamily: 'monospace' }}\n >\n {group.id}\n </Typography>\n {group.hostIds.some((hostId) => changingHosts.has(hostId)) ? (\n <Chip size=\"small\" color=\"info\" variant=\"outlined\" label=\"Changing\" />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell sx={{ verticalAlign: 'top' }}>\n <Stack direction=\"row\" spacing={0.5} useFlexGap sx={{ flexWrap: 'wrap' }}>\n {group.hostIds.map((hostId) => (\n <Chip\n key={hostId}\n size=\"small\"\n variant=\"outlined\"\n label={orgSiteName(mount, hostId)}\n />\n ))}\n </Stack>\n </TableCell>\n <TableCell sx={{ verticalAlign: 'top' }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {draftDisclosure(group.name, group.hostIds, group.id) ?? '—'}\n </Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56, verticalAlign: 'top' }}>\n <RowActionsMenu label={group.name} items={rowActions(group.id)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No consent groups yet. Every site is its own sender: someone who ' +\n 'signs up on one site hears only from that site, and an ' +\n 'unsubscribe applies to that site alone.'}\n </Typography>\n )}\n {unusable > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${unusable === 1 ? 'One saved consent group' : `${unusable} saved consent groups`} ` +\n 'can’t be used — no name, too few or too many sites, or a site ' +\n `another group also claims — so nothing honors ${unusable === 1 ? 'it' : 'them'}. ` +\n `The next change you make removes ${unusable === 1 ? 'it' : 'them'}.`}\n </Typography>\n ) : null}\n </Stack>\n {dialog && orgId ? (\n <ConsentGroupDialog\n open\n mode={dialog.mode}\n groupId={dialog.groupId}\n org={org}\n orgId={orgId}\n sites={sites}\n onClose={() => setDialog(null)}\n onApplied={({ changeId: applied, done }) => {\n setDialog(null)\n if (done) {\n announce(applied, 'done')\n return\n }\n setStarted(applied)\n }}\n />\n ) : null}\n </CardDisplay>\n )\n}\nConsentGroupsCard.displayName = 'ConsentGroupsCard'\n\nexport default ConsentGroupsCard\n"],"names":["pluginDocsHelp","mdiLinkVariantOff","mdiPencilOutline","CardDisplay","MdiIcon","RowActionsMenu","ScrollTable","useSnackbar","Button","Chip","Stack","TableBody","TableCell","TableHead","TableRow","Typography","useCallback","useEffect","useMemo","useRef","useState","useConsentGroupAccess","ConsentGroupChangeProgress","ConsentGroupDialog","draftDisclosure","listConsentGroups","unusableConsentGroupCount","CONSENT_GROUP_MIN_HOSTS","readConsentGroupChangeMarker","orgSiteName","useEmailOrgMount","blockedReason","input","access","siteCount","sitesReady","changing","ready","missing","ConsentGroupsCard","props","org","mount","orgId","enqueueSnackbar","own","undefined","allowed","groups","unusable","marker","sites","hosts","hostsReady","started","setStarted","changeId","announced","Set","announce","id","outcome","current","has","add","variant","persist","seenMarker","previous","dialog","setDialog","blocked","length","Boolean","canEdit","rowBlocked","changingHosts","hostIds","rowActions","groupId","key","label","icon","path","size","disabled","disabledReason","onClick","mode","destructive","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","color","canDrive","onFinished","align","map","group","sx","verticalAlign","alignItems","fontWeight","name","fontFamily","some","hostId","direction","useFlexGap","flexWrap","width","items","open","onClose","onApplied","applied","done","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,iBAAiB,EAAEC,gBAAgB,QAAQ,yBAAwB;AAC5E,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACzE,SAAkCC,qBAAqB,QAAQ,4BAAwB;AACvF,OAAOC,gCAAgC,qCAAiC;AACxE,OAAOC,wBAAyD,4BAAwB;AACxF,SACEC,eAAe,EACfC,iBAAiB,EACjBC,yBAAyB,QACpB,6BAAyB;AAChC,SACEC,uBAAuB,EACvBC,4BAA4B,QACvB,0BAAsB;AAC7B,SAASC,WAAW,EAAEC,gBAAgB,QAAQ,uBAAmB;AAYjE,6EAA6E,GAC7E,SAASC,cAAcC,KAKtB;IACC,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAEC,UAAU,EAAEC,QAAQ,EAAE,GAAGJ;IACpD,IAAI,CAACC,OAAOI,KAAK,EAAE,OAAO;IAC1B,IAAIJ,OAAOK,OAAO,EAAE;QAClB,OAAO,CAAC,aAAa,EAAEL,OAAOK,OAAO,CAAC,qCAAqC,CAAC;IAC9E;IACA,IAAIF,UAAU,OAAO;IACrB,IAAID,cAAcD,YAAYP,yBAAyB;QACrD,OAAO;IACT;IACA,OAAO;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASY,kBAAkBC,KAA6B;;IAC7D,MAAM,EAAEC,GAAG,EAAER,MAAM,EAAE,GAAGO;IACxB,MAAME,QAAQZ;IACd,MAAMa,gBAAQD,yBAAAA,MAAOC,KAAK,mBAAI;IAC9B,MAAM,EAAEC,eAAe,EAAE,GAAGrC;IAC5B,+DAA+D;IAC/D,MAAMsC,MAAMxB,sBAAsBY,SAASa,YAAYH,SAASG;IAChE,MAAMC,UAAUd,iBAAAA,SAAUY;IAE1B,MAAMG,SAAS9B,QAAQ,IAAMO,kBAAkBgB,MAAM;QAACA;KAAI;IAC1D,MAAMQ,WAAWvB,0BAA0Be;IAC3C,MAAMS,SAAStB,6BAA6Ba;IAC5C,MAAMU,iBAAQT,yBAAAA,MAAOU,KAAK,oBAAI,EAAE;IAChC,MAAMjB,sBAAaO,yBAAAA,MAAOW,UAAU,oBAAI;IAExC;;;;GAIC,GACD,MAAM,CAACC,SAASC,WAAW,GAAGnC,SAAwB;IACtDH,UAAU;QACR,IAAIqC,WAAWJ,CAAAA,0BAAAA,OAAQM,QAAQ,MAAKF,SAASC,WAAW;IAC1D,GAAG;QAACD;QAASJ,0BAAAA,OAAQM,QAAQ;KAAC;IAC9B,MAAMA,oBAAWN,0BAAAA,OAAQM,QAAQ,oBAAIF;IAErC;;;GAGC,GACD,MAAMG,YAAYtC,OAAO,IAAIuC;IAC7B,MAAMC,WAAW3C,YACf,CAAC4C,IAAYC;QACX,IAAIJ,UAAUK,OAAO,CAACC,GAAG,CAACH,KAAK;QAC/BH,UAAUK,OAAO,CAACE,GAAG,CAACJ;QACtB,IAAIC,YAAY,QAAQ;YACtBjB,gBAAgB,0BAA0B;gBAAEqB,SAAS;gBAAWC,SAAS;YAAM;QACjF;IACF,GACA;QAACtB;KAAgB;IAEnB,MAAMuB,aAAahD,OAAsB;IACzCF,UAAU;;QACR,MAAMmD,WAAWD,WAAWL,OAAO;QACnCK,WAAWL,OAAO,WAAGZ,0BAAAA,OAAQM,QAAQ,mBAAI;QACzC,IAAIY,YAAY,CAAClB,QAAQS,SAASS,UAAU;IAC9C,GAAG;QAAClB;QAAQS;KAAS;IAErB,MAAM,CAACU,QAAQC,UAAU,GAAGlD,SAGlB;IAEV,MAAMmD,UAAUxC,cAAc;QAC5BE,QAAQc;QACRb,WAAWiB,MAAMqB,MAAM;QACvBrC;QACAC,UAAUqC,QAAQjB;IACpB;IACA,MAAMkB,UAAU3B,QAAQV,KAAK,IAAI,CAACkC,WAAWE,QAAQ9B;IACrD,MAAMgC,aAAa5B,QAAQV,KAAK,GAAIkC,kBAAAA,UAAW,OAAQ;IACvD,MAAMK,gBAAgB,IAAIlB,aAAIR,0BAAAA,OAAQ2B,OAAO,oBAAI,EAAE;IAEnD,MAAMC,aAAa,CAACC,UAA0C;YAC5D;gBACEC,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAC9E;oBAAQ+E,MAAMjF,iBAAiBiF,IAAI;oBAAEC,MAAM;;gBAClDC,UAAU,CAACX;gBACXY,gBAAgBZ,UAAU5B,YAAa6B,qBAAAA,aAAc7B;gBACrDyC,SAAS,IAAMjB,UAAU;wBAAEkB,MAAM;wBAAQT;oBAAQ;YACnD;YACA;gBACEC,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAC9E;oBAAQ+E,MAAMlF,kBAAkBkF,IAAI;oBAAEC,MAAM;;gBACnDK,aAAa;gBACbJ,UAAU,CAACX;gBACXY,gBAAgBZ,UAAU5B,YAAa6B,qBAAAA,aAAc7B;gBACrDyC,SAAS,IAAMjB,UAAU;wBAAEkB,MAAM;wBAAYT;oBAAQ;YACvD;SACD;IAED,qBACE,MAAC5E;QACCuF,QAAO;QACPC,MAAM3F,eAAe,kBAAkB;YAAE4F,QAAQ;QAAkB;QACnEC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACxF;gBACCyD,SAAQ;gBACRoB,UAAU,CAACX;gBACXa,SAAS,IAAMjB,UAAU;wBAAEkB,MAAM;wBAAUT,SAAS;oBAAK;0BAExD;;QAGP;;0BAEA,MAACrE;gBAAMuF,SAAS;;kCACd,KAAClF;wBAAWkD,SAAQ;wBAAQiC,OAAM;kCAC/B,uEACC,qEACA,oEACA,sEACA;;oBAEH3B,wBACC,KAACxD;wBAAWkD,SAAQ;wBAAQiC,OAAM;kCAC/B3B;yBAED;oBACHf,YAAYb,sBACX,KAACrB;wBAECqB,OAAOA;wBACPa,UAAUA;wBACVN,QAAQA;wBACRiD,UAAUpD,QAAQV,KAAK,IAAI,CAACU,QAAQT,OAAO;wBAC3C8D,YAAY,CAACvC;4BACXF,SAASH,UAAUK;4BACnBN,WAAW;wBACb;uBARKC,YAUL;oBACHR,OAAOwB,MAAM,iBACZ,MAAClE;wBAAY8E,MAAK;;0CAChB,KAACvE;0CACC,cAAA,MAACC;;sDACC,KAACF;sDAAW;;sDACZ,KAACA;sDAAW;;sDACZ,KAACA;sDAAW;;sDACZ,KAACA;4CAAUyF,OAAM;;;;;0CAGrB,KAAC1F;0CACEqC,OAAOsD,GAAG,CAAC,CAACC;wCAqCJ/E;yDApCP,MAACV;;0DACC,KAACF;gDAAU4F,IAAI;oDAAEC,eAAe;gDAAM;0DACpC,cAAA,MAAC/F;oDAAMuF,SAAS;oDAAKO,IAAI;wDAAEE,YAAY;oDAAa;;sEAClD,KAAC3F;4DAAWkD,SAAQ;4DAAQuC,IAAI;gEAAEG,YAAY;4DAAI;sEAC/CJ,MAAMK,IAAI;;sEAMb,KAAC7F;4DACCkD,SAAQ;4DACRiC,OAAM;4DACNM,IAAI;gEAAEK,YAAY;4DAAY;sEAE7BN,MAAM3C,EAAE;;wDAEV2C,MAAM1B,OAAO,CAACiC,IAAI,CAAC,CAACC,SAAWnC,cAAcb,GAAG,CAACgD,yBAChD,KAACtG;4DAAK2E,MAAK;4DAAQc,OAAM;4DAAOjC,SAAQ;4DAAWgB,OAAM;6DACvD;;;;0DAGR,KAACrE;gDAAU4F,IAAI;oDAAEC,eAAe;gDAAM;0DACpC,cAAA,KAAC/F;oDAAMsG,WAAU;oDAAMf,SAAS;oDAAKgB,UAAU;oDAACT,IAAI;wDAAEU,UAAU;oDAAO;8DACpEX,MAAM1B,OAAO,CAACyB,GAAG,CAAC,CAACS,uBAClB,KAACtG;4DAEC2E,MAAK;4DACLnB,SAAQ;4DACRgB,OAAOpD,YAAYa,OAAOqE;2DAHrBA;;;0DAQb,KAACnG;gDAAU4F,IAAI;oDAAEC,eAAe;gDAAM;0DACpC,cAAA,KAAC1F;oDAAWkD,SAAQ;oDAAQiC,OAAM;+DAC/B1E,mBAAAA,gBAAgB+E,MAAMK,IAAI,EAAEL,MAAM1B,OAAO,EAAE0B,MAAM3C,EAAE,aAAnDpC,mBAAwD;;;0DAG7D,KAACZ;gDAAUyF,OAAM;gDAAQG,IAAI;oDAAEW,OAAO;oDAAIV,eAAe;gDAAM;0DAC7D,cAAA,KAACpG;oDAAe4E,OAAOsB,MAAMK,IAAI;oDAAEQ,OAAOtC,WAAWyB,MAAM3C,EAAE;;;;uCAxClD2C,MAAM3C,EAAE;;;;uCA+C7B,KAAC7C;wBAAWkD,SAAQ;wBAAQiC,OAAM;kCAC/B,sEACC,4DACA;;oBAGLjD,WAAW,kBACV,KAAClC;wBAAWkD,SAAQ;wBAAUiC,OAAM;kCACjC,GAAGjD,aAAa,IAAI,4BAA4B,GAAGA,SAAS,qBAAqB,CAAC,CAAC,CAAC,CAAC,GACpF,mEACA,CAAC,8CAA8C,EAAEA,aAAa,IAAI,OAAO,OAAO,EAAE,CAAC,GACnF,CAAC,iCAAiC,EAAEA,aAAa,IAAI,OAAO,OAAO,CAAC,CAAC;yBAEvE;;;YAELoB,UAAU1B,sBACT,KAACpB;gBACC8F,IAAI;gBACJ7B,MAAMnB,OAAOmB,IAAI;gBACjBT,SAASV,OAAOU,OAAO;gBACvBtC,KAAKA;gBACLE,OAAOA;gBACPQ,OAAOA;gBACPmE,SAAS,IAAMhD,UAAU;gBACzBiD,WAAW,CAAC,EAAE/D,UAAUgE,OAAO,EAAEC,IAAI,EAAE;oBACrCnD,UAAU;oBACV,IAAImD,MAAM;wBACR9D,SAAS6D,SAAS;wBAClB;oBACF;oBACAjE,WAAWiE;gBACb;iBAEA;;;AAGV;AACAjF,kBAAkBmF,WAAW,GAAG;AAEhC,eAAenF,kBAAiB"}
@@ -279,7 +279,7 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
279
279
  return /*#__PURE__*/ _jsxs(CardDisplay, {
280
280
  header: 'Templates',
281
281
  help: pluginDocsHelp('designedEmails', {
282
- anchor: '#create-a-template'
282
+ anchor: '#find-a-template'
283
283
  }),
284
284
  contentGutterX: true,
285
285
  contentGutterY: true,