@aglyn/plugins-email 1.0.0-beta.231 → 1.0.0-beta.233
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 +16 -16
- package/src/lib/components/consent-group-change-progress.js +4 -0
- package/src/lib/components/consent-group-change-progress.js.map +1 -1
- package/src/lib/components/consent-group-site-picker.js +1 -0
- package/src/lib/components/consent-group-site-picker.js.map +1 -1
- package/src/lib/components/email-screens-card.js +33 -6
- package/src/lib/components/email-screens-card.js.map +1 -1
- package/src/lib/components/email-template-detail.js +3 -1
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/email-topics-card.js +1 -0
- package/src/lib/components/email-topics-card.js.map +1 -1
- package/src/lib/components/list-detail-card.js +4 -0
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-members-panel.js +38 -7
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/lists-card.js +35 -7
- package/src/lib/components/lists-card.js.map +1 -1
- package/src/lib/components/org-email-templates-card.js +34 -7
- package/src/lib/components/org-email-templates-card.js.map +1 -1
- package/src/lib/components/org-sending-domain-detail.js +1 -0
- package/src/lib/components/org-sending-domain-detail.js.map +1 -1
- package/src/lib/components/sending-domain-detail.js +1 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -1
- package/src/lib/components/suppressions-card.js +25 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/constants/list-queries.js +73 -9
- package/src/lib/constants/list-queries.js.map +1 -1
- package/src/lib/model/email-design-document.d.ts +1 -1
- package/src/lib/model/email-design-document.js +1 -1
- package/src/lib/model/email-design-document.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.233",
|
|
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.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.233",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.233",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.233",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.233",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.233",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.233",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.233",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
|
|
36
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.233",
|
|
37
|
+
"@aglyn/shared-util-email": "1.0.0-beta.233",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.233",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.233",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.233",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.233",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.233",
|
|
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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"}
|
|
@@ -29,10 +29,11 @@ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filt
|
|
|
29
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
30
|
import { DUPLICATE_MENU_LABEL, useConsoleHostRoute, useDuplicateResource, useFirestore, useHostResourceApi, useHostVersionApi } from "@aglyn/tenant-feature-instance";
|
|
31
31
|
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
32
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
32
33
|
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
33
34
|
import { collection, doc, updateDoc } from "firebase/firestore";
|
|
34
35
|
import { useRouter } from "next/navigation";
|
|
35
|
-
import { useMemo } from "react";
|
|
36
|
+
import { useMemo, useState } from "react";
|
|
36
37
|
import { EMAIL_TEMPLATE_BASE, EMAIL_TEMPLATE_FILTER_HEADERS, EMAIL_TEMPLATE_QUERY } from "../constants/list-queries.js";
|
|
37
38
|
import { templateProvenance } from "../model/template-provenance.js";
|
|
38
39
|
import { createEmailScreen } from "../utils/create-email-screen.js";
|
|
@@ -43,6 +44,12 @@ import { emailTemplateSoftDelete } from "../utils/email-template-soft-delete.js"
|
|
|
43
44
|
// pre-AGL-621/622 shape — so every "Edit"/"Design" jump out of the Emails
|
|
44
45
|
// page landed on a 404, including the one right after creating a new email
|
|
45
46
|
// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.
|
|
47
|
+
/** Origin sorts the loaded page: installed or yours (AGL-3680). */ const TEMPLATE_PAGE_SORTS = {
|
|
48
|
+
origin: (row)=>templateProvenance(row).origin === 'installed' ? 'installed' : 'local'
|
|
49
|
+
};
|
|
50
|
+
const TEMPLATE_SORT_HEADERS = {
|
|
51
|
+
origin: 'Origin'
|
|
52
|
+
};
|
|
46
53
|
const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCREEN_BESIGNER, {
|
|
47
54
|
orgSlug,
|
|
48
55
|
host,
|
|
@@ -108,12 +115,19 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
108
115
|
});
|
|
109
116
|
const { confirm } = useConfirmationContext();
|
|
110
117
|
const gridFilter = useListGridFilter();
|
|
118
|
+
/*
|
|
119
|
+
* EVERY HEADER SORTS (AGL-3680). Template orders the QUERY by `nameLower`,
|
|
120
|
+
* A to Z by default and Z to A while nothing narrows the list; Origin is
|
|
121
|
+
* the presence of the install provenance, which no query can order by, so
|
|
122
|
+
* it sorts the page on screen and says so.
|
|
123
|
+
*/ const [askedSort, setAskedSort] = useState(EMAIL_TEMPLATE_QUERY.sorts[0]);
|
|
111
124
|
const { rows: emailScreens, hasMore, page, setPage, pageSize, setPageSize, plan } = useListQuery({
|
|
112
125
|
collection: collection(firestore, 'hosts', hostId, 'screens'),
|
|
113
126
|
declaration: EMAIL_TEMPLATE_QUERY,
|
|
114
127
|
request: {
|
|
115
128
|
clauses: gridFilter.clauses,
|
|
116
129
|
search: gridFilter.searchWords,
|
|
130
|
+
sort: askedSort,
|
|
117
131
|
base: EMAIL_TEMPLATE_BASE
|
|
118
132
|
},
|
|
119
133
|
deps: [
|
|
@@ -122,6 +136,16 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
122
136
|
],
|
|
123
137
|
idField: '$id'
|
|
124
138
|
});
|
|
139
|
+
const columnSort = useListColumnSort({
|
|
140
|
+
sorts: EMAIL_TEMPLATE_QUERY.sorts,
|
|
141
|
+
defaultSort: EMAIL_TEMPLATE_QUERY.sorts[0],
|
|
142
|
+
sort: askedSort,
|
|
143
|
+
onSortChange: setAskedSort,
|
|
144
|
+
orderBy: plan.orderBy,
|
|
145
|
+
rows: emailScreens,
|
|
146
|
+
pageSorts: TEMPLATE_PAGE_SORTS,
|
|
147
|
+
headers: TEMPLATE_SORT_HEADERS
|
|
148
|
+
});
|
|
125
149
|
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
126
150
|
const refusals = useMemo(()=>listQueryRefusals(plan.refused, {
|
|
127
151
|
fields: EMAIL_TEMPLATE_QUERY.fields,
|
|
@@ -308,23 +332,26 @@ const besignerHref = (orgSlug, host, screenId, versionId)=>buildRoute(Route.SCRE
|
|
|
308
332
|
}),
|
|
309
333
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
310
334
|
refused: refusals,
|
|
311
|
-
notices:
|
|
335
|
+
notices: [
|
|
336
|
+
...plan.notices,
|
|
337
|
+
...columnSort.notices
|
|
338
|
+
]
|
|
312
339
|
}),
|
|
313
340
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
314
341
|
"aria-label": "Email templates",
|
|
315
|
-
rows:
|
|
342
|
+
rows: columnSort.rows,
|
|
343
|
+
columnSort: columnSort,
|
|
316
344
|
columns: listFilterGridColumns(columns, EMAIL_TEMPLATE_QUERY.fields, {}, EMAIL_TEMPLATE_FILTER_HEADERS),
|
|
317
345
|
rowHeight: TABLE_ROW_HEIGHT,
|
|
318
346
|
onOpen: (_id, row)=>router.push(templateHref(row)),
|
|
319
347
|
// Paged by the footer below, so the grid must not also slice.
|
|
320
348
|
hideFooter: true,
|
|
321
|
-
// The panel and the
|
|
322
|
-
//
|
|
349
|
+
// The panel, the search and the Template header go to the
|
|
350
|
+
// query (Origin sorts the page, and says so).
|
|
323
351
|
filterMode: "server",
|
|
324
352
|
filterModel: gridFilter.filterModel,
|
|
325
353
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
326
354
|
quickFilter: true,
|
|
327
|
-
disableColumnSorting: true,
|
|
328
355
|
noRowsLabel: "No templates match these filters"
|
|
329
356
|
}),
|
|
330
357
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { HOST_EMAIL_TEMPLATES_ZONE } from './email-zones'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */\n orgId?: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, orgId, basePath } = props\n /**\n * The shell's zone renderer (AGL-3596), for other ways to start a design —\n * the `hostEmailTemplates` zone this plugin declares; `null` outside the\n * console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n const createZone = CreateZone ? (\n <CreateZone slot={HOST_EMAIL_TEMPLATES_ZONE.id} hostId={hostId} orgId={orgId} />\n ) : null\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n </Stack>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n {/* Other ways to start a design, the header's zone again. */}\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button variant=\"contained\" onClick={() => void handleCreate()}>\n {'Create your first template'}\n </Button>\n </Stack>\n </>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={emailScreens}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to the query; the grid neither\n // filters nor sorts the page it holds.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","useConsoleWidgetSlot","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","HOST_EMAIL_TEMPLATES_ZONE","emailTemplateSoftDelete","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","orgId","basePath","CreateZone","createZone","slot","id","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","base","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","row","renderCell","value","event","stopPropagation","width","origin","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","dialog","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-screens-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n mdiDeleteOutline,\n mdiContentCopy,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n AppLink,\n CardDisplay,\n MdiIcon,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n DUPLICATE_MENU_LABEL,\n useConsoleHostRoute,\n useDuplicateResource,\n useFirestore,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useMemo, useState } from 'react'\nimport {\n EMAIL_TEMPLATE_BASE,\n EMAIL_TEMPLATE_FILTER_HEADERS,\n EMAIL_TEMPLATE_QUERY,\n} from '../constants/list-queries'\nimport { templateProvenance } from '../model/template-provenance'\nimport { createEmailScreen } from '../utils/create-email-screen'\nimport { HOST_EMAIL_TEMPLATES_ZONE } from './email-zones'\nimport { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\n\n// The besigner route is `/[orgSlug]/hosts/[host]/screens/[screenId]/\n// versions/[versionId]/besigner`. This built `/{hostDocId}/screens/…`, the\n// pre-AGL-621/622 shape — so every \"Edit\"/\"Design\" jump out of the Emails\n// page landed on a 404, including the one right after creating a new email\n// (AGL-685). Takes the resolved org slug + subdomain, not a host doc id.\n/** Origin sorts the loaded page: installed or yours (AGL-3680). */\nconst TEMPLATE_PAGE_SORTS = {\n origin: (row: any) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n}\nconst TEMPLATE_SORT_HEADERS = { origin: 'Origin' }\n\nconst besignerHref = (\n orgSlug: string,\n host: string,\n screenId: string,\n versionId: string,\n) =>\n buildRoute(Route.SCREEN_BESIGNER, { orgSlug, host, screenId, versionId })\n\n/**\n * THE TEMPLATES: reusable besigner documents an email is built from.\n *\n * A template is a screen document with `kind: 'email'`, kept out of the main\n * Screens list and opened in the besigner with only email-safe components on\n * offer. It is not itself a message — a message is what a campaign sends, and\n * one template can be behind many of them, which is why the row leads to the\n * template's own page rather than straight into the editor.\n *\n * A template is not necessarily this org's. One installed from a marketplace\n * listing appears here beside the locally authored ones, carries its\n * publisher's provenance on the same document, and opens the same detail\n * page; the chip beside its name is what distinguishes them.\n *\n * ## A table, on the surface's own row grammar\n *\n * The row opens the template, its name is ALSO a real link so it can be\n * middle-clicked and copied, and editing and deleting live behind the shared\n * overflow menu rather than as text buttons in the row — the same grammar the\n * audiences table reads by. Delete in particular: it sat inline, one mis-click\n * from the name beside it.\n *\n * ## The list IS its query\n *\n * `kind == 'email'`, by name, paged by the query — and every Filters clause\n * and search word is a predicate on it (AGL-3321, `EMAIL_TEMPLATE_QUERY`), so\n * a page is a page of the templates that match and none is matched in the\n * browser. The order is `nameLower`, which every screen writer stamps from\n * `displayName`; a deleted template has its name keys cleared with the\n * delete (`emailTemplateSoftDelete`), so the order leaves it out rather than\n * the card dropping it after the read.\n */\nexport function EmailScreensCard(props: {\n hostId: string\n /** The org the site belongs to, handed to the create zone; `undefined` while it resolves. */\n orgId?: string\n /** The emails hub URL, which every template route hangs beneath. */\n basePath: string\n}) {\n const { hostId, orgId, basePath } = props\n /**\n * The shell's zone renderer (AGL-3596), for other ways to start a design —\n * the `hostEmailTemplates` zone this plugin declares; `null` outside the\n * console shell, where there is no workspace to gate on.\n */\n const CreateZone = useConsoleWidgetSlot()\n const createZone = CreateZone ? (\n <CreateZone slot={HOST_EMAIL_TEMPLATES_ZONE.id} hostId={hostId} orgId={orgId} />\n ) : null\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n // Duplicate (AGL-2936): the copy is a design of its own, sent by nothing\n // until a campaign picks it.\n const duplicate = useDuplicateResource({\n hostId,\n onDuplicated: (_kind, copy) =>\n enqueueSnackbar(`Duplicated as “${copy.name}”`, { variant: 'success' }),\n })\n const { confirm } = useConfirmationContext()\n\n const gridFilter = useListGridFilter()\n /*\n * EVERY HEADER SORTS (AGL-3680). Template orders the QUERY by `nameLower`,\n * A to Z by default and Z to A while nothing narrows the list; Origin is\n * the presence of the install provenance, which no query can order by, so\n * it sorts the page on screen and says so.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(\n EMAIL_TEMPLATE_QUERY.sorts[0],\n )\n const {\n rows: emailScreens,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'screens'),\n declaration: EMAIL_TEMPLATE_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n base: EMAIL_TEMPLATE_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const columnSort = useListColumnSort<any>({\n sorts: EMAIL_TEMPLATE_QUERY.sorts,\n defaultSort: EMAIL_TEMPLATE_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: emailScreens,\n pageSorts: TEMPLATE_PAGE_SORTS,\n headers: TEMPLATE_SORT_HEADERS,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const refusals = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: EMAIL_TEMPLATE_QUERY.fields,\n headers: EMAIL_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const handleCreate = async () => {\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n if (orgSlug && subdomain) {\n void router.push(besignerHref(orgSlug, subdomain, screenId, versionId))\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n }\n }\n\n const handleDelete = async (screen: any) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${screen.displayName ?? 'Untitled template'}\" will be removed. ` +\n 'Emails already sent from it keep their reports.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'screens', screen.$id),\n emailTemplateSoftDelete(),\n )\n }\n\n const templateHref = (screen: any) => `${basePath}/templates/${screen.$id}`\n\n const templateName = (screen: any) =>\n String(screen.displayName ?? 'Untitled template')\n\n const rowActions = (screen: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: templateHref(screen),\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n /*\n The editor's URL needs the resolved org slug and subdomain. Until the\n host route settles both are empty and a link built from them lands on\n `/null/hosts/null`, so the item is shown DISABLED with the reason\n rather than pointing at a 404.\n */\n href:\n orgSlug && subdomain\n ? besignerHref(orgSlug, subdomain, screen.$id, screen.versionId)\n : undefined,\n disabled: !orgSlug || !subdomain,\n disabledReason: 'This site’s console URL has not resolved yet',\n },\n {\n key: 'duplicate',\n label: DUPLICATE_MENU_LABEL,\n icon: <MdiIcon path={mdiContentCopy.path} size={0.8} />,\n onClick: () =>\n duplicate.request('emailDesign', {\n id: screen.$id,\n name: templateName(screen),\n }),\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(screen),\n },\n ]\n\n /*\n * The name is a link AND the row opens the template.\n */\n const columns: GridColDef[] = [\n {\n field: 'displayName',\n headerName: 'Template',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => templateName(row),\n renderCell: ({ row, value }) => (\n <AppLink\n href={templateHref(row)}\n // The row's own handler would fire too and push the same route\n // twice — one history entry per back press.\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n >\n {value}\n </AppLink>\n ),\n },\n {\n /*\n WHOSE template this is, where the reader is choosing between them. An\n installed one is versioned by its publisher and can be withdrawn, which\n is not a property a name can carry.\n */\n field: 'origin',\n headerName: 'Origin',\n width: 140,\n valueGetter: (_value, row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n renderCell: ({ value }) =>\n value === 'installed' ? (\n <Chip size=\"small\" label=\"Installed\" />\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Yours'}\n </Typography>\n ),\n },\n listActionsColumn(\n (row) => (\n <ListRowActions label={templateName(row)} items={rowActions(row)} />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('designedEmails', { anchor: '#find-a-template' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void handleCreate()}\n >\n {'New template'}\n </Button>\n </Stack>\n ),\n }}\n >\n {duplicate.dialog}\n <Stack spacing={1.5}>\n {emailScreens.length === 0 && !hasMore && !filtering ? (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Design a reusable email here, then send it from a campaign. A ' +\n 'new template opens in the besigner with email-safe components ' +\n 'only.'}\n </Typography>\n {/* Other ways to start a design, the header's zone again. */}\n <Stack direction=\"row\" spacing={1}>\n {createZone}\n <Button variant=\"contained\" onClick={() => void handleCreate()}>\n {'Create your first template'}\n </Button>\n </Stack>\n </>\n ) : (\n <>\n <ListFilterChips\n fields={EMAIL_TEMPLATE_QUERY.fields}\n headers={EMAIL_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Email templates\"\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(\n columns,\n EMAIL_TEMPLATE_QUERY.fields,\n {},\n EMAIL_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(templateHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the Template header go to the\n // query (Origin sorts the page, and says so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No templates match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={emailScreens.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nEmailScreensCard.displayName = 'EmailScreensCard'\n\nexport default EmailScreensCard\n"],"names":["buildRoute","pluginDocsHelp","Route","useConsoleWidgetSlot","mdiDeleteOutline","mdiContentCopy","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","useSnackbar","DUPLICATE_MENU_LABEL","useConsoleHostRoute","useDuplicateResource","useFirestore","useHostResourceApi","useHostVersionApi","useListQuery","useListColumnSort","Button","Chip","Stack","Typography","collection","doc","updateDoc","useRouter","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","HOST_EMAIL_TEMPLATES_ZONE","emailTemplateSoftDelete","TEMPLATE_PAGE_SORTS","origin","row","TEMPLATE_SORT_HEADERS","besignerHref","orgSlug","host","screenId","versionId","SCREEN_BESIGNER","EmailScreensCard","props","hostId","orgId","basePath","CreateZone","createZone","slot","id","subdomain","firestore","createHostResource","createHostVersion","router","enqueueSnackbar","duplicate","onDuplicated","_kind","copy","name","variant","confirm","gridFilter","askedSort","setAskedSort","sorts","rows","emailScreens","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","sort","base","deps","idField","columnSort","defaultSort","onSortChange","orderBy","pageSorts","headers","filtering","length","some","word","trim","refusals","refused","fields","handleCreate","push","error","console","message","handleDelete","screen","confirmed","title","description","displayName","confirmationText","confirmationButtonProps","color","then","catch","$id","templateHref","templateName","String","rowActions","key","label","icon","path","size","href","undefined","disabled","disabledReason","onClick","destructive","columns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","value","event","stopPropagation","width","items","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","dialog","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","rowCount","onPageChange","onPageSizeChange"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,UAAU,EAAEC,cAAc,EAAEC,KAAK,QAAQ,eAAc;AAChE,SAASC,oBAAoB,QAAQ,qDAAoD;AACzF,SACEC,gBAAgB,EAChBC,cAAc,EACdC,aAAa,EACbC,gBAAgB,QACX,yBAAwB;AAC/B,SACEC,OAAO,EACPC,WAAW,EACXC,OAAO,EACPC,sBAAsB,QACjB,uBAAsB;AAC7B,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,OAAOC,oBACLC,iBAAiB,QACZ,+DAA8D;AACrE,SACEC,cAAc,EACdC,SAAS,EACTC,iBAAiB,QACZ,uDAAsD;AAE7D,SAASC,qBAAqB,QAAQ,8CAA6C;AACnF,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,oBAAoB,EACpBC,mBAAmB,EACnBC,oBAAoB,EACpBC,YAAY,EACZC,kBAAkB,EAClBC,iBAAiB,QACZ,iCAAgC;AACvC,SAASC,YAAY,QAAQ,sDAAqD;AAClF,SAASC,iBAAiB,QAAQ,kDAAiD;AAEnF,SAASC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAE/D,SAASC,UAAU,EAAEC,GAAG,EAAEC,SAAS,QAAQ,qBAAoB;AAC/D,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SACEC,mBAAmB,EACnBC,6BAA6B,EAC7BC,oBAAoB,QACf,+BAA2B;AAClC,SAASC,kBAAkB,QAAQ,kCAA8B;AACjE,SAASC,iBAAiB,QAAQ,kCAA8B;AAChE,SAASC,yBAAyB,QAAQ,mBAAe;AACzD,SAASC,uBAAuB,QAAQ,yCAAqC;AAE7E,qEAAqE;AACrE,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,iEAAiE,GACjE,MAAMC,sBAAsB;IAC1BC,QAAQ,CAACC,MACPN,mBAAmBM,KAAKD,MAAM,KAAK,cAAc,cAAc;AACnE;AACA,MAAME,wBAAwB;IAAEF,QAAQ;AAAS;AAEjD,MAAMG,eAAe,CACnBC,SACAC,MACAC,UACAC,YAEAxD,WAAWE,MAAMuD,eAAe,EAAE;QAAEJ;QAASC;QAAMC;QAAUC;IAAU;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASE,iBAAiBC,KAMhC;IACC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGH;IACpC;;;;GAIC,GACD,MAAMI,aAAa5D;IACnB,MAAM6D,aAAaD,2BACjB,KAACA;QAAWE,MAAMnB,0BAA0BoB,EAAE;QAAEN,QAAQA;QAAQC,OAAOA;SACrE;IACJ,MAAM,EAAER,OAAO,EAAEc,SAAS,EAAE,GAAG3C,oBAAoBoC;IACnD,MAAMQ,YAAY1C;IAClB,MAAM2C,qBAAqB1C;IAC3B,MAAM2C,oBAAoB1C;IAC1B,MAAM2C,SAASjC;IACf,MAAM,EAAEkC,eAAe,EAAE,GAAGlD;IAC5B,yEAAyE;IACzE,6BAA6B;IAC7B,MAAMmD,YAAYhD,qBAAqB;QACrCmC;QACAc,cAAc,CAACC,OAAOC,OACpBJ,gBAAgB,CAAC,eAAe,EAAEI,KAAKC,IAAI,CAAC,CAAC,CAAC,EAAE;gBAAEC,SAAS;YAAU;IACzE;IACA,MAAM,EAAEC,OAAO,EAAE,GAAGpE;IAEpB,MAAMqE,aAAa3D;IACnB;;;;;GAKC,GACD,MAAM,CAAC4D,WAAWC,aAAa,GAAG1C,SAChCG,qBAAqBwC,KAAK,CAAC,EAAE;IAE/B,MAAM,EACJC,MAAMC,YAAY,EAClBC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACL,GAAG9D,aAAkB;QACpBM,YAAYA,WAAWiC,WAAW,SAASR,QAAQ;QACnDgC,aAAajD;QACbkD,SAAS;YACPC,SAASd,WAAWc,OAAO;YAC3BC,QAAQf,WAAWgB,WAAW;YAC9BC,MAAMhB;YACNiB,MAAMzD;QACR;QACA0D,MAAM;YAAC/B;YAAWR;SAAO;QACzBwC,SAAS;IACX;IACA,MAAMC,aAAavE,kBAAuB;QACxCqD,OAAOxC,qBAAqBwC,KAAK;QACjCmB,aAAa3D,qBAAqBwC,KAAK,CAAC,EAAE;QAC1Cc,MAAMhB;QACNsB,cAAcrB;QACdsB,SAASb,KAAKa,OAAO;QACrBpB,MAAMC;QACNoB,WAAWzD;QACX0D,SAASvD;IACX;IACA,MAAMwD,YACJ3B,WAAWc,OAAO,CAACc,MAAM,GAAG,KAAK5B,WAAWgB,WAAW,CAACa,IAAI,CAAC,CAACC,OAASA,KAAKC,IAAI;IAClF,MAAMC,WAAWzE,QACf,IACExB,kBAAkB4E,KAAKsB,OAAO,EAAE;YAC9BC,QAAQvE,qBAAqBuE,MAAM;YACnCR,SAAShE;QACX,IACF;QAACiD,KAAKsB,OAAO;KAAC;IAGhB,MAAME,eAAe;QACnB,IAAI;YACF,MAAM,EAAE5D,QAAQ,EAAEC,SAAS,EAAE,GAAG,MAAMX,kBACpCe,QACAS,oBACAC;YAEF,IAAIjB,WAAWc,WAAW;gBACxB,KAAKI,OAAO6C,IAAI,CAAChE,aAAaC,SAASc,WAAWZ,UAAUC;YAC9D;QACF,EAAE,OAAO6D,OAAY;;YACnBC,QAAQD,KAAK,CAACA;YACd7C,wBAAgB6C,yBAAAA,MAAOE,OAAO,mBAAI,gCAAgC;gBAChEzC,SAAS;YACX;QACF;IACF;IAEA,MAAM0C,eAAe,OAAOC;YAIlBA;QAHR,MAAMC,YAAY,MAAM3C,QAAQ;YAC9B4C,OAAO;YACPC,aACE,CAAC,CAAC,GAAEH,sBAAAA,OAAOI,WAAW,YAAlBJ,sBAAsB,oBAAoB,mBAAmB,CAAC,GAClE;YACFK,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACR,WAAW;QAChB,MAAMrF,UACJD,IAAIgC,WAAW,SAASR,QAAQ,WAAW6D,OAAOU,GAAG,GACrDpF;IAEJ;IAEA,MAAMqF,eAAe,CAACX,SAAgB,GAAG3D,SAAS,WAAW,EAAE2D,OAAOU,GAAG,EAAE;IAE3E,MAAME,eAAe,CAACZ;YACbA;eAAPa,QAAOb,sBAAAA,OAAOI,WAAW,YAAlBJ,sBAAsB;;IAE/B,MAAMc,aAAa,CAACd,SAAsC;YACxD;gBACEe,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAChI;oBAAQiI,MAAMrI,cAAcqI,IAAI;oBAAEC,MAAM;;gBAC/CC,MAAMT,aAAaX;YACrB;YACA;gBACEe,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAChI;oBAAQiI,MAAMpI,iBAAiBoI,IAAI;oBAAEC,MAAM;;gBAClD;;;;;OAKC,GACDC,MACExF,WAAWc,YACPf,aAAaC,SAASc,WAAWsD,OAAOU,GAAG,EAAEV,OAAOjE,SAAS,IAC7DsF;gBACNC,UAAU,CAAC1F,WAAW,CAACc;gBACvB6E,gBAAgB;YAClB;YACA;gBACER,KAAK;gBACLC,OAAOlH;gBACPmH,oBAAM,KAAChI;oBAAQiI,MAAMtI,eAAesI,IAAI;oBAAEC,MAAM;;gBAChDK,SAAS,IACPxE,UAAUoB,OAAO,CAAC,eAAe;wBAC/B3B,IAAIuD,OAAOU,GAAG;wBACdtD,MAAMwD,aAAaZ;oBACrB;YACJ;YACA;gBACEe,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAChI;oBAAQiI,MAAMvI,iBAAiBuI,IAAI;oBAAEC,MAAM;;gBAClDM,aAAa;gBACbD,SAAS,IAAM,KAAKzB,aAAaC;YACnC;SACD;IAED;;GAEC,GACD,MAAM0B,UAAwB;QAC5B;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,aAAa,CAACC,QAAQvG,MAAQmF,aAAanF;YAC3CwG,YAAY,CAAC,EAAExG,GAAG,EAAEyG,KAAK,EAAE,iBACzB,KAACnJ;oBACCqI,MAAMT,aAAalF;oBACnB,+DAA+D;oBAC/D,4CAA4C;oBAC5C+F,SAAS,CAACW,QACRA,MAAMC,eAAe;8BAGtBF;;QAGP;QACA;YACE;;;;OAIC,GACDP,OAAO;YACPC,YAAY;YACZS,OAAO;YACPN,aAAa,CAACC,QAAQvG,MACpBN,mBAAmBM,KAAKD,MAAM,KAAK,cAAc,cAAc;YACjEyG,YAAY,CAAC,EAAEC,KAAK,EAAE,GACpBA,UAAU,4BACR,KAAC3H;oBAAK4G,MAAK;oBAAQH,OAAM;mCAEzB,KAACvG;oBAAW4C,SAAQ;oBAAQkD,OAAM;8BAC/B;;QAGT;QACA9G,kBACE,CAACgC,oBACC,KAAClC;gBAAeyH,OAAOJ,aAAanF;gBAAM6G,OAAOxB,WAAWrF;gBAE9D;YAAE4G,OAAO;QAAG;KAEf;IAED,qBACE,MAACrJ;QACCuJ,QAAQ;QACRC,MAAMhK,eAAe,kBAAkB;YAAEiK,QAAQ;QAAmB;QACpEC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,MAACrI;gBAAMsI,WAAU;gBAAMC,SAAS;;oBAC7BxG;kCACD,KAACjC;wBACC6G,MAAK;wBACL9D,SAAQ;wBACRmE,SAAS,IAAM,KAAK9B;kCAEnB;;;;QAIT;;YAEC1C,UAAUgG,MAAM;0BACjB,KAACxI;gBAAMuI,SAAS;0BACbnF,aAAauB,MAAM,KAAK,KAAK,CAACtB,WAAW,CAACqB,0BACzC;;sCACE,KAACzE;4BAAW4C,SAAQ;4BAAQkD,OAAM;sCAC/B,mEACC,mEACA;;sCAGJ,MAAC/F;4BAAMsI,WAAU;4BAAMC,SAAS;;gCAC7BxG;8CACD,KAACjC;oCAAO+C,SAAQ;oCAAYmE,SAAS,IAAM,KAAK9B;8CAC7C;;;;;mCAKP;;sCACE,KAACvG;4BACCsG,QAAQvE,qBAAqBuE,MAAM;4BACnCR,SAAShE;4BACToD,SAASd,WAAWc,OAAO;4BAC3B4E,UAAU1F,WAAW2F,UAAU;;sCAEjC,KAAC7J;4BACCmG,SAASD;4BACT4D,SAAS;mCAAIjF,KAAKiF,OAAO;mCAAKvE,WAAWuE,OAAO;6BAAC;;sCAEnD,KAAC3J;4BACC4J,cAAW;4BACXzF,MAAMiB,WAAWjB,IAAI;4BACrBiB,YAAYA;4BACZ8C,SAAShI,sBACPgI,SACAxG,qBAAqBuE,MAAM,EAC3B,CAAC,GACDxE;4BAEFoI,WAAW1J;4BACX2J,QAAQ,CAACC,KAAK9H,MAAQqB,OAAO6C,IAAI,CAACgB,aAAalF;4BAC/C,8DAA8D;4BAC9D+H,UAAU;4BACV,0DAA0D;4BAC1D,8CAA8C;4BAC9CC,YAAW;4BACXC,aAAanG,WAAWmG,WAAW;4BACnCC,qBAAqBpG,WAAWoG,mBAAmB;4BACnDC,WAAW;4BACXC,aAAY;;sCAEd,KAACzK;4BACC0E,MAAMA;4BACNE,UAAUA;4BACV8F,UAAUlG,aAAauB,MAAM;4BAC7BtB,SAASA;4BACTkG,cAAchG;4BACdiG,kBAAkB/F;;;;;;;AAOhC;AACAhC,iBAAiBmE,WAAW,GAAG;AAE/B,eAAenE,iBAAgB"}
|
|
@@ -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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"}
|