@aglyn/plugins-email 1.0.0-beta.231 → 1.0.0-beta.232

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.231",
3
+ "version": "1.0.0-beta.232",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,21 +25,21 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.231",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.231",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.231",
31
- "@aglyn/shared-data-types": "1.0.0-beta.231",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.231",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.231",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
36
- "@aglyn/shared-ui-theme": "1.0.0-beta.231",
37
- "@aglyn/shared-util-email": "1.0.0-beta.231",
38
- "@aglyn/shared-util-http": "1.0.0-beta.231",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.231",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.231",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
28
+ "@aglyn/aglyn": "1.0.0-beta.232",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.232",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.232",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.232",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.232",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.232",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.232",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.232",
36
+ "@aglyn/shared-ui-theme": "1.0.0-beta.232",
37
+ "@aglyn/shared-util-email": "1.0.0-beta.232",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.232",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.232",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.232",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.232",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.232",
43
43
  "@swc/helpers": "0.5.23"
44
44
  },
45
45
  "peerDependencies": {
@@ -254,6 +254,10 @@ import { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_SWEEP_DELAY_MS, postCon
254
254
  canDrive && (stalled || halted || phase === 'carry') ? /*#__PURE__*/ _jsxs(Stack, {
255
255
  direction: "row",
256
256
  spacing: 1,
257
+ useFlexGap: true,
258
+ sx: {
259
+ flexWrap: 'wrap'
260
+ },
257
261
  children: [
258
262
  stalled || halted ? /*#__PURE__*/ _jsx(Button, {
259
263
  size: "small",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-change-progress.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 { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n Button,\n LinearProgress,\n Stack,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n postConsentGroups,\n readConsentGroupChangeJob,\n type ConsentGroupChangeMarker,\n type ConsentGroupChangePhase,\n type ConsentGroupsProgressResponse,\n} from './consent-groups-api'\n\n/**\n * The least time between two `continue` calls from one page. A call that\n * returns at once — the job is waiting on its sweep, or another runner holds\n * it — must not become a loop that asks again the instant it is answered.\n */\nexport const CONSENT_GROUP_CONTINUE_GAP_MS = 3_000\n\n/** What each phase is called while it runs, and what it means for the reader. */\nexport const CONSENT_GROUP_PHASE_TEXT: Readonly<\n Record<ConsentGroupChangePhase, { title: string; body: string }>\n> = {\n carry: {\n title: 'Copying opt-outs — nothing has changed yet',\n body:\n 'Opt-outs are being copied onto every site that will stop sharing them, ' +\n 'so nobody who opted out can be emailed after the change. Until this ' +\n 'finishes, your consent groups work exactly as they did.',\n },\n rehome: {\n title: 'In effect — combining CRM records',\n body:\n 'Every send already follows your new consent groups. The CRM records ' +\n 'these sites keep are being combined or copied, so some may show under ' +\n 'their old group for a little while.',\n },\n sweep: {\n title: 'Finishing up',\n body:\n 'A last pass for anything written while the change ran. You can make ' +\n 'another change once this finishes.',\n },\n}\n\nexport interface ConsentGroupChangeProgressProps {\n orgId: string\n changeId: string\n /** The org's marker for this change, when the org document carries it yet. */\n marker: ConsentGroupChangeMarker | null\n /**\n * The reader may drive and stop the change. Anyone else sees where it is,\n * and opens no read of the job for it.\n */\n canDrive: boolean\n /** The change finished, or was stopped before it took effect. */\n onFinished: (outcome: 'done' | 'canceled') => void\n}\n\n/**\n * A consent group change while it runs (AGL-3320).\n *\n * ## It finishes on its own; an open page only makes it faster\n *\n * The route works in slices of under a minute and a cron resumes it every\n * quarter hour, so closing the page never strands a change. While this is\n * mounted for somebody who may drive it, it asks for the next slice as soon\n * as nobody else holds the job — the job document's lease says when — so a\n * change the admin is watching finishes in minutes rather than quarter hours.\n *\n * ## Only the first phase can be stopped\n *\n * Before the flip nothing an admin would notice has changed: refusals were\n * copied, which never lets anybody be mailed who could not be before, and\n * the old declaration still holds. So **Stop before it takes effect** is\n * offered exactly then. After the flip the new declaration is what every\n * send reads, and stopping would leave the CRM's records half moved.\n *\n * ## A stall is shown, not hidden\n *\n * After five failures in a row the job stops retrying itself on every call\n * and records why. The banner shows that reason and **Retry now**; the cron\n * keeps trying either way.\n */\nexport function ConsentGroupChangeProgress(props: ConsentGroupChangeProgressProps) {\n const { orgId, changeId, marker, canDrive, onFinished } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n const jobRead = useFirestoreDoc<Record<string, unknown>>(\n () =>\n canDrive && orgId && changeId\n ? doc(firestore, 'orgs', orgId, CONSENT_GROUP_CHANGES_COLLECTION, changeId)\n : null,\n [firestore, orgId, changeId, canDrive],\n )\n const job = readConsentGroupChangeJob(\n jobRead.status === 'success' ? jobRead.data : undefined,\n )\n\n const [answer, setAnswer] = useState<ConsentGroupsProgressResponse | null>(null)\n const [busy, setBusy] = useState(false)\n const [stopping, setStopping] = useState(false)\n /** A refusal that stops this page driving until somebody retries. */\n const [halted, setHalted] = useState<string | null>(null)\n /** Re-arms the driver after each answer. */\n const [tick, setTick] = useState(0)\n const lastCallAt = useRef(0)\n const finished = useRef(false)\n const mounted = useRef(true)\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n const markerPhase =\n marker && marker.changeId === changeId ? marker.phase : null\n const answerPhase =\n answer && ['carry', 'rehome', 'sweep'].includes(String(answer.phase))\n ? (answer.phase as ConsentGroupChangePhase)\n : null\n const jobPhase = ['carry', 'rehome', 'sweep'].includes(String(job.phase))\n ? (job.phase as ConsentGroupChangePhase)\n : null\n const phase: ConsentGroupChangePhase =\n markerPhase ?? answerPhase ?? jobPhase ?? 'carry'\n const stalled = job.stalled || answer?.progress?.stalled === true\n const lastError = job.lastError ?? answer?.progress?.lastError ?? null\n\n const finish = useCallback(\n (outcome: 'done' | 'canceled') => {\n if (finished.current) return\n finished.current = true\n onFinished(outcome)\n },\n [onFinished],\n )\n\n // The job can finish without this page: the cron, or another tab.\n useEffect(() => {\n if (job.finished) finish(job.phase === 'canceled' ? 'canceled' : 'done')\n }, [job.finished, job.phase, finish])\n\n const drive = useCallback(async () => {\n if (!user || finished.current) return\n lastCallAt.current = Date.now()\n setBusy(true)\n const result = await postConsentGroups<ConsentGroupsProgressResponse>(user, {\n orgId,\n action: 'continue',\n changeId,\n })\n if (!mounted.current) return\n setBusy(false)\n if (result.ok) {\n setAnswer(result.body)\n if (result.body.done === true) return void finish('done')\n setTick((value) => value + 1)\n return\n }\n setHalted(result.failure.message)\n }, [user, orgId, changeId, finish])\n\n /*\n * The driver. It waits out the least gap between calls, any lease another\n * runner holds, and the delay before the sweep may run — then asks once.\n * Each answer re-arms it through `tick`; a lease renewed under it re-arms\n * it through the job document.\n */\n useEffect(() => {\n if (!canDrive || !user || finished.current) return\n if (busy || stopping || stalled || halted) return\n if (jobRead.status === 'loading') return\n const now = Date.now()\n let at = Math.max(now, lastCallAt.current + CONSENT_GROUP_CONTINUE_GAP_MS)\n if (job.leaseUntilMs != null && job.leaseUntilMs > now) {\n at = Math.max(at, job.leaseUntilMs + 1_000)\n }\n if (phase === 'sweep' && marker?.declaredAtMs) {\n at = Math.max(at, marker.declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS)\n }\n const timer = setTimeout(() => void drive(), at - now)\n return () => clearTimeout(timer)\n }, [\n canDrive,\n user,\n busy,\n stopping,\n stalled,\n halted,\n jobRead.status,\n job.leaseUntilMs,\n phase,\n marker?.declaredAtMs,\n drive,\n tick,\n ])\n\n const retry = () => {\n setHalted(null)\n lastCallAt.current = 0\n void drive()\n }\n\n const stop = async () => {\n if (!user || stopping) return\n const accepted = await confirm({\n title: 'Stop this change?',\n description:\n 'Nothing has taken effect yet, so your consent groups stay exactly as ' +\n 'they are. Opt-outs already copied stay in place — a copied opt-out ' +\n 'never lets anyone be emailed who couldn’t be before.',\n confirmationText: 'Stop the change',\n cancellationText: 'Keep going',\n })\n // `confirm` resolves with no value and REJECTS on cancel (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setStopping(true)\n const result = await postConsentGroups(user, {\n orgId,\n action: 'cancel',\n changeId,\n })\n if (!mounted.current) return\n setStopping(false)\n if (result.ok) {\n enqueueSnackbar('Stopped. Your consent groups are unchanged.', {\n variant: 'success',\n persist: false,\n })\n return void finish('canceled')\n }\n enqueueSnackbar(result.failure.message, {\n variant: result.failure.kind === 'took-effect' ? 'info' : 'error',\n allowDuplicate: true,\n })\n }\n\n const text = CONSENT_GROUP_PHASE_TEXT[phase]\n return (\n <Alert\n severity={stalled || halted ? 'error' : 'info'}\n data-testid=\"consent-group-change-progress\"\n sx={{ '& .MuiAlert-message': { width: '100%' } }}\n >\n <AlertTitle>{text.title}</AlertTitle>\n <Stack spacing={1}>\n <Typography variant=\"body2\">{text.body}</Typography>\n {stalled ? (\n <Typography variant=\"body2\">\n {`This change stopped after repeated errors${\n lastError ? `: ${lastError}` : '.'\n } It keeps trying on its own every 15 minutes, or you can retry now.`}\n </Typography>\n ) : null}\n {halted ? <Typography variant=\"body2\">{halted}</Typography> : null}\n {!canDrive ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'It finishes on its own. Only members who can change consent groups can stop it.'}\n </Typography>\n ) : null}\n {!stalled && !halted ? (\n <LinearProgress aria-label=\"Consent group change in progress\" />\n ) : null}\n {canDrive && (stalled || halted || phase === 'carry') ? (\n <Stack direction=\"row\" spacing={1}>\n {stalled || halted ? (\n <Button size=\"small\" variant=\"contained\" onClick={retry} disabled={busy}>\n {'Retry now'}\n </Button>\n ) : null}\n {phase === 'carry' ? (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={() => void stop()}\n disabled={stopping}\n >\n {'Stop before it takes effect'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </Alert>\n )\n}\nConsentGroupChangeProgress.displayName = 'ConsentGroupChangeProgress'\n\nexport default ConsentGroupChangeProgress\n"],"names":["useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","useUser","Alert","AlertTitle","Button","LinearProgress","Stack","Typography","doc","useCallback","useEffect","useRef","useState","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_SWEEP_DELAY_MS","postConsentGroups","readConsentGroupChangeJob","CONSENT_GROUP_CONTINUE_GAP_MS","CONSENT_GROUP_PHASE_TEXT","carry","title","body","rehome","sweep","ConsentGroupChangeProgress","props","markerPhase","job","answer","orgId","changeId","marker","canDrive","onFinished","firestore","data","user","enqueueSnackbar","confirm","jobRead","status","undefined","setAnswer","busy","setBusy","stopping","setStopping","halted","setHalted","tick","setTick","lastCallAt","finished","mounted","current","phase","answerPhase","includes","String","jobPhase","stalled","progress","lastError","finish","outcome","drive","Date","now","result","action","ok","done","value","failure","message","at","Math","max","leaseUntilMs","declaredAtMs","timer","setTimeout","clearTimeout","retry","stop","accepted","description","confirmationText","cancellationText","then","catch","variant","persist","kind","allowDuplicate","text","severity","data-testid","sx","width","spacing","color","aria-label","direction","size","onClick","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-change-progress.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 { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n Button,\n LinearProgress,\n Stack,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n postConsentGroups,\n readConsentGroupChangeJob,\n type ConsentGroupChangeMarker,\n type ConsentGroupChangePhase,\n type ConsentGroupsProgressResponse,\n} from './consent-groups-api'\n\n/**\n * The least time between two `continue` calls from one page. A call that\n * returns at once — the job is waiting on its sweep, or another runner holds\n * it — must not become a loop that asks again the instant it is answered.\n */\nexport const CONSENT_GROUP_CONTINUE_GAP_MS = 3_000\n\n/** What each phase is called while it runs, and what it means for the reader. */\nexport const CONSENT_GROUP_PHASE_TEXT: Readonly<\n Record<ConsentGroupChangePhase, { title: string; body: string }>\n> = {\n carry: {\n title: 'Copying opt-outs — nothing has changed yet',\n body:\n 'Opt-outs are being copied onto every site that will stop sharing them, ' +\n 'so nobody who opted out can be emailed after the change. Until this ' +\n 'finishes, your consent groups work exactly as they did.',\n },\n rehome: {\n title: 'In effect — combining CRM records',\n body:\n 'Every send already follows your new consent groups. The CRM records ' +\n 'these sites keep are being combined or copied, so some may show under ' +\n 'their old group for a little while.',\n },\n sweep: {\n title: 'Finishing up',\n body:\n 'A last pass for anything written while the change ran. You can make ' +\n 'another change once this finishes.',\n },\n}\n\nexport interface ConsentGroupChangeProgressProps {\n orgId: string\n changeId: string\n /** The org's marker for this change, when the org document carries it yet. */\n marker: ConsentGroupChangeMarker | null\n /**\n * The reader may drive and stop the change. Anyone else sees where it is,\n * and opens no read of the job for it.\n */\n canDrive: boolean\n /** The change finished, or was stopped before it took effect. */\n onFinished: (outcome: 'done' | 'canceled') => void\n}\n\n/**\n * A consent group change while it runs (AGL-3320).\n *\n * ## It finishes on its own; an open page only makes it faster\n *\n * The route works in slices of under a minute and a cron resumes it every\n * quarter hour, so closing the page never strands a change. While this is\n * mounted for somebody who may drive it, it asks for the next slice as soon\n * as nobody else holds the job — the job document's lease says when — so a\n * change the admin is watching finishes in minutes rather than quarter hours.\n *\n * ## Only the first phase can be stopped\n *\n * Before the flip nothing an admin would notice has changed: refusals were\n * copied, which never lets anybody be mailed who could not be before, and\n * the old declaration still holds. So **Stop before it takes effect** is\n * offered exactly then. After the flip the new declaration is what every\n * send reads, and stopping would leave the CRM's records half moved.\n *\n * ## A stall is shown, not hidden\n *\n * After five failures in a row the job stops retrying itself on every call\n * and records why. The banner shows that reason and **Retry now**; the cron\n * keeps trying either way.\n */\nexport function ConsentGroupChangeProgress(props: ConsentGroupChangeProgressProps) {\n const { orgId, changeId, marker, canDrive, onFinished } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n const jobRead = useFirestoreDoc<Record<string, unknown>>(\n () =>\n canDrive && orgId && changeId\n ? doc(firestore, 'orgs', orgId, CONSENT_GROUP_CHANGES_COLLECTION, changeId)\n : null,\n [firestore, orgId, changeId, canDrive],\n )\n const job = readConsentGroupChangeJob(\n jobRead.status === 'success' ? jobRead.data : undefined,\n )\n\n const [answer, setAnswer] = useState<ConsentGroupsProgressResponse | null>(null)\n const [busy, setBusy] = useState(false)\n const [stopping, setStopping] = useState(false)\n /** A refusal that stops this page driving until somebody retries. */\n const [halted, setHalted] = useState<string | null>(null)\n /** Re-arms the driver after each answer. */\n const [tick, setTick] = useState(0)\n const lastCallAt = useRef(0)\n const finished = useRef(false)\n const mounted = useRef(true)\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n const markerPhase =\n marker && marker.changeId === changeId ? marker.phase : null\n const answerPhase =\n answer && ['carry', 'rehome', 'sweep'].includes(String(answer.phase))\n ? (answer.phase as ConsentGroupChangePhase)\n : null\n const jobPhase = ['carry', 'rehome', 'sweep'].includes(String(job.phase))\n ? (job.phase as ConsentGroupChangePhase)\n : null\n const phase: ConsentGroupChangePhase =\n markerPhase ?? answerPhase ?? jobPhase ?? 'carry'\n const stalled = job.stalled || answer?.progress?.stalled === true\n const lastError = job.lastError ?? answer?.progress?.lastError ?? null\n\n const finish = useCallback(\n (outcome: 'done' | 'canceled') => {\n if (finished.current) return\n finished.current = true\n onFinished(outcome)\n },\n [onFinished],\n )\n\n // The job can finish without this page: the cron, or another tab.\n useEffect(() => {\n if (job.finished) finish(job.phase === 'canceled' ? 'canceled' : 'done')\n }, [job.finished, job.phase, finish])\n\n const drive = useCallback(async () => {\n if (!user || finished.current) return\n lastCallAt.current = Date.now()\n setBusy(true)\n const result = await postConsentGroups<ConsentGroupsProgressResponse>(user, {\n orgId,\n action: 'continue',\n changeId,\n })\n if (!mounted.current) return\n setBusy(false)\n if (result.ok) {\n setAnswer(result.body)\n if (result.body.done === true) return void finish('done')\n setTick((value) => value + 1)\n return\n }\n setHalted(result.failure.message)\n }, [user, orgId, changeId, finish])\n\n /*\n * The driver. It waits out the least gap between calls, any lease another\n * runner holds, and the delay before the sweep may run — then asks once.\n * Each answer re-arms it through `tick`; a lease renewed under it re-arms\n * it through the job document.\n */\n useEffect(() => {\n if (!canDrive || !user || finished.current) return\n if (busy || stopping || stalled || halted) return\n if (jobRead.status === 'loading') return\n const now = Date.now()\n let at = Math.max(now, lastCallAt.current + CONSENT_GROUP_CONTINUE_GAP_MS)\n if (job.leaseUntilMs != null && job.leaseUntilMs > now) {\n at = Math.max(at, job.leaseUntilMs + 1_000)\n }\n if (phase === 'sweep' && marker?.declaredAtMs) {\n at = Math.max(at, marker.declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS)\n }\n const timer = setTimeout(() => void drive(), at - now)\n return () => clearTimeout(timer)\n }, [\n canDrive,\n user,\n busy,\n stopping,\n stalled,\n halted,\n jobRead.status,\n job.leaseUntilMs,\n phase,\n marker?.declaredAtMs,\n drive,\n tick,\n ])\n\n const retry = () => {\n setHalted(null)\n lastCallAt.current = 0\n void drive()\n }\n\n const stop = async () => {\n if (!user || stopping) return\n const accepted = await confirm({\n title: 'Stop this change?',\n description:\n 'Nothing has taken effect yet, so your consent groups stay exactly as ' +\n 'they are. Opt-outs already copied stay in place — a copied opt-out ' +\n 'never lets anyone be emailed who couldn’t be before.',\n confirmationText: 'Stop the change',\n cancellationText: 'Keep going',\n })\n // `confirm` resolves with no value and REJECTS on cancel (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setStopping(true)\n const result = await postConsentGroups(user, {\n orgId,\n action: 'cancel',\n changeId,\n })\n if (!mounted.current) return\n setStopping(false)\n if (result.ok) {\n enqueueSnackbar('Stopped. Your consent groups are unchanged.', {\n variant: 'success',\n persist: false,\n })\n return void finish('canceled')\n }\n enqueueSnackbar(result.failure.message, {\n variant: result.failure.kind === 'took-effect' ? 'info' : 'error',\n allowDuplicate: true,\n })\n }\n\n const text = CONSENT_GROUP_PHASE_TEXT[phase]\n return (\n <Alert\n severity={stalled || halted ? 'error' : 'info'}\n data-testid=\"consent-group-change-progress\"\n sx={{ '& .MuiAlert-message': { width: '100%' } }}\n >\n <AlertTitle>{text.title}</AlertTitle>\n <Stack spacing={1}>\n <Typography variant=\"body2\">{text.body}</Typography>\n {stalled ? (\n <Typography variant=\"body2\">\n {`This change stopped after repeated errors${\n lastError ? `: ${lastError}` : '.'\n } It keeps trying on its own every 15 minutes, or you can retry now.`}\n </Typography>\n ) : null}\n {halted ? <Typography variant=\"body2\">{halted}</Typography> : null}\n {!canDrive ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'It finishes on its own. Only members who can change consent groups can stop it.'}\n </Typography>\n ) : null}\n {!stalled && !halted ? (\n <LinearProgress aria-label=\"Consent group change in progress\" />\n ) : null}\n {canDrive && (stalled || halted || phase === 'carry') ? (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n {stalled || halted ? (\n <Button size=\"small\" variant=\"contained\" onClick={retry} disabled={busy}>\n {'Retry now'}\n </Button>\n ) : null}\n {phase === 'carry' ? (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={() => void stop()}\n disabled={stopping}\n >\n {'Stop before it takes effect'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </Alert>\n )\n}\nConsentGroupChangeProgress.displayName = 'ConsentGroupChangeProgress'\n\nexport default ConsentGroupChangeProgress\n"],"names":["useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","useUser","Alert","AlertTitle","Button","LinearProgress","Stack","Typography","doc","useCallback","useEffect","useRef","useState","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_SWEEP_DELAY_MS","postConsentGroups","readConsentGroupChangeJob","CONSENT_GROUP_CONTINUE_GAP_MS","CONSENT_GROUP_PHASE_TEXT","carry","title","body","rehome","sweep","ConsentGroupChangeProgress","props","markerPhase","job","answer","orgId","changeId","marker","canDrive","onFinished","firestore","data","user","enqueueSnackbar","confirm","jobRead","status","undefined","setAnswer","busy","setBusy","stopping","setStopping","halted","setHalted","tick","setTick","lastCallAt","finished","mounted","current","phase","answerPhase","includes","String","jobPhase","stalled","progress","lastError","finish","outcome","drive","Date","now","result","action","ok","done","value","failure","message","at","Math","max","leaseUntilMs","declaredAtMs","timer","setTimeout","clearTimeout","retry","stop","accepted","description","confirmationText","cancellationText","then","catch","variant","persist","kind","allowDuplicate","text","severity","data-testid","sx","width","spacing","color","aria-label","direction","useFlexGap","flexWrap","size","onClick","disabled","displayName"],"mappings":"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"}
@@ -125,6 +125,7 @@ import { MAX_CONSENT_GROUP_HOSTS } from "./consent-groups-api.js";
125
125
  const checked = chosen.has(row.id);
126
126
  const other = memberOf.get(row.id);
127
127
  return /*#__PURE__*/ _jsxs(Stack, {
128
+ useFlexGap: true,
128
129
  direction: "row",
129
130
  spacing: 1,
130
131
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-site-picker.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 type { ConsolePluginOrgHost } from '@aglyn/aglyn'\nimport {\n Box,\n Checkbox,\n Chip,\n FormControl,\n FormControlLabel,\n FormHelperText,\n FormLabel,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { ListedConsentGroup } from './consent-group-editing'\nimport { MAX_CONSENT_GROUP_HOSTS } from './consent-groups-api'\n\n/** Past this many sites the list gets a filter, so a long one stays usable. */\nconst FILTER_FROM = 10\n\nexport interface ConsentGroupSitePickerProps {\n /** Every site the organization has. */\n sites: readonly ConsolePluginOrgHost[]\n /** The groups the org declares now. */\n groups: readonly ListedConsentGroup[]\n /** The group being edited, whose own sites carry no badge; `null` for a new one. */\n groupId: string | null\n selected: readonly string[]\n onChange: (hostIds: string[]) => void\n /** What is wrong with the selection, said under the list. */\n error?: string\n /** Sites the route refused by id, marked where they are listed. */\n refusedHostIds?: ReadonlySet<string>\n disabled?: boolean\n}\n\n/**\n * The sites a consent group names.\n *\n * A site can be one sender with only one set of siblings, so a site already\n * in another group says which — \"In Acme\" — and, once ticked, what ticking it\n * does: \"Moves from Acme\". The move is legitimate, and the review spells out\n * what it costs the group it leaves; the badge is there so nobody makes it\n * without seeing it.\n *\n * A site the group names that the org no longer lists is still shown, ticked\n * and marked, so it can be unticked rather than silently carried into the\n * next declaration.\n */\nexport function ConsentGroupSitePicker(props: ConsentGroupSitePickerProps) {\n const {\n sites,\n groups,\n groupId,\n selected,\n onChange,\n error,\n refusedHostIds,\n disabled,\n } = props\n const [filter, setFilter] = useState('')\n\n /** The group each site belongs to now, other than the one being edited. */\n const memberOf = useMemo(() => {\n const found = new Map<string, ListedConsentGroup>()\n for (const group of groups) {\n if (group.id === groupId) continue\n for (const hostId of group.hostIds) found.set(hostId, group)\n }\n return found\n }, [groups, groupId])\n\n const rows = useMemo(() => {\n const listed = sites.map((site) => ({\n id: site.id,\n name: site.name || site.subdomain || site.id,\n missing: false,\n }))\n const known = new Set(listed.map((site) => site.id))\n const own = groups.find((group) => group.id === groupId)?.hostIds ?? []\n const orphans = own\n .filter((hostId) => !known.has(hostId))\n .map((hostId) => ({ id: hostId, name: hostId, missing: true }))\n return [...listed, ...orphans].sort((a, b) => a.name.localeCompare(b.name))\n }, [sites, groups, groupId])\n\n const chosen = useMemo(() => new Set(selected), [selected])\n const full = chosen.size >= MAX_CONSENT_GROUP_HOSTS\n const needle = filter.trim().toLocaleLowerCase()\n const visible = needle\n ? rows.filter(\n (row) => chosen.has(row.id) || row.name.toLocaleLowerCase().includes(needle),\n )\n : rows\n\n const toggle = (hostId: string, on: boolean) => {\n const next = new Set(chosen)\n if (on) next.add(hostId)\n else next.delete(hostId)\n onChange([...next].sort())\n }\n\n return (\n <FormControl component=\"fieldset\" error={Boolean(error)} disabled={disabled} fullWidth>\n <FormLabel component=\"legend\">{'Sites'}</FormLabel>\n <FormHelperText sx={{ mx: 0 }}>\n {`Choose at least two sites and at most ${MAX_CONSENT_GROUP_HOSTS}. ` +\n 'A site can be in only one consent group.'}\n </FormHelperText>\n {rows.length > FILTER_FROM ? (\n <TextField\n size=\"small\"\n label=\"Find a site\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n sx={{ mt: 1, maxWidth: 320 }}\n />\n ) : null}\n <Box\n sx={{\n mt: 1,\n maxHeight: 320,\n overflowY: 'auto',\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n px: 1.5,\n py: 0.5,\n }}\n >\n {visible.map((row) => {\n const checked = chosen.has(row.id)\n const other = memberOf.get(row.id)\n return (\n <Stack\n key={row.id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={checked}\n disabled={disabled || (!checked && full)}\n onChange={(event) => toggle(row.id, event.target.checked)}\n />\n }\n label={row.name}\n sx={{ mr: 0 }}\n />\n {other ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={checked ? 'warning' : 'default'}\n label={checked ? `Moves from ${other.name}` : `In ${other.name}`}\n />\n ) : null}\n {row.missing ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Not in your organization\"\n />\n ) : null}\n {refusedHostIds?.has(row.id) ? (\n <Chip size=\"small\" color=\"error\" variant=\"outlined\" label=\"Can’t be added\" />\n ) : null}\n </Stack>\n )\n })}\n {!visible.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ py: 1 }}>\n {rows.length ? 'No site matches.' : 'Your organization has no sites yet.'}\n </Typography>\n ) : null}\n </Box>\n <FormHelperText sx={{ mx: 0 }}>\n {error ??\n (full\n ? `That’s the most one group can include (${MAX_CONSENT_GROUP_HOSTS}).`\n : `${chosen.size} selected`)}\n </FormHelperText>\n </FormControl>\n )\n}\nConsentGroupSitePicker.displayName = 'ConsentGroupSitePicker'\n\nexport default ConsentGroupSitePicker\n"],"names":["Box","Checkbox","Chip","FormControl","FormControlLabel","FormHelperText","FormLabel","Stack","TextField","Typography","useMemo","useState","MAX_CONSENT_GROUP_HOSTS","FILTER_FROM","ConsentGroupSitePicker","props","sites","groups","groupId","selected","onChange","error","refusedHostIds","disabled","filter","setFilter","memberOf","found","Map","group","id","hostId","hostIds","set","rows","listed","map","site","name","subdomain","missing","known","Set","own","find","orphans","has","sort","a","b","localeCompare","chosen","full","size","needle","trim","toLocaleLowerCase","visible","row","includes","toggle","on","next","add","delete","component","Boolean","fullWidth","sx","mx","length","label","value","event","target","mt","maxWidth","maxHeight","overflowY","border","borderColor","borderRadius","px","py","checked","other","get","direction","spacing","alignItems","flexWrap","control","mr","variant","color","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-site-picker.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 type { ConsolePluginOrgHost } from '@aglyn/aglyn'\nimport {\n Box,\n Checkbox,\n Chip,\n FormControl,\n FormControlLabel,\n FormHelperText,\n FormLabel,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { ListedConsentGroup } from './consent-group-editing'\nimport { MAX_CONSENT_GROUP_HOSTS } from './consent-groups-api'\n\n/** Past this many sites the list gets a filter, so a long one stays usable. */\nconst FILTER_FROM = 10\n\nexport interface ConsentGroupSitePickerProps {\n /** Every site the organization has. */\n sites: readonly ConsolePluginOrgHost[]\n /** The groups the org declares now. */\n groups: readonly ListedConsentGroup[]\n /** The group being edited, whose own sites carry no badge; `null` for a new one. */\n groupId: string | null\n selected: readonly string[]\n onChange: (hostIds: string[]) => void\n /** What is wrong with the selection, said under the list. */\n error?: string\n /** Sites the route refused by id, marked where they are listed. */\n refusedHostIds?: ReadonlySet<string>\n disabled?: boolean\n}\n\n/**\n * The sites a consent group names.\n *\n * A site can be one sender with only one set of siblings, so a site already\n * in another group says which — \"In Acme\" — and, once ticked, what ticking it\n * does: \"Moves from Acme\". The move is legitimate, and the review spells out\n * what it costs the group it leaves; the badge is there so nobody makes it\n * without seeing it.\n *\n * A site the group names that the org no longer lists is still shown, ticked\n * and marked, so it can be unticked rather than silently carried into the\n * next declaration.\n */\nexport function ConsentGroupSitePicker(props: ConsentGroupSitePickerProps) {\n const {\n sites,\n groups,\n groupId,\n selected,\n onChange,\n error,\n refusedHostIds,\n disabled,\n } = props\n const [filter, setFilter] = useState('')\n\n /** The group each site belongs to now, other than the one being edited. */\n const memberOf = useMemo(() => {\n const found = new Map<string, ListedConsentGroup>()\n for (const group of groups) {\n if (group.id === groupId) continue\n for (const hostId of group.hostIds) found.set(hostId, group)\n }\n return found\n }, [groups, groupId])\n\n const rows = useMemo(() => {\n const listed = sites.map((site) => ({\n id: site.id,\n name: site.name || site.subdomain || site.id,\n missing: false,\n }))\n const known = new Set(listed.map((site) => site.id))\n const own = groups.find((group) => group.id === groupId)?.hostIds ?? []\n const orphans = own\n .filter((hostId) => !known.has(hostId))\n .map((hostId) => ({ id: hostId, name: hostId, missing: true }))\n return [...listed, ...orphans].sort((a, b) => a.name.localeCompare(b.name))\n }, [sites, groups, groupId])\n\n const chosen = useMemo(() => new Set(selected), [selected])\n const full = chosen.size >= MAX_CONSENT_GROUP_HOSTS\n const needle = filter.trim().toLocaleLowerCase()\n const visible = needle\n ? rows.filter(\n (row) => chosen.has(row.id) || row.name.toLocaleLowerCase().includes(needle),\n )\n : rows\n\n const toggle = (hostId: string, on: boolean) => {\n const next = new Set(chosen)\n if (on) next.add(hostId)\n else next.delete(hostId)\n onChange([...next].sort())\n }\n\n return (\n <FormControl component=\"fieldset\" error={Boolean(error)} disabled={disabled} fullWidth>\n <FormLabel component=\"legend\">{'Sites'}</FormLabel>\n <FormHelperText sx={{ mx: 0 }}>\n {`Choose at least two sites and at most ${MAX_CONSENT_GROUP_HOSTS}. ` +\n 'A site can be in only one consent group.'}\n </FormHelperText>\n {rows.length > FILTER_FROM ? (\n <TextField\n size=\"small\"\n label=\"Find a site\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n sx={{ mt: 1, maxWidth: 320 }}\n />\n ) : null}\n <Box\n sx={{\n mt: 1,\n maxHeight: 320,\n overflowY: 'auto',\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n px: 1.5,\n py: 0.5,\n }}\n >\n {visible.map((row) => {\n const checked = chosen.has(row.id)\n const other = memberOf.get(row.id)\n return (\n <Stack\n useFlexGap\n key={row.id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={checked}\n disabled={disabled || (!checked && full)}\n onChange={(event) => toggle(row.id, event.target.checked)}\n />\n }\n label={row.name}\n sx={{ mr: 0 }}\n />\n {other ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={checked ? 'warning' : 'default'}\n label={checked ? `Moves from ${other.name}` : `In ${other.name}`}\n />\n ) : null}\n {row.missing ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Not in your organization\"\n />\n ) : null}\n {refusedHostIds?.has(row.id) ? (\n <Chip size=\"small\" color=\"error\" variant=\"outlined\" label=\"Can’t be added\" />\n ) : null}\n </Stack>\n )\n })}\n {!visible.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ py: 1 }}>\n {rows.length ? 'No site matches.' : 'Your organization has no sites yet.'}\n </Typography>\n ) : null}\n </Box>\n <FormHelperText sx={{ mx: 0 }}>\n {error ??\n (full\n ? `That’s the most one group can include (${MAX_CONSENT_GROUP_HOSTS}).`\n : `${chosen.size} selected`)}\n </FormHelperText>\n </FormControl>\n )\n}\nConsentGroupSitePicker.displayName = 'ConsentGroupSitePicker'\n\nexport default ConsentGroupSitePicker\n"],"names":["Box","Checkbox","Chip","FormControl","FormControlLabel","FormHelperText","FormLabel","Stack","TextField","Typography","useMemo","useState","MAX_CONSENT_GROUP_HOSTS","FILTER_FROM","ConsentGroupSitePicker","props","sites","groups","groupId","selected","onChange","error","refusedHostIds","disabled","filter","setFilter","memberOf","found","Map","group","id","hostId","hostIds","set","rows","listed","map","site","name","subdomain","missing","known","Set","own","find","orphans","has","sort","a","b","localeCompare","chosen","full","size","needle","trim","toLocaleLowerCase","visible","row","includes","toggle","on","next","add","delete","component","Boolean","fullWidth","sx","mx","length","label","value","event","target","mt","maxWidth","maxHeight","overflowY","border","borderColor","borderRadius","px","py","checked","other","get","useFlexGap","direction","spacing","alignItems","flexWrap","control","mr","variant","color","displayName"],"mappings":"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"}
@@ -151,8 +151,10 @@ const templateDocsHelp = pluginDocsHelp('designedEmails', {
151
151
  */ const headerActions = /*#__PURE__*/ _jsxs(Stack, {
152
152
  direction: "row",
153
153
  spacing: 1,
154
+ useFlexGap: true,
154
155
  sx: {
155
- alignItems: 'center'
156
+ alignItems: 'center',
157
+ flexWrap: 'wrap'
156
158
  },
157
159
  children: [
158
160
  /*#__PURE__*/ _jsx(Button, _extends({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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 buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { templateProvenance } from '../model/template-provenance'\nimport EmailDesignPreview from './email-design-preview'\nimport {\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './email-zones'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'The template as an inbox receives it, drawn by the same renderer the ' +\n 'send path uses. Merge tokens are left standing — a real send fills them ' +\n 'from each recipient.',\n})\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'One email template: its name, where it came from, and what it looks ' +\n 'like. The emails sent from it are reported beneath, by the plugin that ' +\n 'sends them.',\n})\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document and its published version. The messages sent from the\n * template are a send's, and sends are the campaign owner's: this page hosts\n * two zones that plugin fills — the report of what those messages did, and\n * who received them — handing over the site, the template and this page's\n * base path, and reads none of the sends itself (AGL-3080).\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const Zone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n </Stack>\n </CardDisplay>\n\n {/*\n What the emails sent from this template did, then who received them:\n each its own boundary, because each card's code arrives after the\n card above it has drawn and must not send it back to a loading state.\n */}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_REPORT_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n basePath={basePath}\n />\n </Suspense>\n ) : null}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","MdiIcon","useConsoleHostRoute","useFirestore","useFirestoreDoc","Alert","Button","Stack","TextField","Typography","doc","updateDoc","Suspense","useMemo","useState","useSnackbar","templateProvenance","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","Section","useConsoleWidgetSlot","previewDocsHelp","anchor","excerpt","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","Zone","firestore","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","provenance","listUrl","besignerUrl","SCREEN_BESIGNER","host","headerActions","direction","spacing","sx","alignItems","component","componentVariant","nativeButton","href","size","color","startIcon","path","disabled","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","undefined","note","severity","warn","label","value","String","onChange","event","target","minWidth","helperText","onClick","fallback","slot","id","nodes","loading","Boolean","subject","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-template-detail.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 buildRoute,\n PageHeaderRecord,\n pluginDocsHelp,\n Route,\n} from '@aglyn/aglyn'\nimport { artifactRenameListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { ICON_VARIANT_BESIGNER } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreDoc,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { Suspense, useMemo, useState } from 'react'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { templateProvenance } from '../model/template-provenance'\nimport EmailDesignPreview from './email-design-preview'\nimport {\n EMAIL_TEMPLATE_RECIPIENTS_ZONE,\n EMAIL_TEMPLATE_REPORT_ZONE,\n} from './email-zones'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\nconst previewDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'The template as an inbox receives it, drawn by the same renderer the ' +\n 'send path uses. Merge tokens are left standing — a real send fills them ' +\n 'from each recipient.',\n})\n\nconst templateDocsHelp = pluginDocsHelp('designedEmails', {\n anchor: '#send-it',\n excerpt:\n 'One email template: its name, where it came from, and what it looks ' +\n 'like. The emails sent from it are reported beneath, by the plugin that ' +\n 'sends them.',\n})\n\nexport interface EmailTemplateDetailProps {\n hostId: string\n /** The `kind: 'email'` screen document this page is about. */\n screenId: string\n /** The emails hub URL, for the way back to the templates list. */\n basePath: string\n}\n\n/**\n * ONE EMAIL TEMPLATE: what it looks like, and what it has done.\n *\n * ## What was missing\n *\n * A template had a row in a list and no page. The only way to see one was to\n * open the besigner, which is the editor — it draws canvas nodes in a\n * browser, not the HTML an inbox receives — and the only way to see how it\n * performed was to find each message sent from it and read those reports one\n * at a time. A template used by six messages had its engagement in six places\n * and nowhere.\n *\n * ## Not necessarily this org's template\n *\n * A template can be installed from a marketplace listing, in which case its\n * content is versioned by somebody else and can be withdrawn. Provenance and\n * standing are read off the document itself — see `template-provenance.ts` —\n * so this page costs no marketplace read and still never presents an\n * installed template as locally authored.\n *\n * ## What this page reads\n *\n * The screen document and its published version. The messages sent from the\n * template are a send's, and sends are the campaign owner's: this page hosts\n * two zones that plugin fills — the report of what those messages did, and\n * who received them — handing over the site, the template and this page's\n * base path, and reads none of the sends itself (AGL-3080).\n */\nexport function EmailTemplateDetail(props: EmailTemplateDetailProps) {\n const { hostId, screenId, basePath } = props\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const Zone = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n\n /*==========================================\n * RENAMING THE DESIGN, on its own page.\n *\n * The name is the only thing that tells one design from another in the\n * campaign composer's picker, and until now nothing could change it — so a\n * site accumulated indistinguishable \"Untitled email\" rows. Edit lives\n * here, on the record's detail page, like every other record edit.\n *\n * `null` means \"not editing\": the field shows the live document's name\n * until the reader types, so an untouched field never writes a stale seed\n * back — only a value the reader actually entered is ever saved, and only\n * the one field.\n *=========================================*/\n const [nameDraft, setNameDraft] = useState<string | null>(null)\n const [renaming, setRenaming] = useState(false)\n const handleRename = async () => {\n const next = (nameDraft ?? '').trim()\n if (!next || renaming) return\n setRenaming(true)\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'screens', screenId), {\n displayName: next,\n // The keys the design lists find it by (AGL-3321).\n ...artifactRenameListKeys('screens', next),\n })\n setNameDraft(null)\n enqueueSnackbar('Design renamed', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The design could not be renamed', { variant: 'error' })\n } finally {\n setRenaming(false)\n }\n }\n\n const { data: screen, status } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'screens', screenId),\n [firestore, hostId, screenId],\n { idField: '$id' },\n )\n // Three states, not two: a document still loading and one that does not\n // exist both arrive as `undefined`, and rendering an empty report for the\n // second is how a mistyped id reads as a template nobody ever sent.\n const notFound = status !== 'loading' && !screen\n const versionId: string | undefined = screen?.versionId\n\n const { data: version } = useFirestoreDoc<any>(\n () =>\n versionId\n ? doc(\n firestore,\n 'hosts',\n hostId,\n 'screens',\n screenId,\n 'versions',\n versionId,\n )\n : null,\n [firestore, hostId, screenId, versionId],\n )\n\n const provenance = useMemo(() => templateProvenance(screen), [screen])\n\n const listUrl = `${basePath}/templates`\n const besignerUrl =\n orgSlug && subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug,\n host: subdomain,\n screenId,\n versionId,\n })\n : ''\n\n /*\n * The header's actions: the way back, and the way in.\n *\n * The same pair the screen, component, template and layout detail pages\n * carry — a link to the listing and a contained button into the besigner\n * wearing the besigner icon. It hangs off `CardDisplay`'s header rather\n * than the page header because Emails is a surface with a SECTION RAIL: an\n * action beside the page title would read as the whole surface's, and this\n * one belongs to the template the section is showing. The rule is the\n * console's own — page header where there are no sections, section card\n * header where there are — and it is why the Forms detail surface, which\n * has no rail, publishes its besigner button upward instead.\n */\n const headerActions = (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={listUrl}\n size=\"small\"\n color=\"primary\"\n >\n {'All templates'}\n </Button>\n {/*\n * A LINK once there is somewhere to go, and a plain disabled button\n * until then.\n *\n * Not one control with `disabled` on it: `disabled` renders an anchor\n * greyed out and still followable, so a template that has never been\n * saved would offer a besigner URL missing its version — a 404, which\n * reads as a broken console rather than as an empty template.\n */}\n {besignerUrl ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={besignerUrl}\n size=\"small\"\n variant=\"contained\"\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n ) : (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled\n startIcon={\n <MdiIcon color=\"inherit\" path={ICON_VARIANT_BESIGNER.path} />\n }\n >\n {'Edit in besigner'}\n </Button>\n )}\n </Stack>\n )\n\n if (notFound) {\n return (\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This template could not be loaded. It may have been deleted.'}\n </Typography>\n </CardDisplay>\n )\n }\n\n return (\n <Stack spacing={3}>\n {/* The page heading and the trail name the template; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord\n title={screen ? screen.displayName || screenId : undefined}\n />\n <CardDisplay\n header={'Email template'}\n help={templateDocsHelp}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={3}>\n {provenance.note ? (\n <Alert severity={provenance.warn ? 'warning' : 'info'}>\n {provenance.note}\n </Alert>\n ) : null}\n\n <Section title=\"Name\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Design name\"\n size=\"small\"\n value={nameDraft ?? String(screen?.displayName ?? '')}\n onChange={(event) => setNameDraft(event.target.value)}\n sx={{ minWidth: 280 }}\n helperText=\"What the campaign composer's design picker shows\"\n />\n <Button\n size=\"small\"\n disabled={nameDraft === null || !nameDraft.trim() || renaming}\n onClick={() => void handleRename()}\n >\n {renaming ? 'Saving…' : 'Rename'}\n </Button>\n </Stack>\n </Section>\n\n </Stack>\n </CardDisplay>\n\n {/*\n What the emails sent from this template did, then who received them:\n each its own boundary, because each card's code arrives after the\n card above it has drawn and must not send it back to a loading state.\n */}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_REPORT_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n basePath={basePath}\n />\n </Suspense>\n ) : null}\n {Zone ? (\n <Suspense fallback={null}>\n <Zone\n slot={EMAIL_TEMPLATE_RECIPIENTS_ZONE.id}\n hostId={hostId}\n screenId={screenId}\n />\n </Suspense>\n ) : null}\n\n {/*\n * Last, and its own card — the same order the email's own page uses.\n * The figures are what a reader opens either page for and the frame is\n * the tallest thing on both, so at the top it pushes every number below\n * the fold.\n */}\n <CardDisplay\n header={'Preview'}\n help={previewDocsHelp}\n contentGutterX\n contentGutterY\n >\n <EmailDesignPreview\n hostId={hostId}\n nodes={version?.nodes}\n loading={version === undefined && Boolean(versionId)}\n subject={String(screen?.emailSubject ?? '')}\n preheader={String(screen?.emailPreheader ?? '')}\n emptyMessage={\n 'This template has nothing in it yet. Open it in the ' +\n 'besigner to build the email.'\n }\n />\n </CardDisplay>\n </Stack>\n )\n}\nEmailTemplateDetail.displayName = 'EmailTemplateDetail'\n\nexport default EmailTemplateDetail\n"],"names":["buildRoute","PageHeaderRecord","pluginDocsHelp","Route","artifactRenameListKeys","ICON_VARIANT_BESIGNER","AppLink","CardDisplay","MdiIcon","useConsoleHostRoute","useFirestore","useFirestoreDoc","Alert","Button","Stack","TextField","Typography","doc","updateDoc","Suspense","useMemo","useState","useSnackbar","templateProvenance","EmailDesignPreview","EMAIL_TEMPLATE_RECIPIENTS_ZONE","EMAIL_TEMPLATE_REPORT_ZONE","Section","useConsoleWidgetSlot","previewDocsHelp","anchor","excerpt","templateDocsHelp","EmailTemplateDetail","props","hostId","screenId","basePath","orgSlug","subdomain","Zone","firestore","enqueueSnackbar","nameDraft","setNameDraft","renaming","setRenaming","handleRename","next","trim","displayName","variant","persist","error","console","data","screen","status","idField","notFound","versionId","version","provenance","listUrl","besignerUrl","SCREEN_BESIGNER","host","headerActions","direction","spacing","useFlexGap","sx","alignItems","flexWrap","component","componentVariant","nativeButton","href","size","color","startIcon","path","disabled","header","help","contentGutterX","contentGutterY","HeaderProps","action","title","undefined","note","severity","warn","label","value","String","onChange","event","target","minWidth","helperText","onClick","fallback","slot","id","nodes","loading","Boolean","subject","emailSubject","preheader","emailPreheader","emptyMessage"],"mappings":"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"}
@@ -259,6 +259,7 @@ import { useOrgEmailTopics, writeEmailTopic } from "./use-org-email-topics.js";
259
259
  children: [
260
260
  /*#__PURE__*/ _jsx(TableCell, {
261
261
  children: /*#__PURE__*/ _jsxs(Stack, {
262
+ useFlexGap: true,
262
263
  direction: "row",
263
264
  spacing: 1,
264
265
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-topics-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 createResourceUid,\n isSubscriptionTopicId,\n pluginDocsHelp,\n DECLARED_SUBSCRIPTION_TOPICS,\n} from '@aglyn/aglyn'\nimport { EMAIL_TOPICS_COLLECTION } from '../model/email-topic-catalog'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\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 { doc, setDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useEmailOrgMount } from './email-org-mount'\nimport { useOrgEmailTopics, writeEmailTopic } from './use-org-email-topics'\n\nexport interface EmailTopicsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every topic route hangs beneath. */\n basePath: string\n}\n\n/**\n * Email topics: the streams a recipient can leave one at a time.\n *\n * ## What this closes\n *\n * `docs/specs/email-competitive-gaps.md` §1f is a table of what six\n * competitors have, and the preference-center row was the one where every one\n * of them had something and we had nothing: unsubscribe was all-or-nothing per\n * site. This card is the merchant's half of the fix — the recipient's half is\n * the hosted preference page the campaign footer now links to.\n *\n * ## Why the four built-ins are not documents\n *\n * Every org that exists today has an empty `emailTopics` collection, and a\n * seeding migration would leave any org created while it ran with none. So\n * `DECLARED_SUBSCRIPTION_TOPICS` is the FLOOR of the catalog rather than its initial\n * contents: the four are present for every org with no write anywhere, and\n * saving one on its detail page writes a document at the same id that renames\n * or retires it. A built-in with no stored document is not a missing row, it\n * is a row nobody has needed to change.\n *\n * ## Create is a drawer; edit is the topic's own page\n *\n * The console's standing shape for a list surface: creating opens the shared\n * `CreateArtifactDrawer`, the same one Screens, Components, Layouts and\n * Templates use, and changing an existing record happens on that record's\n * page rather than in a form stacked above the table. So this card lists and\n * routes, and `email-topic-detail.tsx` edits.\n *\n * ## The row grammar the rest of the surface uses\n *\n * The same one the audiences table sets: the row opens the topic, the topic's\n * name is ALSO a real link so it can be middle-clicked and copied, and the\n * secondary actions sit behind the shared `RowActionsMenu` rather than as text\n * buttons in the row. Retire is the reason the menu has more than one entry —\n * it is the only thing that can be done to a topic without opening it, and it\n * is destructive enough that it should not sit one mis-click from the row's\n * own open handler.\n */\nexport function EmailTopicsCard(props: EmailTopicsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The catalog is the org's: read through the site under one, and straight\n // from the organization on its own page.\n const orgMount = useEmailOrgMount()\n const { topics, scope } = useOrgEmailTopics(hostId, {\n orgId: orgMount?.orgId,\n })\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [error, setError] = useState<unknown>(null)\n const [busyTopicId, setBusyTopicId] = useState('')\n\n /** The topic's own page, which is also its edit page. */\n const topicHref = useCallback(\n (topicId: string) => `${basePath}/topics/${topicId}`,\n [basePath],\n )\n\n const create = useCallback(\n async (values: Record<string, any>) => {\n if (!scope) return\n const name = String(values?.displayName ?? '').trim()\n if (!name) return\n // `createResourceUid` is nanoid's `A-Za-z0-9_-`, so the id it mints\n // always satisfies `isSubscriptionTopicId`. Checked anyway, because this id is\n // signed into every unsubscribe link the topic ever mints and the send\n // path refuses one that is not usable — a topic nothing can be sent\n // under is a row the merchant would have no way to diagnose.\n const id = createResourceUid()\n if (!isSubscriptionTopicId(id)) {\n return void enqueueSnackbar('Could not create the topic', {\n variant: 'error',\n })\n }\n try {\n await setDoc(\n doc(firestore, scope[0], scope[1], EMAIL_TOPICS_COLLECTION, id),\n {\n name,\n description: String(values?.description ?? '').trim(),\n archived: false,\n },\n )\n setDrawerOpen(false)\n setError(null)\n enqueueSnackbar('Topic added', { variant: 'success', persist: false })\n // Straight to the record, which is where every further change is made.\n router.push(topicHref(id))\n } catch (caught) {\n console.error(caught)\n setError(caught)\n }\n },\n [scope, firestore, enqueueSnackbar, topicHref, router],\n )\n\n /**\n * Retire a topic, or bring one back.\n *\n * Retire and not delete: campaigns already sent under a topic minted\n * unsubscribe links carrying its id, those links are sitting in inboxes, and\n * an id that stops resolving is a preference page that cannot name the\n * message the recipient is holding. The Firestore rules refuse a delete for\n * the same reason.\n *\n * Restoring is not asked about — it puts a choice back rather than taking\n * one away, and it is undone by retiring again.\n */\n const toggleArchived = useCallback(\n async (topic: { id: string; name: string; description: string; archived?: boolean }) => {\n if (!scope || busyTopicId) return\n if (!topic.archived) {\n const accepted = await confirm({\n title: `Retire “${topic.name}”?`,\n description:\n 'It leaves the composer and the preference page, so nothing new ' +\n 'can be sent under it and recipients stop seeing it as a choice. ' +\n 'Campaigns already sent under it keep working — their ' +\n 'unsubscribe links still name this topic. You can bring it back ' +\n 'at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating\n // on the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyTopicId(topic.id)\n try {\n await writeEmailTopic(firestore, scope, {\n id: topic.id,\n name: topic.name,\n description: topic.description,\n archived: !topic.archived,\n })\n enqueueSnackbar(topic.archived ? 'Topic restored' : 'Topic retired', {\n variant: 'success',\n persist: false,\n })\n } catch (caught) {\n console.error(caught)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusyTopicId('')\n }\n },\n [scope, busyTopicId, confirm, firestore, enqueueSnackbar],\n )\n\n const rowActions = (topic: {\n id: string\n name: string\n description: string\n archived?: boolean\n }): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit topic',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: topicHref(topic.id),\n },\n {\n key: 'archive',\n label: topic.archived ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={\n topic.archived\n ? mdiArchiveArrowUpOutline.path\n : mdiArchiveOutline.path\n }\n size={0.8}\n />\n ),\n destructive: !topic.archived,\n disabled: Boolean(busyTopicId),\n disabledReason: busyTopicId ? 'Another topic is being saved' : undefined,\n onClick: () => void toggleArchived(topic),\n },\n ]\n\n return (\n <CardDisplay\n header=\"Topics\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#topics' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Button variant=\"contained\" onClick={() => setDrawerOpen(true)}>\n {'New topic'}\n </Button>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The streams your recipients can leave one at a time. Every ' +\n 'campaign belongs to one, and the preference page linked from ' +\n 'each email lets someone stop that stream without stopping ' +\n 'everything. Topics are shared across every site in this ' +\n 'organization, the same way your lists are.'}\n </Typography>\n {!scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site has no organization, so it has no topic list.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Topic'}</TableCell>\n <TableCell>{'What recipients are told they get'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {topics.map((topic) => {\n const href = topicHref(topic.id)\n return (\n <TableRow\n key={topic.id}\n hover\n onClick={() => router.push(href)}\n sx={{ cursor: 'pointer' }}\n >\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n {/*\n The row's own handler would fire too and push the\n same route twice — one history entry per back press.\n */}\n <AppLink\n href={href}\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {topic.name}\n </AppLink>\n {topic.archived ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Retired\"\n />\n ) : null}\n {DECLARED_SUBSCRIPTION_TOPICS.some(\n (it) => it.id === topic.id,\n ) ? (\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n >\n {'Built in'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {topic.description || '—'}\n </Typography>\n </TableCell>\n <TableCell\n align=\"right\"\n sx={{ width: 56 }}\n onClick={(event) => event.stopPropagation()}\n >\n <RowActionsMenu\n label={topic.name}\n items={rowActions(topic)}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n <CreateArtifactDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n title=\"New email topic\"\n submitLabel=\"Create topic\"\n onSubmit={create}\n errorSlot={\n error ? (\n <Typography variant=\"body2\" color=\"error\" sx={{ mt: 2 }}>\n {'Could not create the topic. Try again.'}\n </Typography>\n ) : null\n }\n />\n </CardDisplay>\n )\n}\n\nexport default EmailTopicsCard\n"],"names":["createResourceUid","isSubscriptionTopicId","pluginDocsHelp","DECLARED_SUBSCRIPTION_TOPICS","EMAIL_TOPICS_COLLECTION","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","CreateArtifactDrawer","ScrollTable","useSnackbar","Button","Chip","Stack","TableBody","TableCell","TableHead","TableRow","Typography","doc","setDoc","useRouter","useCallback","useState","useFirestore","useEmailOrgMount","useOrgEmailTopics","writeEmailTopic","EmailTopicsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","orgMount","topics","scope","orgId","drawerOpen","setDrawerOpen","error","setError","busyTopicId","setBusyTopicId","topicHref","topicId","create","values","name","String","displayName","trim","id","variant","description","archived","persist","push","caught","console","toggleArchived","topic","accepted","title","confirmationText","then","catch","rowActions","key","label","icon","path","size","href","destructive","disabled","Boolean","disabledReason","undefined","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","color","align","map","hover","sx","cursor","direction","flexWrap","alignItems","event","stopPropagation","some","it","width","items","open","onClose","submitLabel","onSubmit","errorSlot","mt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-topics-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 createResourceUid,\n isSubscriptionTopicId,\n pluginDocsHelp,\n DECLARED_SUBSCRIPTION_TOPICS,\n} from '@aglyn/aglyn'\nimport { EMAIL_TOPICS_COLLECTION } from '../model/email-topic-catalog'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\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 { doc, setDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useEmailOrgMount } from './email-org-mount'\nimport { useOrgEmailTopics, writeEmailTopic } from './use-org-email-topics'\n\nexport interface EmailTopicsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every topic route hangs beneath. */\n basePath: string\n}\n\n/**\n * Email topics: the streams a recipient can leave one at a time.\n *\n * ## What this closes\n *\n * `docs/specs/email-competitive-gaps.md` §1f is a table of what six\n * competitors have, and the preference-center row was the one where every one\n * of them had something and we had nothing: unsubscribe was all-or-nothing per\n * site. This card is the merchant's half of the fix — the recipient's half is\n * the hosted preference page the campaign footer now links to.\n *\n * ## Why the four built-ins are not documents\n *\n * Every org that exists today has an empty `emailTopics` collection, and a\n * seeding migration would leave any org created while it ran with none. So\n * `DECLARED_SUBSCRIPTION_TOPICS` is the FLOOR of the catalog rather than its initial\n * contents: the four are present for every org with no write anywhere, and\n * saving one on its detail page writes a document at the same id that renames\n * or retires it. A built-in with no stored document is not a missing row, it\n * is a row nobody has needed to change.\n *\n * ## Create is a drawer; edit is the topic's own page\n *\n * The console's standing shape for a list surface: creating opens the shared\n * `CreateArtifactDrawer`, the same one Screens, Components, Layouts and\n * Templates use, and changing an existing record happens on that record's\n * page rather than in a form stacked above the table. So this card lists and\n * routes, and `email-topic-detail.tsx` edits.\n *\n * ## The row grammar the rest of the surface uses\n *\n * The same one the audiences table sets: the row opens the topic, the topic's\n * name is ALSO a real link so it can be middle-clicked and copied, and the\n * secondary actions sit behind the shared `RowActionsMenu` rather than as text\n * buttons in the row. Retire is the reason the menu has more than one entry —\n * it is the only thing that can be done to a topic without opening it, and it\n * is destructive enough that it should not sit one mis-click from the row's\n * own open handler.\n */\nexport function EmailTopicsCard(props: EmailTopicsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The catalog is the org's: read through the site under one, and straight\n // from the organization on its own page.\n const orgMount = useEmailOrgMount()\n const { topics, scope } = useOrgEmailTopics(hostId, {\n orgId: orgMount?.orgId,\n })\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [error, setError] = useState<unknown>(null)\n const [busyTopicId, setBusyTopicId] = useState('')\n\n /** The topic's own page, which is also its edit page. */\n const topicHref = useCallback(\n (topicId: string) => `${basePath}/topics/${topicId}`,\n [basePath],\n )\n\n const create = useCallback(\n async (values: Record<string, any>) => {\n if (!scope) return\n const name = String(values?.displayName ?? '').trim()\n if (!name) return\n // `createResourceUid` is nanoid's `A-Za-z0-9_-`, so the id it mints\n // always satisfies `isSubscriptionTopicId`. Checked anyway, because this id is\n // signed into every unsubscribe link the topic ever mints and the send\n // path refuses one that is not usable — a topic nothing can be sent\n // under is a row the merchant would have no way to diagnose.\n const id = createResourceUid()\n if (!isSubscriptionTopicId(id)) {\n return void enqueueSnackbar('Could not create the topic', {\n variant: 'error',\n })\n }\n try {\n await setDoc(\n doc(firestore, scope[0], scope[1], EMAIL_TOPICS_COLLECTION, id),\n {\n name,\n description: String(values?.description ?? '').trim(),\n archived: false,\n },\n )\n setDrawerOpen(false)\n setError(null)\n enqueueSnackbar('Topic added', { variant: 'success', persist: false })\n // Straight to the record, which is where every further change is made.\n router.push(topicHref(id))\n } catch (caught) {\n console.error(caught)\n setError(caught)\n }\n },\n [scope, firestore, enqueueSnackbar, topicHref, router],\n )\n\n /**\n * Retire a topic, or bring one back.\n *\n * Retire and not delete: campaigns already sent under a topic minted\n * unsubscribe links carrying its id, those links are sitting in inboxes, and\n * an id that stops resolving is a preference page that cannot name the\n * message the recipient is holding. The Firestore rules refuse a delete for\n * the same reason.\n *\n * Restoring is not asked about — it puts a choice back rather than taking\n * one away, and it is undone by retiring again.\n */\n const toggleArchived = useCallback(\n async (topic: { id: string; name: string; description: string; archived?: boolean }) => {\n if (!scope || busyTopicId) return\n if (!topic.archived) {\n const accepted = await confirm({\n title: `Retire “${topic.name}”?`,\n description:\n 'It leaves the composer and the preference page, so nothing new ' +\n 'can be sent under it and recipients stop seeing it as a choice. ' +\n 'Campaigns already sent under it keep working — their ' +\n 'unsubscribe links still name this topic. You can bring it back ' +\n 'at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with NO VALUE and REJECTS on cancel, so gating\n // on the resolved value alone makes this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyTopicId(topic.id)\n try {\n await writeEmailTopic(firestore, scope, {\n id: topic.id,\n name: topic.name,\n description: topic.description,\n archived: !topic.archived,\n })\n enqueueSnackbar(topic.archived ? 'Topic restored' : 'Topic retired', {\n variant: 'success',\n persist: false,\n })\n } catch (caught) {\n console.error(caught)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setBusyTopicId('')\n }\n },\n [scope, busyTopicId, confirm, firestore, enqueueSnackbar],\n )\n\n const rowActions = (topic: {\n id: string\n name: string\n description: string\n archived?: boolean\n }): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit topic',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: topicHref(topic.id),\n },\n {\n key: 'archive',\n label: topic.archived ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={\n topic.archived\n ? mdiArchiveArrowUpOutline.path\n : mdiArchiveOutline.path\n }\n size={0.8}\n />\n ),\n destructive: !topic.archived,\n disabled: Boolean(busyTopicId),\n disabledReason: busyTopicId ? 'Another topic is being saved' : undefined,\n onClick: () => void toggleArchived(topic),\n },\n ]\n\n return (\n <CardDisplay\n header=\"Topics\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#topics' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Button variant=\"contained\" onClick={() => setDrawerOpen(true)}>\n {'New topic'}\n </Button>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The streams your recipients can leave one at a time. Every ' +\n 'campaign belongs to one, and the preference page linked from ' +\n 'each email lets someone stop that stream without stopping ' +\n 'everything. Topics are shared across every site in this ' +\n 'organization, the same way your lists are.'}\n </Typography>\n {!scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site has no organization, so it has no topic list.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Topic'}</TableCell>\n <TableCell>{'What recipients are told they get'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {topics.map((topic) => {\n const href = topicHref(topic.id)\n return (\n <TableRow\n key={topic.id}\n hover\n onClick={() => router.push(href)}\n sx={{ cursor: 'pointer' }}\n >\n <TableCell>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n {/*\n The row's own handler would fire too and push the\n same route twice — one history entry per back press.\n */}\n <AppLink\n href={href}\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {topic.name}\n </AppLink>\n {topic.archived ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label=\"Retired\"\n />\n ) : null}\n {DECLARED_SUBSCRIPTION_TOPICS.some(\n (it) => it.id === topic.id,\n ) ? (\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n >\n {'Built in'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {topic.description || '—'}\n </Typography>\n </TableCell>\n <TableCell\n align=\"right\"\n sx={{ width: 56 }}\n onClick={(event) => event.stopPropagation()}\n >\n <RowActionsMenu\n label={topic.name}\n items={rowActions(topic)}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n </Stack>\n <CreateArtifactDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n title=\"New email topic\"\n submitLabel=\"Create topic\"\n onSubmit={create}\n errorSlot={\n error ? (\n <Typography variant=\"body2\" color=\"error\" sx={{ mt: 2 }}>\n {'Could not create the topic. Try again.'}\n </Typography>\n ) : null\n }\n />\n </CardDisplay>\n )\n}\n\nexport default EmailTopicsCard\n"],"names":["createResourceUid","isSubscriptionTopicId","pluginDocsHelp","DECLARED_SUBSCRIPTION_TOPICS","EMAIL_TOPICS_COLLECTION","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","CreateArtifactDrawer","ScrollTable","useSnackbar","Button","Chip","Stack","TableBody","TableCell","TableHead","TableRow","Typography","doc","setDoc","useRouter","useCallback","useState","useFirestore","useEmailOrgMount","useOrgEmailTopics","writeEmailTopic","EmailTopicsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","orgMount","topics","scope","orgId","drawerOpen","setDrawerOpen","error","setError","busyTopicId","setBusyTopicId","topicHref","topicId","create","values","name","String","displayName","trim","id","variant","description","archived","persist","push","caught","console","toggleArchived","topic","accepted","title","confirmationText","then","catch","rowActions","key","label","icon","path","size","href","destructive","disabled","Boolean","disabledReason","undefined","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","color","align","map","hover","sx","cursor","useFlexGap","direction","flexWrap","alignItems","event","stopPropagation","some","it","width","items","open","onClose","submitLabel","onSubmit","errorSlot","mt"],"mappings":"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"}
@@ -142,6 +142,10 @@ export function ListDetailCard(props) {
142
142
  const headerActions = /*#__PURE__*/ _jsxs(Stack, {
143
143
  direction: "row",
144
144
  spacing: 1,
145
+ useFlexGap: true,
146
+ sx: {
147
+ flexWrap: 'wrap'
148
+ },
145
149
  children: [
146
150
  /*#__PURE__*/ _jsx(Button, _extends({
147
151
  component: AppLink