@aglyn/plugins-email 1.0.0-beta.234 → 1.0.0-beta.235

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +16 -16
  2. package/src/lib/components/email-screens-card.js +2 -2
  3. package/src/lib/components/email-screens-card.js.map +1 -1
  4. package/src/lib/components/email-template-detail.js +2 -4
  5. package/src/lib/components/email-template-detail.js.map +1 -1
  6. package/src/lib/components/email-topic-detail.js +1 -1
  7. package/src/lib/components/email-topic-detail.js.map +1 -1
  8. package/src/lib/components/list-detail-card.js +5 -6
  9. package/src/lib/components/list-detail-card.js.map +1 -1
  10. package/src/lib/components/list-edit-card.js +1 -1
  11. package/src/lib/components/list-edit-card.js.map +1 -1
  12. package/src/lib/components/lists-card.js +2 -2
  13. package/src/lib/components/lists-card.js.map +1 -1
  14. package/src/lib/components/org-email-templates-card.js +4 -4
  15. package/src/lib/components/org-email-templates-card.js.map +1 -1
  16. package/src/lib/components/org-sending-card.js +1 -1
  17. package/src/lib/components/org-sending-card.js.map +1 -1
  18. package/src/lib/components/org-sending-domain-detail.js +1 -1
  19. package/src/lib/components/org-sending-domain-detail.js.map +1 -1
  20. package/src/lib/components/org-suppressions-card.js +1 -1
  21. package/src/lib/components/org-suppressions-card.js.map +1 -1
  22. package/src/lib/components/sending-domain-detail.js +1 -1
  23. package/src/lib/components/sending-domain-detail.js.map +1 -1
  24. package/src/lib/components/sending-domains-card.js +1 -3
  25. package/src/lib/components/sending-domains-card.js.map +1 -1
  26. package/src/lib/components/sending-sender-drawer.js +1 -3
  27. package/src/lib/components/sending-sender-drawer.js.map +1 -1
  28. package/src/lib/components/site-consent-group-card.js +1 -1
  29. package/src/lib/components/site-consent-group-card.js.map +1 -1
  30. package/src/lib/components/suppressions-card.js +1 -1
  31. package/src/lib/components/suppressions-card.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-edit-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\n/**\n * A LIST'S SETTINGS: its name, how its membership is decided, and the filters\n * that decide it.\n *\n * ## Why it is its own route\n *\n * Editing the rule that decides an audience is not a thing to do in a row of a\n * table. It is nine fields, several of them consequential enough that the\n * merchant should see what they currently say before changing them, and a\n * rule saved by accident silently re-populates the audience on the next sweep.\n * A route also means the edit is linkable and the back button leaves it.\n *\n * ## The rule is edited as a DRAFT and written once\n *\n * The controls hold text; `draftToRule` turns it into a rule; Save writes that\n * rule. Nothing is written per keystroke, because a dynamic list's rule is\n * read by a sweep every fifteen minutes, which would otherwise re-materialize\n * the audience against a half-typed intermediate state.\n *\n * ## The two outcomes, and the sentence that tells them apart\n *\n * The SAME filters serve both kinds of list, and the difference is what\n * happens afterwards:\n *\n * - **Live** keeps the rule. Membership is re-evaluated on the sweep and the\n * audience keeps growing as people start matching.\n * - **Fixed** does not. The filters are a way to FIND people; who is on the\n * list is whoever was added, and it stays that way.\n *\n * A reader must never be unsure which they have, so the control says it in\n * those words and the page repeats it under whichever is selected. Switching\n * is safe in both directions and neither drops anybody: the sweep reconciles\n * away only rows IT wrote (`via: 'rule'`), so a person somebody added by hand\n * survives becoming live, and everyone on the list survives becoming fixed.\n * The rule is kept on a fixed list rather than deleted — it is the filter that\n * found these people, and losing it on a toggle would lose the work.\n *\n * ## Whose people the filters read is a choice, not a side effect\n *\n * A list is the organization's and its rule reads ONE site's leads, members\n * and form submissions — the list's `hostId`. That is written when somebody\n * chooses it and at no other time. A rename saved from another site's page\n * must not move the filters onto that site's people: the next sweep would\n * re-draw the whole audience from somebody else's customers. So the page\n * reads the stored site, offers the change in words, and writes `hostId` only\n * when it differs from what is stored — which is also how a list that never\n * had one gets the site it is first set up on.\n */\n\nimport {\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useRef, useState } from 'react'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport DynamicListRuleFields, {\n draftToRule,\n useRuleDraft,\n} from './dynamic-list-rule-fields'\nimport {\n OrgSiteSelect,\n useEmailDataScope,\n useEmailOrgMount,\n} from './email-org-mount'\n\nexport interface ListEditCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n listId: string\n /** The emails hub URL, which the audiences routes hang beneath. */\n basePath: string\n}\n\nexport function ListEditCard(props: ListEditCardProps) {\n const { hostId, listId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const orgMount = useEmailOrgMount()\n const { scope } = useEmailDataScope(hostId)\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null),\n [firestore, scope, listId],\n )\n\n const storedName = String(list?.['name'] ?? '')\n const storedKind: 'manual' | 'dynamic' =\n list?.['kind'] === 'dynamic' ? 'dynamic' : 'manual'\n /*\n * Normalized before the form ever sees it, by the same function the\n * materializer reads it back through. A rule shown one way and evaluated\n * another is the defect this screen exists to make visible.\n */\n const storedRule = list ? normalizeDynamicListRule(list['rule']) : undefined\n\n const [name, setName] = useState('')\n const [kind, setKind] = useState<'manual' | 'dynamic'>('manual')\n const dynamic = kind === 'dynamic'\n /*\n * Seeded ONCE, when the document first arrives. The listen is live, so\n * re-seeding on every snapshot would take the field away from whoever is\n * typing in it — including on the snapshot their own save produces.\n */\n const seeded = useRef(false)\n useEffect(() => {\n if (!list || seeded.current) return\n seeded.current = true\n setName(storedName)\n setKind(storedKind)\n }, [list, storedName, storedKind])\n\n const [draft, setDraft] = useRuleDraft(storedRule, listId)\n const [busy, setBusy] = useState(false)\n\n /*\n * WHOSE PEOPLE THE FILTERS READ — see the file header.\n *\n * The stored site stands until somebody chooses another. A list with none\n * yet starts on the site whose page this is, or on the organization's page\n * on the reader's pick this session (or the only site). A stored site the\n * org's page does not list — one since deleted — is not offered as the\n * answer there, so the reader chooses a live one.\n */\n const storedHostId = String(list?.['hostId'] ?? '')\n const [chosenHostId, setChosenHostId] = useState('')\n const storedIsOffered = orgMount\n ? orgMount.hosts.some((site) => site.id === storedHostId)\n : Boolean(storedHostId)\n const ruleHostId: string =\n chosenHostId ||\n (storedIsOffered ? storedHostId : '') ||\n (hostId ?? orgMount?.pickedHostId ?? '')\n /** Under a site, the filters were last set up on a different one. */\n const drawsFromAnotherSite =\n hostId != null && Boolean(storedHostId) && storedHostId !== hostId\n\n const audiencesHref = `${basePath}/audiences`\n const detailHref = `${audiencesHref}/${listId}`\n\n const handleSave = async () => {\n const next = name.trim()\n /*\n * An empty name is not a rename. The list table orders on `name`, and\n * `orderBy` drops a document that does not carry the field — so a list\n * renamed to nothing would vanish from the page that lists it.\n */\n if (!next || !scope || busy) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, scope[0], scope[1], 'lists', listId), {\n // The name and the search keys the lists table queries, together\n // (AGL-3321).\n ...nameSearchFields(next),\n kind,\n /*\n * The rule is written whichever kind was chosen. On a live list it is\n * what the sweep evaluates; on a fixed one it is the filter that finds\n * people to add, and keeping it is what lets a merchant refine the\n * search across visits instead of retyping it.\n */\n rule: draftToRule(draft),\n /*\n * WHICH SITE'S people the rule draws from.\n *\n * Lists are org-shared but leads, members and form submissions are\n * host-owned, and org contacts are read narrowed to one host. A rule\n * with no host has no silos at all, so the sweep skips it. Written\n * only when it changes — see the file header.\n */\n ...(ruleHostId && ruleHostId !== storedHostId\n ? { hostId: ruleHostId }\n : {}),\n })\n enqueueSnackbar('List saved', { variant: 'success', persist: false })\n router.push(detailHref)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The list was not saved', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }\n\n const headerActions = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Back to the audience'}\n </Button>\n )\n\n if (!list) {\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Edit list'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Edit audience'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: dynamic ? '#lists-built-from-a-rule' : '#manual-lists',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <TextField\n size=\"small\"\n label=\"List name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n sx={{ flexGrow: 1, minWidth: 240, maxWidth: 360 }}\n />\n {/*\n The one decision on this page that changes what the list DOES, so\n the options are named for their outcome rather than for the field\n they set. \"Dynamic\" and \"manual\" describe the implementation; \"keeps\n growing\" and \"does not change\" describe what the merchant gets.\n */}\n <TextField\n select\n size=\"small\"\n label=\"Membership\"\n value={kind}\n onChange={(event) =>\n setKind(event.target.value as 'manual' | 'dynamic')\n }\n sx={{ minWidth: 240 }}\n >\n <MenuItem value=\"dynamic\">{'Live — keeps growing'}</MenuItem>\n <MenuItem value=\"manual\">{'Fixed — does not change'}</MenuItem>\n </TextField>\n </Stack>\n\n <Alert severity={dynamic ? 'info' : 'success'}>\n {dynamic\n ? 'Everyone matching these filters is enrolled automatically, ' +\n 'about every fifteen minutes, and leaves when they stop ' +\n 'matching. Anyone added by hand stays. Being matched is not ' +\n 'consent to be emailed — a campaign still only reaches people ' +\n 'whose consent is on record.'\n : 'Membership does not change on its own. Use the filters below ' +\n 'to FIND people, then add them from the audience page — who ' +\n 'you add is who stays on the list.'}\n </Alert>\n\n {/*\n WHOSE PEOPLE, said before the filters that read them. On the org\n page it is a picker. Under a site it is said only when the stored\n site is another one, with the one control that moves it here.\n */}\n {orgMount ? (\n <OrgSiteSelect\n mount={orgMount}\n label=\"Site\"\n value={ruleHostId}\n onChange={setChosenHostId}\n helperText={\n 'The site whose leads, members and form submissions these ' +\n 'filters read. A list draws from one site’s people.'\n }\n />\n ) : drawsFromAnotherSite ? (\n <Alert\n severity=\"info\"\n action={\n <Button\n color=\"inherit\"\n size=\"small\"\n onClick={() =>\n setChosenHostId(chosenHostId ? '' : String(hostId))\n }\n >\n {chosenHostId ? 'Keep the other site' : 'Use this site'}\n </Button>\n }\n >\n {chosenHostId\n ? 'Saving moves these filters onto this site’s people. The ' +\n 'next sweep re-draws the audience from them.'\n : 'These filters read another site’s people — the site this ' +\n 'list was set up on. Saving here keeps it that way.'}\n </Alert>\n ) : null}\n\n {scope && ruleHostId ? (\n <DynamicListRuleFields\n key={ruleHostId}\n scope={scope as readonly [string, string]}\n hostId={ruleHostId}\n draft={draft}\n onChange={setDraft}\n listId={listId}\n />\n ) : orgMount ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Choose the site whose people these filters read to edit them.'}\n </Typography>\n ) : null}\n {/*\n A rule with no source matches nobody, and an audience emptied that\n way looks exactly like one whose sweep has not run yet. It is only a\n warning on a LIVE list: on a fixed one an unset filter selects\n nothing to add and takes nobody off.\n */}\n {draft.sources.length || !dynamic ? null : (\n <Alert severity=\"warning\">\n {'These filters draw from no source, so they match nobody. The ' +\n 'next sweep would empty the list of everyone the rule enrolled.'}\n </Alert>\n )}\n\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !name.trim() || !scope}\n onClick={() => void handleSave()}\n >\n {busy ? 'Saving…' : 'Save'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n >\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/*\n The audience being edited. This route is one level BELOW it, and the\n way back is the card header's own button — the trail names where the\n reader is and does not link its last step.\n */}\n <PageHeaderRecord title={storedName || listId} />\n {card}\n </>\n )\n}\nListEditCard.displayName = 'ListEditCard'\n\nexport default ListEditCard\n"],"names":["normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","nameSearchFields","AppLink","CardDisplay","useSnackbar","Alert","Button","MenuItem","Stack","TextField","Typography","doc","updateDoc","useRouter","useEffect","useRef","useState","useFirestore","useFirestoreDoc","DynamicListRuleFields","draftToRule","useRuleDraft","OrgSiteSelect","useEmailDataScope","useEmailOrgMount","ListEditCard","props","hostId","listId","basePath","firestore","router","enqueueSnackbar","orgMount","scope","data","list","status","storedName","String","storedKind","storedRule","undefined","name","setName","kind","setKind","dynamic","seeded","current","draft","setDraft","busy","setBusy","storedHostId","chosenHostId","setChosenHostId","storedIsOffered","hosts","some","site","id","Boolean","ruleHostId","pickedHostId","drawsFromAnotherSite","audiencesHref","detailHref","handleSave","next","trim","rule","variant","persist","push","error","console","headerActions","component","componentVariant","nativeButton","href","size","color","settled","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","card","contentBordered","spacing","direction","useFlexGap","sx","flexWrap","label","value","onChange","event","target","flexGrow","minWidth","maxWidth","select","severity","mount","helperText","onClick","sources","length","disabled","title","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/list-edit-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\n/**\n * A LIST'S SETTINGS: its name, how its membership is decided, and the filters\n * that decide it.\n *\n * ## Why it is its own route\n *\n * Editing the rule that decides an audience is not a thing to do in a row of a\n * table. It is nine fields, several of them consequential enough that the\n * merchant should see what they currently say before changing them, and a\n * rule saved by accident silently re-populates the audience on the next sweep.\n * A route also means the edit is linkable and the back button leaves it.\n *\n * ## The rule is edited as a DRAFT and written once\n *\n * The controls hold text; `draftToRule` turns it into a rule; Save writes that\n * rule. Nothing is written per keystroke, because a dynamic list's rule is\n * read by a sweep every fifteen minutes, which would otherwise re-materialize\n * the audience against a half-typed intermediate state.\n *\n * ## The two outcomes, and the sentence that tells them apart\n *\n * The SAME filters serve both kinds of list, and the difference is what\n * happens afterwards:\n *\n * - **Live** keeps the rule. Membership is re-evaluated on the sweep and the\n * audience keeps growing as people start matching.\n * - **Fixed** does not. The filters are a way to FIND people; who is on the\n * list is whoever was added, and it stays that way.\n *\n * A reader must never be unsure which they have, so the control says it in\n * those words and the page repeats it under whichever is selected. Switching\n * is safe in both directions and neither drops anybody: the sweep reconciles\n * away only rows IT wrote (`via: 'rule'`), so a person somebody added by hand\n * survives becoming live, and everyone on the list survives becoming fixed.\n * The rule is kept on a fixed list rather than deleted — it is the filter that\n * found these people, and losing it on a toggle would lose the work.\n *\n * ## Whose people the filters read is a choice, not a side effect\n *\n * A list is the organization's and its rule reads ONE site's leads, members\n * and form submissions — the list's `hostId`. That is written when somebody\n * chooses it and at no other time. A rename saved from another site's page\n * must not move the filters onto that site's people: the next sweep would\n * re-draw the whole audience from somebody else's customers. So the page\n * reads the stored site, offers the change in words, and writes `hostId` only\n * when it differs from what is stored — which is also how a list that never\n * had one gets the site it is first set up on.\n */\n\nimport {\n normalizeDynamicListRule,\n PageHeaderRecord,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useRef, useState } from 'react'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport DynamicListRuleFields, {\n draftToRule,\n useRuleDraft,\n} from './dynamic-list-rule-fields'\nimport {\n OrgSiteSelect,\n useEmailDataScope,\n useEmailOrgMount,\n} from './email-org-mount'\n\nexport interface ListEditCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n listId: string\n /** The emails hub URL, which the audiences routes hang beneath. */\n basePath: string\n}\n\nexport function ListEditCard(props: ListEditCardProps) {\n const { hostId, listId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const orgMount = useEmailOrgMount()\n const { scope } = useEmailDataScope(hostId)\n\n const { data: list, status } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'lists', listId) : null),\n [firestore, scope, listId],\n )\n\n const storedName = String(list?.['name'] ?? '')\n const storedKind: 'manual' | 'dynamic' =\n list?.['kind'] === 'dynamic' ? 'dynamic' : 'manual'\n /*\n * Normalized before the form ever sees it, by the same function the\n * materializer reads it back through. A rule shown one way and evaluated\n * another is the defect this screen exists to make visible.\n */\n const storedRule = list ? normalizeDynamicListRule(list['rule']) : undefined\n\n const [name, setName] = useState('')\n const [kind, setKind] = useState<'manual' | 'dynamic'>('manual')\n const dynamic = kind === 'dynamic'\n /*\n * Seeded ONCE, when the document first arrives. The listen is live, so\n * re-seeding on every snapshot would take the field away from whoever is\n * typing in it — including on the snapshot their own save produces.\n */\n const seeded = useRef(false)\n useEffect(() => {\n if (!list || seeded.current) return\n seeded.current = true\n setName(storedName)\n setKind(storedKind)\n }, [list, storedName, storedKind])\n\n const [draft, setDraft] = useRuleDraft(storedRule, listId)\n const [busy, setBusy] = useState(false)\n\n /*\n * WHOSE PEOPLE THE FILTERS READ — see the file header.\n *\n * The stored site stands until somebody chooses another. A list with none\n * yet starts on the site whose page this is, or on the organization's page\n * on the reader's pick this session (or the only site). A stored site the\n * org's page does not list — one since deleted — is not offered as the\n * answer there, so the reader chooses a live one.\n */\n const storedHostId = String(list?.['hostId'] ?? '')\n const [chosenHostId, setChosenHostId] = useState('')\n const storedIsOffered = orgMount\n ? orgMount.hosts.some((site) => site.id === storedHostId)\n : Boolean(storedHostId)\n const ruleHostId: string =\n chosenHostId ||\n (storedIsOffered ? storedHostId : '') ||\n (hostId ?? orgMount?.pickedHostId ?? '')\n /** Under a site, the filters were last set up on a different one. */\n const drawsFromAnotherSite =\n hostId != null && Boolean(storedHostId) && storedHostId !== hostId\n\n const audiencesHref = `${basePath}/audiences`\n const detailHref = `${audiencesHref}/${listId}`\n\n const handleSave = async () => {\n const next = name.trim()\n /*\n * An empty name is not a rename. The list table orders on `name`, and\n * `orderBy` drops a document that does not carry the field — so a list\n * renamed to nothing would vanish from the page that lists it.\n */\n if (!next || !scope || busy) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, scope[0], scope[1], 'lists', listId), {\n // The name and the search keys the lists table queries, together\n // (AGL-3321).\n ...nameSearchFields(next),\n kind,\n /*\n * The rule is written whichever kind was chosen. On a live list it is\n * what the sweep evaluates; on a fixed one it is the filter that finds\n * people to add, and keeping it is what lets a merchant refine the\n * search across visits instead of retyping it.\n */\n rule: draftToRule(draft),\n /*\n * WHICH SITE'S people the rule draws from.\n *\n * Lists are org-shared but leads, members and form submissions are\n * host-owned, and org contacts are read narrowed to one host. A rule\n * with no host has no silos at all, so the sweep skips it. Written\n * only when it changes — see the file header.\n */\n ...(ruleHostId && ruleHostId !== storedHostId\n ? { hostId: ruleHostId }\n : {}),\n })\n enqueueSnackbar('List saved', { variant: 'success', persist: false })\n router.push(detailHref)\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The list was not saved', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }\n\n const headerActions = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Back to the audience'}\n </Button>\n )\n\n if (!list) {\n const settled = Boolean(scope) && status !== 'loading'\n return (\n <CardDisplay\n header={'Edit list'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#lists-built-from-a-rule' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: headerActions }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'This list could not be loaded. It may have been deleted.'\n : 'Loading this audience…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Edit audience'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: dynamic ? '#lists-built-from-a-rule' : '#manual-lists',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{ action: headerActions }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <TextField\n size=\"small\"\n label=\"List name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n sx={{ flexGrow: 1, minWidth: 240, maxWidth: 360 }}\n />\n {/*\n The one decision on this page that changes what the list DOES, so\n the options are named for their outcome rather than for the field\n they set. \"Dynamic\" and \"manual\" describe the implementation; \"keeps\n growing\" and \"does not change\" describe what the merchant gets.\n */}\n <TextField\n select\n size=\"small\"\n label=\"Membership\"\n value={kind}\n onChange={(event) =>\n setKind(event.target.value as 'manual' | 'dynamic')\n }\n sx={{ minWidth: 240 }}\n >\n <MenuItem value=\"dynamic\">{'Live — keeps growing'}</MenuItem>\n <MenuItem value=\"manual\">{'Fixed — does not change'}</MenuItem>\n </TextField>\n </Stack>\n\n <Alert severity={dynamic ? 'info' : 'success'}>\n {dynamic\n ? 'Everyone matching these filters is enrolled automatically, ' +\n 'about every fifteen minutes, and leaves when they stop ' +\n 'matching. Anyone added by hand stays. Being matched is not ' +\n 'consent to be emailed — a campaign still only reaches people ' +\n 'whose consent is on record.'\n : 'Membership does not change on its own. Use the filters below ' +\n 'to FIND people, then add them from the audience page — who ' +\n 'you add is who stays on the list.'}\n </Alert>\n\n {/*\n WHOSE PEOPLE, said before the filters that read them. On the org\n page it is a picker. Under a site it is said only when the stored\n site is another one, with the one control that moves it here.\n */}\n {orgMount ? (\n <OrgSiteSelect\n mount={orgMount}\n label=\"Site\"\n value={ruleHostId}\n onChange={setChosenHostId}\n helperText={\n 'The site whose leads, members and form submissions these ' +\n 'filters read. A list draws from one site’s people.'\n }\n />\n ) : drawsFromAnotherSite ? (\n <Alert\n severity=\"info\"\n action={\n <Button\n color=\"inherit\"\n size=\"small\"\n onClick={() =>\n setChosenHostId(chosenHostId ? '' : String(hostId))\n }\n >\n {chosenHostId ? 'Keep the other site' : 'Use this site'}\n </Button>\n }\n >\n {chosenHostId\n ? 'Saving moves these filters onto this site’s people. The ' +\n 'next sweep re-draws the audience from them.'\n : 'These filters read another site’s people — the site this ' +\n 'list was set up on. Saving here keeps it that way.'}\n </Alert>\n ) : null}\n\n {scope && ruleHostId ? (\n <DynamicListRuleFields\n key={ruleHostId}\n scope={scope as readonly [string, string]}\n hostId={ruleHostId}\n draft={draft}\n onChange={setDraft}\n listId={listId}\n />\n ) : orgMount ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Choose the site whose people these filters read to edit them.'}\n </Typography>\n ) : null}\n {/*\n A rule with no source matches nobody, and an audience emptied that\n way looks exactly like one whose sweep has not run yet. It is only a\n warning on a LIVE list: on a fixed one an unset filter selects\n nothing to add and takes nobody off.\n */}\n {draft.sources.length || !dynamic ? null : (\n <Alert severity=\"warning\">\n {'These filters draw from no source, so they match nobody. The ' +\n 'next sweep would empty the list of everyone the rule enrolled.'}\n </Alert>\n )}\n\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !name.trim() || !scope}\n onClick={() => void handleSave()}\n >\n {busy ? 'Saving…' : 'Save'}\n </Button>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={detailHref}\n size=\"small\"\n >\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n\n return (\n <>\n {/*\n The audience being edited. This route is one level BELOW it, and the\n way back is the card header's own button — the trail names where the\n reader is and does not link its last step.\n */}\n <PageHeaderRecord title={storedName || listId} />\n {card}\n </>\n )\n}\nListEditCard.displayName = 'ListEditCard'\n\nexport default ListEditCard\n"],"names":["normalizeDynamicListRule","PageHeaderRecord","pluginDocsHelp","nameSearchFields","AppLink","CardDisplay","useSnackbar","Alert","Button","MenuItem","Stack","TextField","Typography","doc","updateDoc","useRouter","useEffect","useRef","useState","useFirestore","useFirestoreDoc","DynamicListRuleFields","draftToRule","useRuleDraft","OrgSiteSelect","useEmailDataScope","useEmailOrgMount","ListEditCard","props","hostId","listId","basePath","firestore","router","enqueueSnackbar","orgMount","scope","data","list","status","storedName","String","storedKind","storedRule","undefined","name","setName","kind","setKind","dynamic","seeded","current","draft","setDraft","busy","setBusy","storedHostId","chosenHostId","setChosenHostId","storedIsOffered","hosts","some","site","id","Boolean","ruleHostId","pickedHostId","drawsFromAnotherSite","audiencesHref","detailHref","handleSave","next","trim","rule","variant","persist","push","error","console","headerActions","component","componentVariant","nativeButton","href","size","color","settled","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","card","contentBordered","spacing","direction","useFlexGap","sx","flexWrap","label","value","onChange","event","target","flexGrow","minWidth","maxWidth","select","severity","mount","helperText","onClick","sources","length","disabled","title","displayName"],"mappings":"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"}
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { createResourceUid, pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { nameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
21
- import { mdiDeleteOutline, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
21
+ import { mdiDeleteOutline, mdiPageNextOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
22
22
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
23
23
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
24
24
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
@@ -274,7 +274,7 @@ const LIST_SELECT_FIELDS = [
274
274
  key: 'details',
275
275
  label: 'Open details',
276
276
  icon: /*#__PURE__*/ _jsx(MdiIcon, {
277
- path: mdiEyeOutline.path,
277
+ path: mdiPageNextOutline.path,
278
278
  size: 0.8
279
279
  }),
280
280
  href: listHref(list)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/lists-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 { createResourceUid, pluginDocsHelp } from '@aglyn/aglyn'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n mdiDeleteOutline,\n mdiEyeOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } 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 { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n setDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } 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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n EMAIL_LIST_FILTER_HEADERS,\n EMAIL_LIST_FILTER_OPTIONS,\n EMAIL_LIST_QUERY,\n} from '../constants/list-queries'\nimport { useEmailDataScope } from './email-org-mount'\n\nexport interface OrgListsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every list route hangs beneath. */\n basePath: string\n}\n\n/** The Membership filter's choices are picked, so the panel shows a select. */\n/** The page-sorted header, for its notice (AGL-3680). */\nconst LIST_SORT_HEADERS = { subscribers: 'Subscribers' }\nconst LIST_SELECT_FIELDS = ['kind']\n\n/**\n * Email lists (AGL-254): static audiences at `orgs/{orgId}/lists`, shared\n * across the org's sites. Members arrive via the enrollList automation\n * step, popup email capture, or manual adds; campaigns target a list from\n * the audience picker.\n *\n * ## A list is a resource, so it has pages\n *\n * The row opens `…/audiences/{listId}`, and everything a list can be asked or\n * told lives there or on its edit page. The membership used to expand INSIDE\n * this table, which made three things impossible at once: a merchant could not\n * link anybody to one audience, the browser's back button did not walk out of\n * a list, and the rename lived under a table it was not about.\n *\n * Row click and the row's own link are two affordances rather than one. A\n * click handler cannot be middle-clicked, copied, or opened from the browser's\n * context menu, and a link alone loses the whole-row target.\n */\nexport function OrgListsCard(props: OrgListsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const { data: user } = useUser()\n // The org lookup is async (AGL-1061): `scope` is null until it settles,\n // and stays null for a host with no owning org. Lists have always been\n // org-shared (AGL-254) — there is no host path to fall back to any more\n // (AGL-1050), so creating one is held rather than misdirected. On the\n // organization's page the mount names the org and nothing is looked up.\n const { scope } = useEmailDataScope(hostId)\n\n /*\n * Ordered by the server and paged, rather than capped and re-sorted here\n * (AGL-2501, AGL-2292).\n *\n * `limit(50)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids from `createResourceUid()` — an arbitrary\n * fifty of the org's lists, which the `localeCompare` below then arranged\n * alphabetically. An agency past fifty lists saw a believable A-to-Z page\n * that was missing most of the alphabet, and the campaign composer's\n * audience picker reads the same collection.\n *\n * `name` is safe to order on here, which is a claim about the writers and\n * not a preference: this card is the only thing that CREATES a list\n * (`handleCreate` refuses an empty name), the automation step and the\n * newsletter enrollment both resolve an existing list and deliberately\n * never create one, and `lists` is absent from `IMPORTABLE_FIELDS`, so no\n * restore path can produce a nameless document for `orderBy` to drop.\n *\n * Every filter and search word is a predicate on the same query\n * (AGL-3321), so a page is a page of the matches — see `EMAIL_LIST_QUERY`.\n */\n const gridFilter = useListGridFilter({ selectFields: LIST_SELECT_FIELDS })\n /*\n * EVERY HEADER SORTS (AGL-3680). List and Membership order the QUERY (see\n * `EMAIL_LIST_QUERY`); Subscribers is counted per row after the page\n * loads, so it sorts the page on screen and says so.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(\n EMAIL_LIST_QUERY.sorts[0],\n )\n const {\n rows: lists,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: scope ? collection(firestore, scope[0], scope[1], 'lists') : null,\n declaration: EMAIL_LIST_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n },\n deps: [firestore, scope?.[0], scope?.[1]],\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_LIST_QUERY.fields,\n headers: EMAIL_LIST_FILTER_HEADERS,\n options: EMAIL_LIST_FILTER_OPTIONS,\n }),\n [plan.refused],\n )\n const [counts, setCounts] = useState<Record<string, number>>({})\n useEffect(() => {\n // `lists` can only be non-empty when `scope` resolved, but the\n // effect must say so for itself — it reads `scope` outside the query.\n if (!scope) return undefined\n let active = true\n void Promise.all(\n lists.map(async (list: any) => {\n try {\n const snapshot = await getCountFromServer(\n collection(\n firestore,\n scope[0],\n scope[1],\n 'lists',\n list.$id,\n 'members',\n ),\n )\n return [list.$id, snapshot.data().count] as const\n } catch {\n return [list.$id, 0] as const\n }\n }),\n ).then((entries) => {\n if (active) setCounts(Object.fromEntries(entries))\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, scope, JSON.stringify(lists.map((l: any) => l.$id))])\n const pageSorts = useMemo(\n () => ({ subscribers: (row: any) => counts[row.$id] }),\n [counts],\n )\n const columnSort = useListColumnSort<any>({\n sorts: EMAIL_LIST_QUERY.sorts,\n defaultSort: EMAIL_LIST_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: lists,\n pageSorts,\n headers: LIST_SORT_HEADERS,\n })\n\n /*\n * Creating is a DRAWER, and the drawer is the shared one.\n *\n * The card used to carry a name box, a membership select and a Create\n * button stacked above the table. A create form wedged over a list has\n * nowhere to grow, which is how the rule behind a dynamic list came to be\n * authored through four controls out of nine — the form could not hold the\n * rest. Naming happens here; everything the audience IS happens on its edit\n * page, which has the room.\n */\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<unknown>(null)\n const [creating, setCreating] = useState(false)\n\n const handleCreate = async (values: Record<string, any>) => {\n const name = String(values?.['displayName'] ?? '').trim()\n // Without an org there is nowhere org-shared to put the list, and the\n // host path that used to absorb it is gone (AGL-1050). The button is\n // disabled in this state; this is the same answer for a stale closure.\n if (!name || !scope || creating) return\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n try {\n await setDoc(doc(firestore, scope[0], scope[1], 'lists', id), {\n // The name and the search keys the table queries (AGL-3321).\n ...nameSearchFields(name),\n /*\n * Manual until somebody says otherwise.\n *\n * A list arrives holding the people you put on it, and becomes\n * rule-driven on its edit page where the rule can actually be\n * written. Offering the choice in the naming drawer would ask for a\n * decision before the thing it decides — a rule — can be seen.\n */\n kind: 'manual',\n createdAt: Timestamp.now(),\n })\n setCreateOpen(false)\n enqueueSnackbar(`List \"${name}\" created`, {\n variant: 'success',\n persist: false,\n })\n router.push(`${basePath}/audiences/${id}`)\n } catch (error) {\n console.error(error)\n setCreateError(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setCreating(false)\n }\n }\n\n const handleDelete = async (list: any) => {\n if (!scope) return\n const accepted = await confirm({\n title: 'Delete list?',\n description: `\"${list.name}\" and its enrollments stop being targetable; automations enrolling into it start reporting errors.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating on\n // the resolved value alone made this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n // The list owns a `members` subcollection of enrolled contacts, and\n // Firestore doesn't cascade — deleting the doc from here left that PII\n // behind, unreachable but still readable to every org member. Only the\n // Admin SDK can recursiveDelete, so this goes through the erase route\n // (AGL-946).\n try {\n const response = await authorizedFetch(user, '/api/resources/erase', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n scope: scope[0],\n scopeId: scope[1],\n kind: 'lists',\n id: list.$id,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) throw new Error(result?.error ?? 'List delete failed')\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'List delete failed', {\n variant: 'error',\n })\n }\n enqueueSnackbar('List deleted', { variant: 'success', persist: false })\n }\n\n const listHref = (list: any) => `${basePath}/audiences/${list.$id}`\n\n const rowActions = (list: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: listHref(list),\n },\n {\n key: 'edit',\n label: 'Edit list',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: `${listHref(list)}/edit`,\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(list),\n },\n ]\n\n /*\n * One row per list. The name is a link AND the row opens the list: a click\n * handler cannot be middle-clicked, copied or opened from the context menu,\n * and a link alone loses the whole-row target.\n */\n const columns: GridColDef[] = [\n {\n field: 'name',\n headerName: 'List',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <AppLink\n href={listHref(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 {row.name}\n </AppLink>\n ),\n },\n {\n field: 'kind',\n headerName: 'Membership',\n flex: 1,\n minWidth: 220,\n // A list that predates `kind` is a manual one.\n valueGetter: (_value, row) => (row.kind === 'dynamic' ? 'dynamic' : 'manual'),\n renderCell: ({ row }) =>\n row.kind === 'dynamic' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n /*\n Freshness, not just kind. A dynamic list that has stopped being\n swept looks exactly like one whose population has not changed,\n so the last evaluation time is the only thing that tells them\n apart — and it is the first thing to look at when a merchant\n reports that somebody is missing from an audience.\n */\n label={\n row.lastEvaluatedAt?.toDate\n ? `Rule · ${row.lastEvaluatedAt.toDate().toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Manual\" />\n ),\n },\n {\n field: 'subscribers',\n headerName: 'Subscribers',\n width: 130,\n valueGetter: (_value, row) => counts[row.$id] ?? '…',\n },\n listActionsColumn(\n (row) => (\n <ListRowActions\n label={String(row.name ?? row.$id)}\n items={rowActions(row)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Email lists\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!scope || creating}\n onClick={() => setCreateOpen(true)}\n >\n {creating ? 'Creating…' : 'Create audience'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Audiences shared across your organization. A manual list holds ' +\n 'the people you enroll into it. A dynamic list holds everyone ' +\n 'matching a rule, re-checked about every fifteen minutes. Target ' +\n 'either from the campaign composer.'}\n </Typography>\n {/*\n A list document stores a name and nothing else descriptive, so the\n shared Description box is refused: a field the writer discards on\n write is worse than a field that was never offered.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new audience\"\n submitLabel=\"Create audience\"\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {'This audience could not be created.'}\n </Alert>\n ) : null\n }\n includeDescription={false}\n />\n {lists.length === 0 && !hasMore && !filtering ? null : (\n <>\n <ListFilterChips\n fields={EMAIL_LIST_QUERY.fields}\n headers={EMAIL_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={EMAIL_LIST_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Email lists\"\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(\n columns,\n EMAIL_LIST_QUERY.fields,\n EMAIL_LIST_FILTER_OPTIONS,\n EMAIL_LIST_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(listHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the headers go to the query\n // (Subscribers sorts the page, and says so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No lists match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={lists.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOrgListsCard.displayName = 'OrgListsCard'\n\nexport default OrgListsCard\n"],"names":["createResourceUid","pluginDocsHelp","nameSearchFields","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","CreateArtifactDrawer","useSnackbar","Timestamp","Alert","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","setDoc","useRouter","useEffect","useMemo","useState","useFirestore","useUser","useListQuery","useListColumnSort","authorizedFetch","EMAIL_LIST_FILTER_HEADERS","EMAIL_LIST_FILTER_OPTIONS","EMAIL_LIST_QUERY","useEmailDataScope","LIST_SORT_HEADERS","subscribers","LIST_SELECT_FIELDS","OrgListsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","data","user","scope","gridFilter","selectFields","askedSort","setAskedSort","sorts","rows","lists","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","sort","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","options","counts","setCounts","undefined","active","Promise","all","map","list","snapshot","$id","count","then","entries","Object","fromEntries","JSON","stringify","l","pageSorts","row","columnSort","defaultSort","onSortChange","orderBy","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","handleCreate","values","name","String","id","kind","createdAt","now","variant","persist","push","error","console","handleDelete","accepted","title","description","confirmationText","confirmationButtonProps","color","catch","response","method","body","scopeId","result","json","ok","Error","message","listHref","rowActions","key","label","icon","path","size","href","destructive","onClick","columns","field","headerName","flex","minWidth","renderCell","event","stopPropagation","valueGetter","_value","lastEvaluatedAt","toDate","toLocaleString","width","items","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","disabled","spacing","open","onClose","submitLabel","onSubmit","errorSlot","severity","sx","mt","mb","includeDescription","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/lists-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 { createResourceUid, pluginDocsHelp } from '@aglyn/aglyn'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n mdiDeleteOutline,\n mdiPageNextOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon, useConfirmationContext } 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 { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { Alert, Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n doc,\n getCountFromServer,\n setDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } 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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n EMAIL_LIST_FILTER_HEADERS,\n EMAIL_LIST_FILTER_OPTIONS,\n EMAIL_LIST_QUERY,\n} from '../constants/list-queries'\nimport { useEmailDataScope } from './email-org-mount'\n\nexport interface OrgListsCardProps {\n /** The site, or `null` on the organization's Emails page. */\n hostId: string | null\n /** The emails hub URL, which every list route hangs beneath. */\n basePath: string\n}\n\n/** The Membership filter's choices are picked, so the panel shows a select. */\n/** The page-sorted header, for its notice (AGL-3680). */\nconst LIST_SORT_HEADERS = { subscribers: 'Subscribers' }\nconst LIST_SELECT_FIELDS = ['kind']\n\n/**\n * Email lists (AGL-254): static audiences at `orgs/{orgId}/lists`, shared\n * across the org's sites. Members arrive via the enrollList automation\n * step, popup email capture, or manual adds; campaigns target a list from\n * the audience picker.\n *\n * ## A list is a resource, so it has pages\n *\n * The row opens `…/audiences/{listId}`, and everything a list can be asked or\n * told lives there or on its edit page. The membership used to expand INSIDE\n * this table, which made three things impossible at once: a merchant could not\n * link anybody to one audience, the browser's back button did not walk out of\n * a list, and the rename lived under a table it was not about.\n *\n * Row click and the row's own link are two affordances rather than one. A\n * click handler cannot be middle-clicked, copied, or opened from the browser's\n * context menu, and a link alone loses the whole-row target.\n */\nexport function OrgListsCard(props: OrgListsCardProps) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const { data: user } = useUser()\n // The org lookup is async (AGL-1061): `scope` is null until it settles,\n // and stays null for a host with no owning org. Lists have always been\n // org-shared (AGL-254) — there is no host path to fall back to any more\n // (AGL-1050), so creating one is held rather than misdirected. On the\n // organization's page the mount names the org and nothing is looked up.\n const { scope } = useEmailDataScope(hostId)\n\n /*\n * Ordered by the server and paged, rather than capped and re-sorted here\n * (AGL-2501, AGL-2292).\n *\n * `limit(50)` carried no `orderBy`, so Firestore answered it in\n * DOCUMENT-ID order over ids from `createResourceUid()` — an arbitrary\n * fifty of the org's lists, which the `localeCompare` below then arranged\n * alphabetically. An agency past fifty lists saw a believable A-to-Z page\n * that was missing most of the alphabet, and the campaign composer's\n * audience picker reads the same collection.\n *\n * `name` is safe to order on here, which is a claim about the writers and\n * not a preference: this card is the only thing that CREATES a list\n * (`handleCreate` refuses an empty name), the automation step and the\n * newsletter enrollment both resolve an existing list and deliberately\n * never create one, and `lists` is absent from `IMPORTABLE_FIELDS`, so no\n * restore path can produce a nameless document for `orderBy` to drop.\n *\n * Every filter and search word is a predicate on the same query\n * (AGL-3321), so a page is a page of the matches — see `EMAIL_LIST_QUERY`.\n */\n const gridFilter = useListGridFilter({ selectFields: LIST_SELECT_FIELDS })\n /*\n * EVERY HEADER SORTS (AGL-3680). List and Membership order the QUERY (see\n * `EMAIL_LIST_QUERY`); Subscribers is counted per row after the page\n * loads, so it sorts the page on screen and says so.\n */\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(\n EMAIL_LIST_QUERY.sorts[0],\n )\n const {\n rows: lists,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: scope ? collection(firestore, scope[0], scope[1], 'lists') : null,\n declaration: EMAIL_LIST_QUERY,\n request: {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n },\n deps: [firestore, scope?.[0], scope?.[1]],\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_LIST_QUERY.fields,\n headers: EMAIL_LIST_FILTER_HEADERS,\n options: EMAIL_LIST_FILTER_OPTIONS,\n }),\n [plan.refused],\n )\n const [counts, setCounts] = useState<Record<string, number>>({})\n useEffect(() => {\n // `lists` can only be non-empty when `scope` resolved, but the\n // effect must say so for itself — it reads `scope` outside the query.\n if (!scope) return undefined\n let active = true\n void Promise.all(\n lists.map(async (list: any) => {\n try {\n const snapshot = await getCountFromServer(\n collection(\n firestore,\n scope[0],\n scope[1],\n 'lists',\n list.$id,\n 'members',\n ),\n )\n return [list.$id, snapshot.data().count] as const\n } catch {\n return [list.$id, 0] as const\n }\n }),\n ).then((entries) => {\n if (active) setCounts(Object.fromEntries(entries))\n })\n return () => {\n active = false\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, scope, JSON.stringify(lists.map((l: any) => l.$id))])\n const pageSorts = useMemo(\n () => ({ subscribers: (row: any) => counts[row.$id] }),\n [counts],\n )\n const columnSort = useListColumnSort<any>({\n sorts: EMAIL_LIST_QUERY.sorts,\n defaultSort: EMAIL_LIST_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: plan.orderBy,\n rows: lists,\n pageSorts,\n headers: LIST_SORT_HEADERS,\n })\n\n /*\n * Creating is a DRAWER, and the drawer is the shared one.\n *\n * The card used to carry a name box, a membership select and a Create\n * button stacked above the table. A create form wedged over a list has\n * nowhere to grow, which is how the rule behind a dynamic list came to be\n * authored through four controls out of nine — the form could not hold the\n * rest. Naming happens here; everything the audience IS happens on its edit\n * page, which has the room.\n */\n const [createOpen, setCreateOpen] = useState(false)\n const [createError, setCreateError] = useState<unknown>(null)\n const [creating, setCreating] = useState(false)\n\n const handleCreate = async (values: Record<string, any>) => {\n const name = String(values?.['displayName'] ?? '').trim()\n // Without an org there is nowhere org-shared to put the list, and the\n // host path that used to absorb it is gone (AGL-1050). The button is\n // disabled in this state; this is the same answer for a stale closure.\n if (!name || !scope || creating) return\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n try {\n await setDoc(doc(firestore, scope[0], scope[1], 'lists', id), {\n // The name and the search keys the table queries (AGL-3321).\n ...nameSearchFields(name),\n /*\n * Manual until somebody says otherwise.\n *\n * A list arrives holding the people you put on it, and becomes\n * rule-driven on its edit page where the rule can actually be\n * written. Offering the choice in the naming drawer would ask for a\n * decision before the thing it decides — a rule — can be seen.\n */\n kind: 'manual',\n createdAt: Timestamp.now(),\n })\n setCreateOpen(false)\n enqueueSnackbar(`List \"${name}\" created`, {\n variant: 'success',\n persist: false,\n })\n router.push(`${basePath}/audiences/${id}`)\n } catch (error) {\n console.error(error)\n setCreateError(error)\n enqueueSnackbar('An error has occurred', { variant: 'error' })\n } finally {\n setCreating(false)\n }\n }\n\n const handleDelete = async (list: any) => {\n if (!scope) return\n const accepted = await confirm({\n title: 'Delete list?',\n description: `\"${list.name}\" and its enrollments stop being targetable; automations enrolling into it start reporting errors.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating on\n // the resolved value alone made this always return (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n // The list owns a `members` subcollection of enrolled contacts, and\n // Firestore doesn't cascade — deleting the doc from here left that PII\n // behind, unreachable but still readable to every org member. Only the\n // Admin SDK can recursiveDelete, so this goes through the erase route\n // (AGL-946).\n try {\n const response = await authorizedFetch(user, '/api/resources/erase', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n scope: scope[0],\n scopeId: scope[1],\n kind: 'lists',\n id: list.$id,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) throw new Error(result?.error ?? 'List delete failed')\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'List delete failed', {\n variant: 'error',\n })\n }\n enqueueSnackbar('List deleted', { variant: 'success', persist: false })\n }\n\n const listHref = (list: any) => `${basePath}/audiences/${list.$id}`\n\n const rowActions = (list: any): RowActionsMenuItem[] => [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiPageNextOutline.path} size={0.8} />,\n href: listHref(list),\n },\n {\n key: 'edit',\n label: 'Edit list',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: `${listHref(list)}/edit`,\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(list),\n },\n ]\n\n /*\n * One row per list. The name is a link AND the row opens the list: a click\n * handler cannot be middle-clicked, copied or opened from the context menu,\n * and a link alone loses the whole-row target.\n */\n const columns: GridColDef[] = [\n {\n field: 'name',\n headerName: 'List',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <AppLink\n href={listHref(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 {row.name}\n </AppLink>\n ),\n },\n {\n field: 'kind',\n headerName: 'Membership',\n flex: 1,\n minWidth: 220,\n // A list that predates `kind` is a manual one.\n valueGetter: (_value, row) => (row.kind === 'dynamic' ? 'dynamic' : 'manual'),\n renderCell: ({ row }) =>\n row.kind === 'dynamic' ? (\n <Chip\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n /*\n Freshness, not just kind. A dynamic list that has stopped being\n swept looks exactly like one whose population has not changed,\n so the last evaluation time is the only thing that tells them\n apart — and it is the first thing to look at when a merchant\n reports that somebody is missing from an audience.\n */\n label={\n row.lastEvaluatedAt?.toDate\n ? `Rule · ${row.lastEvaluatedAt.toDate().toLocaleString()}`\n : 'Rule · not yet evaluated'\n }\n />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label=\"Manual\" />\n ),\n },\n {\n field: 'subscribers',\n headerName: 'Subscribers',\n width: 130,\n valueGetter: (_value, row) => counts[row.$id] ?? '…',\n },\n listActionsColumn(\n (row) => (\n <ListRowActions\n label={String(row.name ?? row.$id)}\n items={rowActions(row)}\n />\n ),\n { width: 72 },\n ),\n ]\n\n return (\n <CardDisplay\n header=\"Email lists\"\n help={pluginDocsHelp('emailCampaigns', { anchor: '#email-lists' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!scope || creating}\n onClick={() => setCreateOpen(true)}\n >\n {creating ? 'Creating…' : 'Create audience'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Audiences shared across your organization. A manual list holds ' +\n 'the people you enroll into it. A dynamic list holds everyone ' +\n 'matching a rule, re-checked about every fifteen minutes. Target ' +\n 'either from the campaign composer.'}\n </Typography>\n {/*\n A list document stores a name and nothing else descriptive, so the\n shared Description box is refused: a field the writer discards on\n write is worse than a field that was never offered.\n */}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create new audience\"\n submitLabel=\"Create audience\"\n onSubmit={handleCreate}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {'This audience could not be created.'}\n </Alert>\n ) : null\n }\n includeDescription={false}\n />\n {lists.length === 0 && !hasMore && !filtering ? null : (\n <>\n <ListFilterChips\n fields={EMAIL_LIST_QUERY.fields}\n headers={EMAIL_LIST_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={EMAIL_LIST_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={refusals}\n notices={[...plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Email lists\"\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={listFilterGridColumns(\n columns,\n EMAIL_LIST_QUERY.fields,\n EMAIL_LIST_FILTER_OPTIONS,\n EMAIL_LIST_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n onOpen={(_id, row) => router.push(listHref(row))}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel, the search and the headers go to the query\n // (Subscribers sorts the page, and says so).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No lists match these filters\"\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={lists.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOrgListsCard.displayName = 'OrgListsCard'\n\nexport default OrgListsCard\n"],"names":["createResourceUid","pluginDocsHelp","nameSearchFields","mdiDeleteOutline","mdiPageNextOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","TABLE_ROW_HEIGHT","useListGridFilter","CreateArtifactDrawer","useSnackbar","Timestamp","Alert","Button","Chip","Stack","Typography","collection","doc","getCountFromServer","setDoc","useRouter","useEffect","useMemo","useState","useFirestore","useUser","useListQuery","useListColumnSort","authorizedFetch","EMAIL_LIST_FILTER_HEADERS","EMAIL_LIST_FILTER_OPTIONS","EMAIL_LIST_QUERY","useEmailDataScope","LIST_SORT_HEADERS","subscribers","LIST_SELECT_FIELDS","OrgListsCard","props","hostId","basePath","firestore","router","enqueueSnackbar","confirm","data","user","scope","gridFilter","selectFields","askedSort","setAskedSort","sorts","rows","lists","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","search","searchWords","sort","deps","idField","filtering","length","some","word","trim","refusals","refused","fields","headers","options","counts","setCounts","undefined","active","Promise","all","map","list","snapshot","$id","count","then","entries","Object","fromEntries","JSON","stringify","l","pageSorts","row","columnSort","defaultSort","onSortChange","orderBy","createOpen","setCreateOpen","createError","setCreateError","creating","setCreating","handleCreate","values","name","String","id","kind","createdAt","now","variant","persist","push","error","console","handleDelete","accepted","title","description","confirmationText","confirmationButtonProps","color","catch","response","method","body","scopeId","result","json","ok","Error","message","listHref","rowActions","key","label","icon","path","size","href","destructive","onClick","columns","field","headerName","flex","minWidth","renderCell","event","stopPropagation","valueGetter","_value","lastEvaluatedAt","toDate","toLocaleString","width","items","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","disabled","spacing","open","onClose","submitLabel","onSubmit","errorSlot","severity","sx","mt","mb","includeDescription","onChange","setClauses","notices","aria-label","rowHeight","onOpen","_id","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,iBAAiB,EAAEC,cAAc,QAAQ,eAAc;AAChE,SAASC,gBAAgB,QAAQ,qCAAoC;AACrE,SACEC,gBAAgB,EAChBC,kBAAkB,EAClBC,gBAAgB,QACX,yBAAwB;AAC/B,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AAC5F,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,oBAAoB,QAAQ,6BAA4B;AACjE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,SAAS,QAAQ,+BAA8B;AACxD,SAASC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEtE,SACEC,UAAU,EACVC,GAAG,EACHC,kBAAkB,EAClBC,MAAM,QACD,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,YAAY,EAAEC,OAAO,QAAQ,iCAAgC;AACtE,SAASC,YAAY,QAAQ,sDAAqD;AAClF,SAASC,iBAAiB,QAAQ,kDAAiD;AAEnF,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SACEC,yBAAyB,EACzBC,yBAAyB,EACzBC,gBAAgB,QACX,+BAA2B;AAClC,SAASC,iBAAiB,QAAQ,uBAAmB;AASrD,6EAA6E,GAC7E,uDAAuD,GACvD,MAAMC,oBAAoB;IAAEC,aAAa;AAAc;AACvD,MAAMC,qBAAqB;IAAC;CAAO;AAEnC;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGF;IAC7B,MAAMG,YAAYhB;IAClB,MAAMiB,SAASrB;IACf,MAAM,EAAEsB,eAAe,EAAE,GAAGjC;IAC5B,MAAM,EAAEkC,OAAO,EAAE,GAAG9C;IACpB,MAAM,EAAE+C,MAAMC,IAAI,EAAE,GAAGpB;IACvB,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,sEAAsE;IACtE,wEAAwE;IACxE,MAAM,EAAEqB,KAAK,EAAE,GAAGd,kBAAkBM;IAEpC;;;;;;;;;;;;;;;;;;;;GAoBC,GACD,MAAMS,aAAaxC,kBAAkB;QAAEyC,cAAcb;IAAmB;IACxE;;;;GAIC,GACD,MAAM,CAACc,WAAWC,aAAa,GAAG3B,SAChCQ,iBAAiBoB,KAAK,CAAC,EAAE;IAE3B,MAAM,EACJC,MAAMC,KAAK,EACXC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACL,GAAGjC,aAAkB;QACpBV,YAAY8B,QAAQ9B,WAAWwB,WAAWM,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,WAAW;QACzEc,aAAa7B;QACb8B,SAAS;YACPC,SAASf,WAAWe,OAAO;YAC3BC,QAAQhB,WAAWiB,WAAW;YAC9BC,MAAMhB;QACR;QACAiB,MAAM;YAAC1B;YAAWM,yBAAAA,KAAO,CAAC,EAAE;YAAEA,yBAAAA,KAAO,CAAC,EAAE;SAAC;QACzCqB,SAAS;IACX;IACA,MAAMC,YACJrB,WAAWe,OAAO,CAACO,MAAM,GAAG,KAAKtB,WAAWiB,WAAW,CAACM,IAAI,CAAC,CAACC,OAASA,KAAKC,IAAI;IAClF,MAAMC,WAAWnD,QACf,IACErB,kBAAkB0D,KAAKe,OAAO,EAAE;YAC9BC,QAAQ5C,iBAAiB4C,MAAM;YAC/BC,SAAS/C;YACTgD,SAAS/C;QACX,IACF;QAAC6B,KAAKe,OAAO;KAAC;IAEhB,MAAM,CAACI,QAAQC,UAAU,GAAGxD,SAAiC,CAAC;IAC9DF,UAAU;QACR,+DAA+D;QAC/D,sEAAsE;QACtE,IAAI,CAACyB,OAAO,OAAOkC;QACnB,IAAIC,SAAS;QACb,KAAKC,QAAQC,GAAG,CACd9B,MAAM+B,GAAG,CAAC,OAAOC;YACf,IAAI;gBACF,MAAMC,WAAW,MAAMpE,mBACrBF,WACEwB,WACAM,KAAK,CAAC,EAAE,EACRA,KAAK,CAAC,EAAE,EACR,SACAuC,KAAKE,GAAG,EACR;gBAGJ,OAAO;oBAACF,KAAKE,GAAG;oBAAED,SAAS1C,IAAI,GAAG4C,KAAK;iBAAC;YAC1C,EAAE,eAAM;gBACN,OAAO;oBAACH,KAAKE,GAAG;oBAAE;iBAAE;YACtB;QACF,IACAE,IAAI,CAAC,CAACC;YACN,IAAIT,QAAQF,UAAUY,OAAOC,WAAW,CAACF;QAC3C;QACA,OAAO;YACLT,SAAS;QACX;IACA,uDAAuD;IACzD,GAAG;QAACzC;QAAWM;QAAO+C,KAAKC,SAAS,CAACzC,MAAM+B,GAAG,CAAC,CAACW,IAAWA,EAAER,GAAG;KAAG;IACnE,MAAMS,YAAY1E,QAChB,IAAO,CAAA;YAAEY,aAAa,CAAC+D,MAAanB,MAAM,CAACmB,IAAIV,GAAG,CAAC;QAAC,CAAA,GACpD;QAACT;KAAO;IAEV,MAAMoB,aAAavE,kBAAuB;QACxCwB,OAAOpB,iBAAiBoB,KAAK;QAC7BgD,aAAapE,iBAAiBoB,KAAK,CAAC,EAAE;QACtCc,MAAMhB;QACNmD,cAAclD;QACdmD,SAAS1C,KAAK0C,OAAO;QACrBjD,MAAMC;QACN2C;QACApB,SAAS3C;IACX;IAEA;;;;;;;;;GASC,GACD,MAAM,CAACqE,YAAYC,cAAc,GAAGhF,SAAS;IAC7C,MAAM,CAACiF,aAAaC,eAAe,GAAGlF,SAAkB;IACxD,MAAM,CAACmF,UAAUC,YAAY,GAAGpF,SAAS;IAEzC,MAAMqF,eAAe,OAAOC;;QAC1B,MAAMC,OAAOC,eAAOF,0BAAAA,MAAQ,CAAC,cAAc,mBAAI,IAAIrC,IAAI;QACvD,sEAAsE;QACtE,qEAAqE;QACrE,uEAAuE;QACvE,IAAI,CAACsC,QAAQ,CAAChE,SAAS4D,UAAU;QACjCC,YAAY;QACZF,eAAe;QACf,MAAMO,KAAK5H;QACX,IAAI;YACF,MAAM+B,OAAOF,IAAIuB,WAAWM,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,SAASkE,KAAK,aAEzD1H,iBAAiBwH;gBACpB;;;;;;;SAOC,GACDG,MAAM;gBACNC,WAAWxG,UAAUyG,GAAG;;YAE1BZ,cAAc;YACd7D,gBAAgB,CAAC,MAAM,EAAEoE,KAAK,SAAS,CAAC,EAAE;gBACxCM,SAAS;gBACTC,SAAS;YACX;YACA5E,OAAO6E,IAAI,CAAC,GAAG/E,SAAS,WAAW,EAAEyE,IAAI;QAC3C,EAAE,OAAOO,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdd,eAAec;YACf7E,gBAAgB,yBAAyB;gBAAE0E,SAAS;YAAQ;QAC9D,SAAU;YACRT,YAAY;QACd;IACF;IAEA,MAAMc,eAAe,OAAOpC;QAC1B,IAAI,CAACvC,OAAO;QACZ,MAAM4E,WAAW,MAAM/E,QAAQ;YAC7BgF,OAAO;YACPC,aAAa,CAAC,CAAC,EAAEvC,KAAKyB,IAAI,CAAC,kGAAkG,CAAC;YAC9He,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,EACE,uEAAuE;QACvE,8DAA8D;SAC7DtC,IAAI,CAAC,IAAM,MACXuC,KAAK,CAAC,IAAM;QACf,IAAI,CAACN,UAAU;QACf,oEAAoE;QACpE,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,aAAa;QACb,IAAI;;YACF,MAAMO,WAAW,MAAMrG,gBAAgBiB,MAAM,wBAAwB;gBACnEqF,QAAQ;gBACRtD,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CuD,MAAMtC,KAAKC,SAAS,CAAC;oBACnBhD,OAAOA,KAAK,CAAC,EAAE;oBACfsF,SAAStF,KAAK,CAAC,EAAE;oBACjBmE,MAAM;oBACND,IAAI3B,KAAKE,GAAG;gBACd;YACF;YACA,MAAM8C,SAAS,MAAMJ,SAASK,IAAI,GAAGN,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;YACnD,IAAI,CAACC,SAASM,EAAE,EAAE,MAAM,IAAIC,cAAMH,0BAAAA,OAAQd,KAAK,mBAAI;QACrD,EAAE,OAAOA,OAAY;;YACnB,OAAO,KAAK7E,yBAAgB6E,yBAAAA,MAAOkB,OAAO,oBAAI,sBAAsB;gBAClErB,SAAS;YACX;QACF;QACA1E,gBAAgB,gBAAgB;YAAE0E,SAAS;YAAWC,SAAS;QAAM;IACvE;IAEA,MAAMqB,WAAW,CAACrD,OAAc,GAAG9C,SAAS,WAAW,EAAE8C,KAAKE,GAAG,EAAE;IAEnE,MAAMoD,aAAa,CAACtD,OAAoC;YACtD;gBACEuD,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAClJ;oBAAQmJ,MAAMvJ,mBAAmBuJ,IAAI;oBAAEC,MAAM;;gBACpDC,MAAMP,SAASrD;YACjB;YACA;gBACEuD,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAClJ;oBAAQmJ,MAAMtJ,iBAAiBsJ,IAAI;oBAAEC,MAAM;;gBAClDC,MAAM,GAAGP,SAASrD,MAAM,KAAK,CAAC;YAChC;YACA;gBACEuD,KAAK;gBACLC,OAAO;gBACPC,oBAAM,KAAClJ;oBAAQmJ,MAAMxJ,iBAAiBwJ,IAAI;oBAAEC,MAAM;;gBAClDE,aAAa;gBACbC,SAAS,IAAM,KAAK1B,aAAapC;YACnC;SACD;IAED;;;;GAIC,GACD,MAAM+D,UAAwB;QAC5B;YACEC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,YAAY,CAAC,EAAExD,GAAG,EAAE,iBAClB,KAACvG;oBACCuJ,MAAMP,SAASzC;oBACf,+DAA+D;oBAC/D,4CAA4C;oBAC5CkD,SAAS,CAACO,QACRA,MAAMC,eAAe;8BAGtB1D,IAAIa,IAAI;;QAGf;QACA;YACEuC,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACV,+CAA+C;YAC/CI,aAAa,CAACC,QAAQ5D,MAASA,IAAIgB,IAAI,KAAK,YAAY,YAAY;YACpEwC,YAAY,CAAC,EAAExD,GAAG,EAAE;oBAcZA;uBAbNA,IAAIgB,IAAI,KAAK,0BACX,KAACpG;oBACCmI,MAAK;oBACLjB,OAAM;oBACNX,SAAQ;oBACR;;;;;;aAMC,GACDyB,OACE5C,EAAAA,uBAAAA,IAAI6D,eAAe,qBAAnB7D,qBAAqB8D,MAAM,IACvB,CAAC,OAAO,EAAE9D,IAAI6D,eAAe,CAACC,MAAM,GAAGC,cAAc,IAAI,GACzD;mCAIR,KAACnJ;oBAAKmI,MAAK;oBAAQ5B,SAAQ;oBAAWyB,OAAM;;;QAElD;QACA;YACEQ,OAAO;YACPC,YAAY;YACZW,OAAO;YACPL,aAAa,CAACC,QAAQ5D;oBAAQnB;wBAAAA,kBAAAA,MAAM,CAACmB,IAAIV,GAAG,CAAC,YAAfT,kBAAmB;;QACnD;QACA1E,kBACE,CAAC6F;gBAEiBA;iCADhB,KAAC/F;gBACC2I,OAAO9B,QAAOd,YAAAA,IAAIa,IAAI,YAARb,YAAYA,IAAIV,GAAG;gBACjC2E,OAAOvB,WAAW1C;;WAGtB;YAAEgE,OAAO;QAAG;KAEf;IAED,qBACE,KAACtK;QACCwK,QAAO;QACPC,MAAM/K,eAAe,kBAAkB;YAAEgL,QAAQ;QAAe;QAChEC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXC,sBACE,KAAC9J;gBACCoI,MAAK;gBACL5B,SAAQ;gBACRuD,UAAU,CAAC7H,SAAS4D;gBACpByC,SAAS,IAAM5C,cAAc;0BAE5BG,WAAW,cAAc;;QAGhC;kBAEA,cAAA,MAAC5F;YAAM8J,SAAS;;8BACd,KAAC7J;oBAAWqG,SAAQ;oBAAQW,OAAM;8BAC/B,oEACC,kEACA,qEACA;;8BAOJ,KAACvH;oBACCqK,MAAMvE;oBACNwE,SAAS,IAAMvE,cAAc;oBAC7BoB,OAAM;oBACNoD,aAAY;oBACZC,UAAUpE;oBACVqE,WACEzE,4BACE,KAAC7F;wBAAMuK,UAAS;wBAAQC,IAAI;4BAAEC,IAAI;4BAAGC,IAAI;wBAAE;kCACxC;yBAED;oBAENC,oBAAoB;;gBAErBjI,MAAMgB,MAAM,KAAK,KAAK,CAACf,WAAW,CAACc,YAAY,qBAC9C;;sCACE,KAACtE;4BACC6E,QAAQ5C,iBAAiB4C,MAAM;4BAC/BC,SAAS/C;4BACTiC,SAASf,WAAWe,OAAO;4BAC3ByH,UAAUxI,WAAWyI,UAAU;4BAC/B3G,SAAS/C;;sCAEX,KAAC9B;4BACC0E,SAASD;4BACTgH,SAAS;mCAAI9H,KAAK8H,OAAO;mCAAKvF,WAAWuF,OAAO;6BAAC;;sCAEnD,KAACtL;4BACCuL,cAAW;4BACXtI,MAAM8C,WAAW9C,IAAI;4BACrB8C,YAAYA;4BACZkD,SAAS/I,sBACP+I,SACArH,iBAAiB4C,MAAM,EACvB7C,2BACAD;4BAEF8J,WAAWrL;4BACXsL,QAAQ,CAACC,KAAK5F,MAAQxD,OAAO6E,IAAI,CAACoB,SAASzC;4BAC3C,8DAA8D;4BAC9D6F,UAAU;4BACV,wDAAwD;4BACxD,6CAA6C;4BAC7CC,YAAW;4BACXC,aAAajJ,WAAWiJ,WAAW;4BACnCC,qBAAqBlJ,WAAWkJ,mBAAmB;4BACnDC,WAAW;4BACXC,aAAY;;sCAEd,KAACpM;4BACCwD,MAAMA;4BACNE,UAAUA;4BACV2I,UAAU/I,MAAMgB,MAAM;4BACtBf,SAASA;4BACT+I,cAAc7I;4BACd8I,kBAAkB5I;;;;;;;AAOhC;AACAtB,aAAamK,WAAW,GAAG;AAE3B,eAAenK,aAAY"}
@@ -17,7 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { buildRoute, pluginDocsHelp, Route } from "@aglyn/aglyn";
20
- import { mdiDeleteOutline, mdiEyeOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
20
+ import { mdiDeleteOutline, mdiPageNextOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
21
21
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
22
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
23
23
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
@@ -334,7 +334,7 @@ function OrgEmailTemplatesTable(props) {
334
334
  key: 'details',
335
335
  label: 'Open details',
336
336
  icon: /*#__PURE__*/ _jsx(MdiIcon, {
337
- path: mdiEyeOutline.path,
337
+ path: mdiPageNextOutline.path,
338
338
  size: 0.8
339
339
  }),
340
340
  href: page != null ? page : undefined,
@@ -454,8 +454,8 @@ function OrgEmailTemplatesTable(props) {
454
454
  const noSites = mount.hostsReady && mount.hosts.length === 0;
455
455
  return /*#__PURE__*/ _jsxs(CardDisplay, {
456
456
  header: 'Templates',
457
- help: pluginDocsHelp('designedEmails', {
458
- anchor: '#find-a-template'
457
+ help: pluginDocsHelp('emailCampaigns', {
458
+ anchor: '#organization-emails-page'
459
459
  }),
460
460
  contentGutterX: true,
461
461
  contentGutterY: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-email-templates-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n 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 type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_ROW_HEIGHT,\n} from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { listQueryConstraints } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef, GridSortModel } from '@mui/x-data-grid'\nimport {\n type ListPageSort,\n sortListRows,\n} from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, 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 { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\nimport {\n OrgSiteSelect,\n orgSiteEmailsPath,\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\n\n/**\n * How many of one site's templates the organization's list reads.\n *\n * Lower than the site's own list, because this one reads a page of sites at\n * once: a site with more than this is named under the table, with a link to\n * its own Templates page, which reads its whole set.\n */\nexport const ORG_TEMPLATE_CEILING = 50\n\n/*\n * What the organization's templates grid's Filters panel offers (AGL-3321).\n *\n * The template clauses and the search are the site list's own\n * (`EMAIL_TEMPLATE_QUERY`): ONE plan, put on EVERY site's query, so each\n * site's read is already the answer and nothing is matched in the browser.\n * Site is not a field of a screen but a choice of WHICH sites' queries run:\n * a Site clause reads exactly the sites it names, whichever page of sites\n * they are on. Origin (yours or installed) is not offered, for the reason the\n * declaration gives: a query cannot ask for a field to be absent.\n */\nconst SITE_FIELD: ListFilterField = {\n column: 'site',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\nconst ORG_TEMPLATE_FILTER_FIELDS: readonly ListFilterField[] = [\n ...EMAIL_TEMPLATE_QUERY.fields,\n SITE_FIELD,\n]\nconst ORG_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n ...EMAIL_TEMPLATE_FILTER_HEADERS,\n site: 'Site',\n}\n\n/** The sites a Site clause names, or null when none is in force. */\nexport function orgTemplateSiteScope(\n clauses: readonly ListFilterClause[],\n): string[] | null {\n const named = clauses\n .filter((clause) => clause.field === SITE_FIELD.column)\n .map((clause) =>\n clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n )\n if (!named.length) return null\n // Several Site clauses all hold: a site must be named by each.\n return named.reduce((kept, next) => kept.filter((id) => next.includes(id)))\n}\n\n/** How each header orders the matches in hand (AGL-3680). */\nconst ORG_TEMPLATE_SORTS: Readonly<Record<string, ListPageSort<Record<string, any>>>> = {\n displayName: (row) => String(row['displayName'] ?? 'Untitled template'),\n site: (row) => row['siteName'],\n origin: (row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n}\n\n/** One site's read, as the table assembles it. */\ninterface SiteRead {\n rows: Array<Record<string, any>>\n /** The site holds more templates than {@link ORG_TEMPLATE_CEILING}. */\n truncated: boolean\n /** The read has answered — rows or none. */\n settled: boolean\n}\n\n/**\n * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.\n *\n * A component rather than a hook in a loop, because the number of sites\n * changes and a hook's call count may not: each mounted reader is one\n * listener, and a page of sites mounts one per site on it.\n *\n * Every predicate is on the QUERY — the email scope, every clause and the\n * search word, one plan for all the sites (AGL-3321) — so a site's read is\n * its first templates THAT MATCH, by name, and a site with a thousand pages\n * reads its matching emails and nothing else. It reads one past the ceiling,\n * so the table can say when a site holds more matches than it listed.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n plan: ListQueryPlan\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, plan, onRead } = props\n const firestore = useFirestore()\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n ...listQueryConstraints(plan),\n limit(ORG_TEMPLATE_CEILING + 1),\n ),\n [firestore, hostId, planKey],\n { idField: '$id' },\n )\n // One window per snapshot, so a re-render that brings no new snapshot hands\n // the table the same rows it already holds.\n const window = useMemo(\n () => ceilingedWindow(data, ORG_TEMPLATE_CEILING),\n [data],\n )\n useEffect(() => {\n onRead(hostId, {\n rows: window.rows,\n truncated: window.truncated,\n settled: status !== 'loading',\n })\n }, [window, status, hostId, onRead])\n return null\n}\nSiteTemplatesReader.displayName = 'SiteTemplatesReader'\n\n/**\n * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one\n * table.\n *\n * A template is a screen on ONE site — its besigner, its assets and the\n * messages built from it are that site's — so this table reads each site in\n * turn and names the site on every row. A row opens the template's page on\n * its own site, where its report and its recipients are; nothing here is an\n * organization-level template page, because there is no such thing.\n *\n * The reads are one listener per site, over one page of sites at a time —\n * ordered by name — each its site's query for the clauses and search in\n * force, capped at {@link ORG_TEMPLATE_CEILING} matches. The shared footer\n * pages the matches in hand; an organization with more sites than one page\n * chooses which page of sites is read with the Sites picker above the\n * table, or names sites with the Site filter. Both caps say so on screen when\n * they bite, so a missing template is never mistaken for one that does not\n * exist.\n *\n * \"New template\" asks which site the design is made on — the besigner opens\n * on a site — and skips the question when the org has one site.\n */\nexport function OrgEmailTemplatesCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgEmailTemplatesTable mount={mount} /> : null\n}\nOrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard'\n\nfunction OrgEmailTemplatesTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const sitePage = useOrgSitePage(mount)\n const gridFilter = useListGridFilter({ selectFields: [SITE_FIELD.column] })\n /*\n * Which sites are read: the ones a Site clause names, else the page of\n * sites. The other clauses and the search are ONE plan every site's query\n * carries.\n */\n const siteScope = useMemo(\n () => orgTemplateSiteScope(gridFilter.clauses),\n [gridFilter.clauses],\n )\n const sites = useMemo(\n () =>\n siteScope\n ? mount.hosts.filter((host) => siteScope.includes(host.id))\n : sitePage.sites,\n [siteScope, mount.hosts, sitePage.sites],\n )\n const plan = useMemo(\n () =>\n planListQuery(\n EMAIL_TEMPLATE_QUERY,\n {\n clauses: gridFilter.clauses.filter(\n (clause) => clause.field !== SITE_FIELD.column,\n ),\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n nameSearchNormalizers,\n ),\n [gridFilter.clauses, gridFilter.searchWords],\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: ORG_TEMPLATE_FILTER_FIELDS,\n headers: ORG_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const [reads, setReads] = useState<Record<string, SiteRead>>({})\n const onRead = useCallback(\n (hostId: string, read: SiteRead) =>\n setReads((current) => {\n const previous = current[hostId]\n // An answer the table already holds changes nothing to draw.\n if (\n previous &&\n previous.rows === read.rows &&\n previous.truncated === read.truncated &&\n previous.settled === read.settled\n ) {\n return current\n }\n return { ...current, [hostId]: read }\n }),\n [],\n )\n\n /*\n * One row per template, named by site. The row id joins the site to the\n * screen id: two sites can hold a screen with the same id, and a grid that\n * keyed on the screen alone would draw one of them twice. Every row is an\n * answer some site's query gave; the sort only interleaves the sites'\n * answers by the name each was already ordered by.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? []).map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n siteName: orgSiteName(mount, site.id),\n })),\n )\n .sort(\n (a, b) =>\n String(a['nameLower'] ?? '').localeCompare(String(b['nameLower'] ?? '')) ||\n orgSiteName(mount, a.hostId).localeCompare(\n orgSiteName(mount, b.hostId),\n ),\n ),\n [sites, reads, mount],\n )\n const settled = sites.every((site) => reads[site.id]?.settled)\n const crowded = sites.filter((site) => reads[site.id]?.truncated)\n\n /*\n * The matches in hand, a page at a time on the shared footer. Choosing\n * another page of sites, or another question, starts from the first page:\n * page four of one answer is not a position in another.\n */\n const filterOptions = useMemo(\n () => ({\n // Every site the organization has: a Site clause reads the sites it\n // names wherever they sit in the page of sites.\n site: mount.hosts.map((site) => ({ value: site.id, label: orgSiteName(mount, site.id) })),\n }),\n [mount],\n )\n /*\n * EVERY HEADER SORTS (AGL-3680), over EVERY match in hand rather than the\n * page on screen: the rows are all loaded (each site capped, and the cap\n * says so above), so the sort is exact and the footer pages the sorted\n * whole. Site sorts by the name the column shows, not the site id.\n */\n const [sortModel, setSortModel] = useState<GridSortModel>([])\n const sorted = useMemo(() => {\n const [first] = sortModel\n const value = first?.sort ? ORG_TEMPLATE_SORTS[first.field] : undefined\n return value && first?.sort ? sortListRows(rows, value, first.sort) : rows\n }, [rows, sortModel])\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const siteKey = sites.map((site) => site.id).join(',')\n const sortKey = JSON.stringify(sortModel)\n // A new answer, or a new order, starts again at its first page.\n useEffect(() => setPage(0), [siteKey, planKey, sortKey])\n const shown = useMemo(\n () => sorted.slice(page * pageSize, page * pageSize + pageSize),\n [sorted, page, pageSize],\n )\n const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize)\n\n const templateName = (row: Record<string, any>) =>\n String(row['displayName'] ?? 'Untitled template')\n /** The template's own page, on the site that holds it. */\n const templateHref = (row: Record<string, any>) =>\n orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`)\n const siteSubdomain = (hostId: string) =>\n mount.hosts.find((site) => site.id === hostId)?.subdomain ?? null\n const besignerHref = (\n hostId: string,\n screenId: string,\n versionId: unknown,\n ) => {\n const subdomain = siteSubdomain(hostId)\n return subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug: mount.orgSlug,\n host: subdomain,\n screenId,\n versionId: String(versionId),\n })\n : null\n }\n\n const handleDelete = async (row: Record<string, any>) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${templateName(row)}\" will be removed from ` +\n `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` +\n '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', row['hostId'], 'screens', row['screenId']),\n emailTemplateSoftDelete(),\n )\n }\n\n const rowActions = (row: Record<string, any>): RowActionsMenuItem[] => {\n const page = templateHref(row)\n const editor = besignerHref(\n row['hostId'],\n row['screenId'],\n row['versionId'],\n )\n const unlinkedReason =\n 'This site has no console address yet, so it cannot be linked to.'\n return [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: page ?? undefined,\n disabled: !page,\n disabledReason: unlinkedReason,\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: editor ?? undefined,\n disabled: !editor,\n disabledReason: row['versionId']\n ? unlinkedReason\n : 'This template has never been saved in the besigner. Open it from its page.',\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(row),\n },\n ]\n }\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 const href = templateHref(row)\n return href ? (\n <AppLink\n href={href}\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 value\n )\n },\n },\n {\n field: 'site',\n headerName: 'Site',\n width: 170,\n valueGetter: (_value, row) => row.hostId,\n renderCell: ({ row }) => row.siteName,\n },\n {\n // Whose template this is, as on a site's own list.\n field: 'origin',\n headerName: 'Origin',\n width: 130,\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 /*==========================================\n * CREATE, ON A SITE.\n *\n * The same document a site's own list creates — `createEmailScreen`\n * through the quota-enforcing resources route — made as the chosen site,\n * then straight into that site's besigner. One site answers the question\n * itself; otherwise the dialog asks, defaulting to the session's pick.\n *=========================================*/\n const [choosing, setChoosing] = useState(false)\n const [createHostId, setCreateHostId] = useState('')\n const [creating, setCreating] = useState(false)\n\n const create = async (hostId: string) => {\n if (creating || !hostId) return\n setCreating(true)\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n setChoosing(false)\n const editor = besignerHref(hostId, screenId, versionId)\n if (editor) {\n void router.push(editor)\n } else {\n enqueueSnackbar(\n `Template created on ${orgSiteName(mount, hostId)}. Open it from ` +\n 'that site’s Emails page.',\n { variant: 'success' },\n )\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n } finally {\n setCreating(false)\n }\n }\n\n const handleNew = () => {\n if (mount.hosts.length === 1) return void create(mount.hosts[0].id)\n setCreateHostId(mount.pickedHostId ?? '')\n setChoosing(true)\n }\n\n const noSites = mount.hostsReady && mount.hosts.length === 0\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 <Button\n size=\"small\"\n variant=\"contained\"\n disabled={noSites || creating}\n onClick={handleNew}\n >\n {creating ? 'Creating…' : 'New template'}\n </Button>\n ),\n }}\n >\n {sites.map((site) => (\n <SiteTemplatesReader\n key={site.id}\n hostId={site.id}\n plan={plan}\n onRead={onRead}\n />\n ))}\n <Stack spacing={1.5}>\n {/*\n WHICH SITES' TEMPLATES ARE LISTED, when there are more sites than\n one page holds. Only the chosen page's sites are read; a template on\n a site of another page is one choice away, not missing. It picks\n what is read, a scope, so it is not one of the grid's filters.\n */}\n {sitePageCount > 1 && !siteScope ? (\n <TextField\n select\n size=\"small\"\n label=\"Sites\"\n value={sitePage.page}\n onChange={(event) => sitePage.setPage(Number(event.target.value))}\n sx={{ maxWidth: 260 }}\n >\n {Array.from({ length: sitePageCount }, (_, index) => (\n <MenuItem key={index} value={index}>\n {orgSitePageLabel({\n from: index * sitePage.pageSize + 1,\n to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),\n count: sitePage.count,\n })}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A template is designed on ' +\n 'a site, in its besigner, so it appears here once there is one.'}\n </Typography>\n ) : rows.length === 0 && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'None of these sites has an email template yet. Design one ' +\n 'here, then send it from a campaign — a new template opens in ' +\n 'the besigner with email-safe components only.'\n : 'Loading templates…'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={ORG_TEMPLATE_FILTER_FIELDS}\n headers={ORG_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={listFilterGridColumns(\n columns,\n ORG_TEMPLATE_FILTER_FIELDS,\n filterOptions,\n ORG_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to every site's query; a header\n // sorts every match in hand before the footer pages it.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n sortingMode=\"server\"\n sortModel={sortModel}\n onSortModelChange={setSortModel}\n noRowsLabel=\"No templates match these filters\"\n onOpen={(_id, row) => {\n const href = templateHref(row)\n if (href) router.push(href)\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {crowded.map((site) => {\n const href = orgSiteEmailsPath(mount, site.id, 'templates')\n return (\n <Alert key={site.id} severity=\"info\">\n {`${orgSiteName(mount, site.id)} has more than ` +\n `${ORG_TEMPLATE_CEILING} ${filtering ? 'matching templates' : 'templates'}, ` +\n 'and only the first of them are listed here. '}\n {href ? (\n <AppLink href={href}>{'See all of its templates'}</AppLink>\n ) : null}\n </Alert>\n )\n })}\n </Stack>\n\n <Dialog\n open={choosing}\n onClose={creating ? undefined : () => setChoosing(false)}\n fullWidth\n maxWidth=\"xs\"\n >\n <DialogTitle>{'New template'}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'A template is designed on one site, in its besigner, and is ' +\n 'sent from that site’s campaigns.'}\n </DialogContentText>\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={createHostId}\n onChange={setCreateHostId}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setChoosing(false)} disabled={creating}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!createHostId || creating}\n onClick={() => void create(createHostId)}\n >\n {creating ? 'Creating…' : 'Create and open'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable'\n\nexport default OrgEmailTemplatesCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiEyeOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","planListQuery","useListGridFilter","nameSearchNormalizers","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","listQueryConstraints","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","sortListRows","collection","doc","limit","query","updateDoc","useRouter","useCallback","useEffect","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","emailTemplateSoftDelete","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","SITE_FIELD","column","kind","path","operators","ORG_TEMPLATE_FILTER_FIELDS","fields","ORG_TEMPLATE_FILTER_HEADERS","site","orgTemplateSiteScope","clauses","named","filter","clause","field","map","value","split","entry","trim","Boolean","length","reduce","kept","next","id","includes","ORG_TEMPLATE_SORTS","displayName","row","String","origin","SiteTemplatesReader","props","hostId","plan","onRead","firestore","planKey","JSON","stringify","filters","orderBy","data","status","idField","window","rows","truncated","settled","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","gridFilter","selectFields","siteScope","sites","hosts","host","search","searchWords","base","filtering","some","word","refusals","refused","headers","reads","setReads","read","current","previous","flatMap","screen","$id","screenId","siteName","sort","a","b","localeCompare","every","crowded","filterOptions","label","sortModel","setSortModel","sorted","first","undefined","page","setPage","pageSize","setPageSize","siteKey","join","sortKey","shown","slice","sitePageCount","Math","ceil","count","templateName","templateHref","siteSubdomain","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","rowActions","editor","unlinkedReason","key","icon","size","href","disabled","disabledReason","destructive","onClick","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","event","stopPropagation","width","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","setClauses","options","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","onSortModelChange","noRowsLabel","onOpen","_id","rowCount","onPageChange","onPageSizeChange","severity","open","onClose","fullWidth","mb"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/org-email-templates-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, pluginDocsHelp, Route } from '@aglyn/aglyn'\nimport {\n mdiDeleteOutline,\n mdiPageNextOutline,\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 type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n type ListFilterClause,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_ROW_HEIGHT,\n} from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { listQueryConstraints } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useHostVersionApi,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef, GridSortModel } from '@mui/x-data-grid'\nimport {\n type ListPageSort,\n sortListRows,\n} from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport { collection, doc, limit, query, updateDoc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, 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 { emailTemplateSoftDelete } from '../utils/email-template-soft-delete'\nimport {\n OrgSiteSelect,\n orgSiteEmailsPath,\n orgSiteName,\n orgSitePageLabel,\n useEmailOrgMount,\n useOrgSitePage,\n type EmailOrgMount,\n} from './email-org-mount'\n\n/**\n * How many of one site's templates the organization's list reads.\n *\n * Lower than the site's own list, because this one reads a page of sites at\n * once: a site with more than this is named under the table, with a link to\n * its own Templates page, which reads its whole set.\n */\nexport const ORG_TEMPLATE_CEILING = 50\n\n/*\n * What the organization's templates grid's Filters panel offers (AGL-3321).\n *\n * The template clauses and the search are the site list's own\n * (`EMAIL_TEMPLATE_QUERY`): ONE plan, put on EVERY site's query, so each\n * site's read is already the answer and nothing is matched in the browser.\n * Site is not a field of a screen but a choice of WHICH sites' queries run:\n * a Site clause reads exactly the sites it names, whichever page of sites\n * they are on. Origin (yours or installed) is not offered, for the reason the\n * declaration gives: a query cannot ask for a field to be absent.\n */\nconst SITE_FIELD: ListFilterField = {\n column: 'site',\n kind: 'exact',\n path: 'hostId',\n operators: ['equals', 'isAnyOf'],\n}\nconst ORG_TEMPLATE_FILTER_FIELDS: readonly ListFilterField[] = [\n ...EMAIL_TEMPLATE_QUERY.fields,\n SITE_FIELD,\n]\nconst ORG_TEMPLATE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n ...EMAIL_TEMPLATE_FILTER_HEADERS,\n site: 'Site',\n}\n\n/** The sites a Site clause names, or null when none is in force. */\nexport function orgTemplateSiteScope(\n clauses: readonly ListFilterClause[],\n): string[] | null {\n const named = clauses\n .filter((clause) => clause.field === SITE_FIELD.column)\n .map((clause) =>\n clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n )\n if (!named.length) return null\n // Several Site clauses all hold: a site must be named by each.\n return named.reduce((kept, next) => kept.filter((id) => next.includes(id)))\n}\n\n/** How each header orders the matches in hand (AGL-3680). */\nconst ORG_TEMPLATE_SORTS: Readonly<Record<string, ListPageSort<Record<string, any>>>> = {\n displayName: (row) => String(row['displayName'] ?? 'Untitled template'),\n site: (row) => row['siteName'],\n origin: (row) =>\n templateProvenance(row).origin === 'installed' ? 'installed' : 'local',\n}\n\n/** One site's read, as the table assembles it. */\ninterface SiteRead {\n rows: Array<Record<string, any>>\n /** The site holds more templates than {@link ORG_TEMPLATE_CEILING}. */\n truncated: boolean\n /** The read has answered — rows or none. */\n settled: boolean\n}\n\n/**\n * ONE SITE'S EMAIL TEMPLATES, read while the organization's list is on screen.\n *\n * A component rather than a hook in a loop, because the number of sites\n * changes and a hook's call count may not: each mounted reader is one\n * listener, and a page of sites mounts one per site on it.\n *\n * Every predicate is on the QUERY — the email scope, every clause and the\n * search word, one plan for all the sites (AGL-3321) — so a site's read is\n * its first templates THAT MATCH, by name, and a site with a thousand pages\n * reads its matching emails and nothing else. It reads one past the ceiling,\n * so the table can say when a site holds more matches than it listed.\n */\nfunction SiteTemplatesReader(props: {\n hostId: string\n plan: ListQueryPlan\n onRead: (hostId: string, read: SiteRead) => void\n}) {\n const { hostId, plan, onRead } = props\n const firestore = useFirestore()\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const { data, status } = useFirestoreCollection<Record<string, any>>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'screens'),\n ...listQueryConstraints(plan),\n limit(ORG_TEMPLATE_CEILING + 1),\n ),\n [firestore, hostId, planKey],\n { idField: '$id' },\n )\n // One window per snapshot, so a re-render that brings no new snapshot hands\n // the table the same rows it already holds.\n const window = useMemo(\n () => ceilingedWindow(data, ORG_TEMPLATE_CEILING),\n [data],\n )\n useEffect(() => {\n onRead(hostId, {\n rows: window.rows,\n truncated: window.truncated,\n settled: status !== 'loading',\n })\n }, [window, status, hostId, onRead])\n return null\n}\nSiteTemplatesReader.displayName = 'SiteTemplatesReader'\n\n/**\n * THE ORGANIZATION'S TEMPLATES: every site's reusable email designs, in one\n * table.\n *\n * A template is a screen on ONE site — its besigner, its assets and the\n * messages built from it are that site's — so this table reads each site in\n * turn and names the site on every row. A row opens the template's page on\n * its own site, where its report and its recipients are; nothing here is an\n * organization-level template page, because there is no such thing.\n *\n * The reads are one listener per site, over one page of sites at a time —\n * ordered by name — each its site's query for the clauses and search in\n * force, capped at {@link ORG_TEMPLATE_CEILING} matches. The shared footer\n * pages the matches in hand; an organization with more sites than one page\n * chooses which page of sites is read with the Sites picker above the\n * table, or names sites with the Site filter. Both caps say so on screen when\n * they bite, so a missing template is never mistaken for one that does not\n * exist.\n *\n * \"New template\" asks which site the design is made on — the besigner opens\n * on a site — and skips the question when the org has one site.\n */\nexport function OrgEmailTemplatesCard() {\n const mount = useEmailOrgMount()\n return mount ? <OrgEmailTemplatesTable mount={mount} /> : null\n}\nOrgEmailTemplatesCard.displayName = 'OrgEmailTemplatesCard'\n\nfunction OrgEmailTemplatesTable(props: { mount: EmailOrgMount }) {\n const { mount } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const createHostResource = useHostResourceApi()\n const createHostVersion = useHostVersionApi()\n const sitePage = useOrgSitePage(mount)\n const gridFilter = useListGridFilter({ selectFields: [SITE_FIELD.column] })\n /*\n * Which sites are read: the ones a Site clause names, else the page of\n * sites. The other clauses and the search are ONE plan every site's query\n * carries.\n */\n const siteScope = useMemo(\n () => orgTemplateSiteScope(gridFilter.clauses),\n [gridFilter.clauses],\n )\n const sites = useMemo(\n () =>\n siteScope\n ? mount.hosts.filter((host) => siteScope.includes(host.id))\n : sitePage.sites,\n [siteScope, mount.hosts, sitePage.sites],\n )\n const plan = useMemo(\n () =>\n planListQuery(\n EMAIL_TEMPLATE_QUERY,\n {\n clauses: gridFilter.clauses.filter(\n (clause) => clause.field !== SITE_FIELD.column,\n ),\n search: gridFilter.searchWords,\n base: EMAIL_TEMPLATE_BASE,\n },\n nameSearchNormalizers,\n ),\n [gridFilter.clauses, gridFilter.searchWords],\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: ORG_TEMPLATE_FILTER_FIELDS,\n headers: ORG_TEMPLATE_FILTER_HEADERS,\n }),\n [plan.refused],\n )\n\n const [reads, setReads] = useState<Record<string, SiteRead>>({})\n const onRead = useCallback(\n (hostId: string, read: SiteRead) =>\n setReads((current) => {\n const previous = current[hostId]\n // An answer the table already holds changes nothing to draw.\n if (\n previous &&\n previous.rows === read.rows &&\n previous.truncated === read.truncated &&\n previous.settled === read.settled\n ) {\n return current\n }\n return { ...current, [hostId]: read }\n }),\n [],\n )\n\n /*\n * One row per template, named by site. The row id joins the site to the\n * screen id: two sites can hold a screen with the same id, and a grid that\n * keyed on the screen alone would draw one of them twice. Every row is an\n * answer some site's query gave; the sort only interleaves the sites'\n * answers by the name each was already ordered by.\n */\n const rows = useMemo(\n () =>\n sites\n .flatMap((site) =>\n (reads[site.id]?.rows ?? []).map((screen) => ({\n ...screen,\n $id: `${site.id}:${screen['$id']}`,\n screenId: String(screen['$id']),\n hostId: site.id,\n siteName: orgSiteName(mount, site.id),\n })),\n )\n .sort(\n (a, b) =>\n String(a['nameLower'] ?? '').localeCompare(String(b['nameLower'] ?? '')) ||\n orgSiteName(mount, a.hostId).localeCompare(\n orgSiteName(mount, b.hostId),\n ),\n ),\n [sites, reads, mount],\n )\n const settled = sites.every((site) => reads[site.id]?.settled)\n const crowded = sites.filter((site) => reads[site.id]?.truncated)\n\n /*\n * The matches in hand, a page at a time on the shared footer. Choosing\n * another page of sites, or another question, starts from the first page:\n * page four of one answer is not a position in another.\n */\n const filterOptions = useMemo(\n () => ({\n // Every site the organization has: a Site clause reads the sites it\n // names wherever they sit in the page of sites.\n site: mount.hosts.map((site) => ({ value: site.id, label: orgSiteName(mount, site.id) })),\n }),\n [mount],\n )\n /*\n * EVERY HEADER SORTS (AGL-3680), over EVERY match in hand rather than the\n * page on screen: the rows are all loaded (each site capped, and the cap\n * says so above), so the sort is exact and the footer pages the sorted\n * whole. Site sorts by the name the column shows, not the site id.\n */\n const [sortModel, setSortModel] = useState<GridSortModel>([])\n const sorted = useMemo(() => {\n const [first] = sortModel\n const value = first?.sort ? ORG_TEMPLATE_SORTS[first.field] : undefined\n return value && first?.sort ? sortListRows(rows, value, first.sort) : rows\n }, [rows, sortModel])\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const planKey = JSON.stringify({ filters: plan.filters, orderBy: plan.orderBy })\n const siteKey = sites.map((site) => site.id).join(',')\n const sortKey = JSON.stringify(sortModel)\n // A new answer, or a new order, starts again at its first page.\n useEffect(() => setPage(0), [siteKey, planKey, sortKey])\n const shown = useMemo(\n () => sorted.slice(page * pageSize, page * pageSize + pageSize),\n [sorted, page, pageSize],\n )\n const sitePageCount = Math.ceil(sitePage.count / sitePage.pageSize)\n\n const templateName = (row: Record<string, any>) =>\n String(row['displayName'] ?? 'Untitled template')\n /** The template's own page, on the site that holds it. */\n const templateHref = (row: Record<string, any>) =>\n orgSiteEmailsPath(mount, row['hostId'], `templates/${row['screenId']}`)\n const siteSubdomain = (hostId: string) =>\n mount.hosts.find((site) => site.id === hostId)?.subdomain ?? null\n const besignerHref = (\n hostId: string,\n screenId: string,\n versionId: unknown,\n ) => {\n const subdomain = siteSubdomain(hostId)\n return subdomain && versionId\n ? buildRoute(Route.SCREEN_BESIGNER, {\n orgSlug: mount.orgSlug,\n host: subdomain,\n screenId,\n versionId: String(versionId),\n })\n : null\n }\n\n const handleDelete = async (row: Record<string, any>) => {\n const confirmed = await confirm({\n title: 'Delete this template?',\n description:\n `\"${templateName(row)}\" will be removed from ` +\n `${orgSiteName(mount, row['hostId'])}. Emails already sent from it ` +\n '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', row['hostId'], 'screens', row['screenId']),\n emailTemplateSoftDelete(),\n )\n }\n\n const rowActions = (row: Record<string, any>): RowActionsMenuItem[] => {\n const page = templateHref(row)\n const editor = besignerHref(\n row['hostId'],\n row['screenId'],\n row['versionId'],\n )\n const unlinkedReason =\n 'This site has no console address yet, so it cannot be linked to.'\n return [\n {\n key: 'details',\n label: 'Open details',\n icon: <MdiIcon path={mdiPageNextOutline.path} size={0.8} />,\n href: page ?? undefined,\n disabled: !page,\n disabledReason: unlinkedReason,\n },\n {\n key: 'besigner',\n label: 'Edit in besigner',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n href: editor ?? undefined,\n disabled: !editor,\n disabledReason: row['versionId']\n ? unlinkedReason\n : 'This template has never been saved in the besigner. Open it from its page.',\n },\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleDelete(row),\n },\n ]\n }\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 const href = templateHref(row)\n return href ? (\n <AppLink\n href={href}\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 value\n )\n },\n },\n {\n field: 'site',\n headerName: 'Site',\n width: 170,\n valueGetter: (_value, row) => row.hostId,\n renderCell: ({ row }) => row.siteName,\n },\n {\n // Whose template this is, as on a site's own list.\n field: 'origin',\n headerName: 'Origin',\n width: 130,\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 /*==========================================\n * CREATE, ON A SITE.\n *\n * The same document a site's own list creates — `createEmailScreen`\n * through the quota-enforcing resources route — made as the chosen site,\n * then straight into that site's besigner. One site answers the question\n * itself; otherwise the dialog asks, defaulting to the session's pick.\n *=========================================*/\n const [choosing, setChoosing] = useState(false)\n const [createHostId, setCreateHostId] = useState('')\n const [creating, setCreating] = useState(false)\n\n const create = async (hostId: string) => {\n if (creating || !hostId) return\n setCreating(true)\n try {\n const { screenId, versionId } = await createEmailScreen(\n hostId,\n createHostResource,\n createHostVersion,\n )\n setChoosing(false)\n const editor = besignerHref(hostId, screenId, versionId)\n if (editor) {\n void router.push(editor)\n } else {\n enqueueSnackbar(\n `Template created on ${orgSiteName(mount, hostId)}. Open it from ` +\n 'that site’s Emails page.',\n { variant: 'success' },\n )\n }\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'Creating the template failed', {\n variant: 'error',\n })\n } finally {\n setCreating(false)\n }\n }\n\n const handleNew = () => {\n if (mount.hosts.length === 1) return void create(mount.hosts[0].id)\n setCreateHostId(mount.pickedHostId ?? '')\n setChoosing(true)\n }\n\n const noSites = mount.hostsReady && mount.hosts.length === 0\n\n return (\n <CardDisplay\n header={'Templates'}\n help={pluginDocsHelp('emailCampaigns', { anchor: '#organization-emails-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={noSites || creating}\n onClick={handleNew}\n >\n {creating ? 'Creating…' : 'New template'}\n </Button>\n ),\n }}\n >\n {sites.map((site) => (\n <SiteTemplatesReader\n key={site.id}\n hostId={site.id}\n plan={plan}\n onRead={onRead}\n />\n ))}\n <Stack spacing={1.5}>\n {/*\n WHICH SITES' TEMPLATES ARE LISTED, when there are more sites than\n one page holds. Only the chosen page's sites are read; a template on\n a site of another page is one choice away, not missing. It picks\n what is read, a scope, so it is not one of the grid's filters.\n */}\n {sitePageCount > 1 && !siteScope ? (\n <TextField\n select\n size=\"small\"\n label=\"Sites\"\n value={sitePage.page}\n onChange={(event) => sitePage.setPage(Number(event.target.value))}\n sx={{ maxWidth: 260 }}\n >\n {Array.from({ length: sitePageCount }, (_, index) => (\n <MenuItem key={index} value={index}>\n {orgSitePageLabel({\n from: index * sitePage.pageSize + 1,\n to: Math.min((index + 1) * sitePage.pageSize, sitePage.count),\n count: sitePage.count,\n })}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {noSites ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. A template is designed on ' +\n 'a site, in its besigner, so it appears here once there is one.'}\n </Typography>\n ) : rows.length === 0 && !filtering ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled\n ? 'None of these sites has an email template yet. Design one ' +\n 'here, then send it from a campaign — a new template opens in ' +\n 'the besigner with email-safe components only.'\n : 'Loading templates…'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={ORG_TEMPLATE_FILTER_FIELDS}\n headers={ORG_TEMPLATE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices refused={refusals} notices={plan.notices} />\n <ListTable\n aria-label=\"Email templates\"\n rows={shown}\n columns={listFilterGridColumns(\n columns,\n ORG_TEMPLATE_FILTER_FIELDS,\n filterOptions,\n ORG_TEMPLATE_FILTER_HEADERS,\n )}\n rowHeight={TABLE_ROW_HEIGHT}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n // The panel and the search go to every site's query; a header\n // sorts every match in hand before the footer pages it.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n sortingMode=\"server\"\n sortModel={sortModel}\n onSortModelChange={setSortModel}\n noRowsLabel=\"No templates match these filters\"\n onOpen={(_id, row) => {\n const href = templateHref(row)\n if (href) router.push(href)\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {crowded.map((site) => {\n const href = orgSiteEmailsPath(mount, site.id, 'templates')\n return (\n <Alert key={site.id} severity=\"info\">\n {`${orgSiteName(mount, site.id)} has more than ` +\n `${ORG_TEMPLATE_CEILING} ${filtering ? 'matching templates' : 'templates'}, ` +\n 'and only the first of them are listed here. '}\n {href ? (\n <AppLink href={href}>{'See all of its templates'}</AppLink>\n ) : null}\n </Alert>\n )\n })}\n </Stack>\n\n <Dialog\n open={choosing}\n onClose={creating ? undefined : () => setChoosing(false)}\n fullWidth\n maxWidth=\"xs\"\n >\n <DialogTitle>{'New template'}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'A template is designed on one site, in its besigner, and is ' +\n 'sent from that site’s campaigns.'}\n </DialogContentText>\n <OrgSiteSelect\n mount={mount}\n label=\"Site\"\n value={createHostId}\n onChange={setCreateHostId}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setChoosing(false)} disabled={creating}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!createHostId || creating}\n onClick={() => void create(createHostId)}\n >\n {creating ? 'Creating…' : 'Create and open'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nOrgEmailTemplatesTable.displayName = 'OrgEmailTemplatesTable'\n\nexport default OrgEmailTemplatesCard\n"],"names":["buildRoute","pluginDocsHelp","Route","mdiDeleteOutline","mdiPageNextOutline","mdiPencilOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","listFilterGridColumns","planListQuery","useListGridFilter","nameSearchNormalizers","TABLE_PAGE_SIZE_DEFAULT","TABLE_ROW_HEIGHT","useSnackbar","ceilingedWindow","listQueryConstraints","useFirestore","useFirestoreCollection","useHostResourceApi","useHostVersionApi","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","Typography","sortListRows","collection","doc","limit","query","updateDoc","useRouter","useCallback","useEffect","useMemo","useState","EMAIL_TEMPLATE_BASE","EMAIL_TEMPLATE_FILTER_HEADERS","EMAIL_TEMPLATE_QUERY","templateProvenance","createEmailScreen","emailTemplateSoftDelete","OrgSiteSelect","orgSiteEmailsPath","orgSiteName","orgSitePageLabel","useEmailOrgMount","useOrgSitePage","ORG_TEMPLATE_CEILING","SITE_FIELD","column","kind","path","operators","ORG_TEMPLATE_FILTER_FIELDS","fields","ORG_TEMPLATE_FILTER_HEADERS","site","orgTemplateSiteScope","clauses","named","filter","clause","field","map","value","split","entry","trim","Boolean","length","reduce","kept","next","id","includes","ORG_TEMPLATE_SORTS","displayName","row","String","origin","SiteTemplatesReader","props","hostId","plan","onRead","firestore","planKey","JSON","stringify","filters","orderBy","data","status","idField","window","rows","truncated","settled","OrgEmailTemplatesCard","mount","OrgEmailTemplatesTable","router","enqueueSnackbar","confirm","createHostResource","createHostVersion","sitePage","gridFilter","selectFields","siteScope","sites","hosts","host","search","searchWords","base","filtering","some","word","refusals","refused","headers","reads","setReads","read","current","previous","flatMap","screen","$id","screenId","siteName","sort","a","b","localeCompare","every","crowded","filterOptions","label","sortModel","setSortModel","sorted","first","undefined","page","setPage","pageSize","setPageSize","siteKey","join","sortKey","shown","slice","sitePageCount","Math","ceil","count","templateName","templateHref","siteSubdomain","find","subdomain","besignerHref","versionId","SCREEN_BESIGNER","orgSlug","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","rowActions","editor","unlinkedReason","key","icon","size","href","disabled","disabledReason","destructive","onClick","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","event","stopPropagation","width","variant","items","choosing","setChoosing","createHostId","setCreateHostId","creating","setCreating","create","push","error","console","message","handleNew","pickedHostId","noSites","hostsReady","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","spacing","select","onChange","Number","target","sx","maxWidth","Array","from","_","index","to","min","setClauses","options","notices","aria-label","rowHeight","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","onSortModelChange","noRowsLabel","onOpen","_id","rowCount","onPageChange","onPageSizeChange","severity","open","onClose","fullWidth","mb"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,UAAU,EAAEC,cAAc,EAAEC,KAAK,QAAQ,eAAc;AAChE,SACEC,gBAAgB,EAChBC,kBAAkB,EAClBC,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;AAG7D,SAEEC,qBAAqB,QAChB,8CAA6C;AACpD,SAEEC,aAAa,QACR,6CAA4C;AACnD,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,qBAAqB,QAAQ,qCAAoC;AAC1E,SACEC,uBAAuB,EACvBC,gBAAgB,QACX,8CAA6C;AACpD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,eAAe,QAAQ,+DAA8D;AAC9F,SAASC,oBAAoB,QAAQ,sDAAqD;AAC1F,SACEC,YAAY,EACZC,sBAAsB,EACtBC,kBAAkB,EAClBC,iBAAiB,QACZ,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,iBAAiB,EACjBC,WAAW,EACXC,QAAQ,EACRC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AAEtB,SAEEC,YAAY,QACP,uDAAsD;AAC7D,SAASC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAEC,KAAK,EAAEC,SAAS,QAAQ,qBAAoB;AAC7E,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SACEC,mBAAmB,EACnBC,6BAA6B,EAC7BC,oBAAoB,QACf,+BAA2B;AAClC,SAASC,kBAAkB,QAAQ,kCAA8B;AACjE,SAASC,iBAAiB,QAAQ,kCAA8B;AAChE,SAASC,uBAAuB,QAAQ,yCAAqC;AAC7E,SACEC,aAAa,EACbC,iBAAiB,EACjBC,WAAW,EACXC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,QAET,uBAAmB;AAE1B;;;;;;CAMC,GACD,OAAO,MAAMC,uBAAuB,GAAE;AAEtC;;;;;;;;;;CAUC,GACD,MAAMC,aAA8B;IAClCC,QAAQ;IACRC,MAAM;IACNC,MAAM;IACNC,WAAW;QAAC;QAAU;KAAU;AAClC;AACA,MAAMC,6BAAyD;OAC1DhB,qBAAqBiB,MAAM;IAC9BN;CACD;AACD,MAAMO,8BAAgE,aACjEnB;IACHoB,MAAM;;AAGR,kEAAkE,GAClE,OAAO,SAASC,qBACdC,OAAoC;IAEpC,MAAMC,QAAQD,QACXE,MAAM,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAKd,WAAWC,MAAM,EACrDc,GAAG,CAAC,CAACF,SACJA,OAAOG,KAAK,CACTC,KAAK,CAAC,KACNF,GAAG,CAAC,CAACG,QAAUA,MAAMC,IAAI,IACzBP,MAAM,CAACQ;IAEd,IAAI,CAACT,MAAMU,MAAM,EAAE,OAAO;IAC1B,+DAA+D;IAC/D,OAAOV,MAAMW,MAAM,CAAC,CAACC,MAAMC,OAASD,KAAKX,MAAM,CAAC,CAACa,KAAOD,KAAKE,QAAQ,CAACD;AACxE;AAEA,2DAA2D,GAC3D,MAAME,qBAAkF;IACtFC,aAAa,CAACC;YAAeA;eAAPC,QAAOD,mBAAAA,GAAG,CAAC,cAAc,YAAlBA,mBAAsB;;IACnDrB,MAAM,CAACqB,MAAQA,GAAG,CAAC,WAAW;IAC9BE,QAAQ,CAACF,MACPvC,mBAAmBuC,KAAKE,MAAM,KAAK,cAAc,cAAc;AACnE;AAWA;;;;;;;;;;;;CAYC,GACD,SAASC,oBAAoBC,KAI5B;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEC,MAAM,EAAE,GAAGH;IACjC,MAAMI,YAAY7E;IAClB,MAAM8E,UAAUC,KAAKC,SAAS,CAAC;QAAEC,SAASN,KAAKM,OAAO;QAAEC,SAASP,KAAKO,OAAO;IAAC;IAC9E,MAAM,EAAEC,IAAI,EAAEC,MAAM,EAAE,GAAGnF,uBACvB,IACEmB,MACEH,WAAW4D,WAAW,SAASH,QAAQ,eACpC3E,qBAAqB4E,OACxBxD,MAAMoB,uBAAuB,KAEjC;QAACsC;QAAWH;QAAQI;KAAQ,EAC5B;QAAEO,SAAS;IAAM;IAEnB,4EAA4E;IAC5E,4CAA4C;IAC5C,MAAMC,SAAS7D,QACb,IAAM3B,gBAAgBqF,MAAM5C,uBAC5B;QAAC4C;KAAK;IAER3D,UAAU;QACRoD,OAAOF,QAAQ;YACba,MAAMD,OAAOC,IAAI;YACjBC,WAAWF,OAAOE,SAAS;YAC3BC,SAASL,WAAW;QACtB;IACF,GAAG;QAACE;QAAQF;QAAQV;QAAQE;KAAO;IACnC,OAAO;AACT;AACAJ,oBAAoBJ,WAAW,GAAG;AAElC;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASsB;IACd,MAAMC,QAAQtD;IACd,OAAOsD,sBAAQ,KAACC;QAAuBD,OAAOA;SAAY;AAC5D;AACAD,sBAAsBtB,WAAW,GAAG;AAEpC,SAASwB,uBAAuBnB,KAA+B;IAC7D,MAAM,EAAEkB,KAAK,EAAE,GAAGlB;IAClB,MAAMI,YAAY7E;IAClB,MAAM6F,SAASvE;IACf,MAAM,EAAEwE,eAAe,EAAE,GAAGjG;IAC5B,MAAM,EAAEkG,OAAO,EAAE,GAAGhH;IACpB,MAAMiH,qBAAqB9F;IAC3B,MAAM+F,oBAAoB9F;IAC1B,MAAM+F,WAAW5D,eAAeqD;IAChC,MAAMQ,aAAa1G,kBAAkB;QAAE2G,cAAc;YAAC5D,WAAWC,MAAM;SAAC;IAAC;IACzE;;;;GAIC,GACD,MAAM4D,YAAY5E,QAChB,IAAMwB,qBAAqBkD,WAAWjD,OAAO,GAC7C;QAACiD,WAAWjD,OAAO;KAAC;IAEtB,MAAMoD,QAAQ7E,QACZ,IACE4E,YACIV,MAAMY,KAAK,CAACnD,MAAM,CAAC,CAACoD,OAASH,UAAUnC,QAAQ,CAACsC,KAAKvC,EAAE,KACvDiC,SAASI,KAAK,EACpB;QAACD;QAAWV,MAAMY,KAAK;QAAEL,SAASI,KAAK;KAAC;IAE1C,MAAM3B,OAAOlD,QACX,IACEjC,cACEqC,sBACA;YACEqB,SAASiD,WAAWjD,OAAO,CAACE,MAAM,CAChC,CAACC,SAAWA,OAAOC,KAAK,KAAKd,WAAWC,MAAM;YAEhDgE,QAAQN,WAAWO,WAAW;YAC9BC,MAAMhF;QACR,GACAjC,wBAEJ;QAACyG,WAAWjD,OAAO;QAAEiD,WAAWO,WAAW;KAAC;IAE9C,MAAME,YACJT,WAAWjD,OAAO,CAACW,MAAM,GAAG,KAAKsC,WAAWO,WAAW,CAACG,IAAI,CAAC,CAACC,OAASA,KAAKnD,IAAI;IAClF,MAAMoD,WAAWtF,QACf,IACEtC,kBAAkBwF,KAAKqC,OAAO,EAAE;YAC9BlE,QAAQD;YACRoE,SAASlE;QACX,IACF;QAAC4B,KAAKqC,OAAO;KAAC;IAGhB,MAAM,CAACE,OAAOC,SAAS,GAAGzF,SAAmC,CAAC;IAC9D,MAAMkD,SAASrD,YACb,CAACmD,QAAgB0C,OACfD,SAAS,CAACE;YACR,MAAMC,WAAWD,OAAO,CAAC3C,OAAO;YAChC,6DAA6D;YAC7D,IACE4C,YACAA,SAAS/B,IAAI,KAAK6B,KAAK7B,IAAI,IAC3B+B,SAAS9B,SAAS,KAAK4B,KAAK5B,SAAS,IACrC8B,SAAS7B,OAAO,KAAK2B,KAAK3B,OAAO,EACjC;gBACA,OAAO4B;YACT;YACA,OAAO,aAAKA;gBAAS,CAAC3C,OAAO,EAAE0C;;QACjC,IACF,EAAE;IAGJ;;;;;;GAMC,GACD,MAAM7B,OAAO9D,QACX,IACE6E,MACGiB,OAAO,CAAC,CAACvE;;gBACPkE;mBAAD,UAACA,iBAAAA,KAAK,CAAClE,KAAKiB,EAAE,CAAC,qBAAdiD,eAAgB3B,IAAI,mBAAI,EAAE,EAAEhC,GAAG,CAAC,CAACiE,SAAY,aACzCA;oBACHC,KAAK,GAAGzE,KAAKiB,EAAE,CAAC,CAAC,EAAEuD,MAAM,CAAC,MAAM,EAAE;oBAClCE,UAAUpD,OAAOkD,MAAM,CAAC,MAAM;oBAC9B9C,QAAQ1B,KAAKiB,EAAE;oBACf0D,UAAUxF,YAAYwD,OAAO3C,KAAKiB,EAAE;;WAGvC2D,IAAI,CACH,CAACC,GAAGC;gBACKD,cAA2CC;mBAAlDxD,QAAOuD,eAAAA,CAAC,CAAC,YAAY,YAAdA,eAAkB,IAAIE,aAAa,CAACzD,QAAOwD,eAAAA,CAAC,CAAC,YAAY,YAAdA,eAAkB,QACpE3F,YAAYwD,OAAOkC,EAAEnD,MAAM,EAAEqD,aAAa,CACxC5F,YAAYwD,OAAOmC,EAAEpD,MAAM;YAGrC;QAAC4B;QAAOY;QAAOvB;KAAM;IAEvB,MAAMF,UAAUa,MAAM0B,KAAK,CAAC,CAAChF;YAASkE;gBAAAA,iBAAAA,KAAK,CAAClE,KAAKiB,EAAE,CAAC,qBAAdiD,eAAgBzB,OAAO;;IAC7D,MAAMwC,UAAU3B,MAAMlD,MAAM,CAAC,CAACJ;YAASkE;gBAAAA,iBAAAA,KAAK,CAAClE,KAAKiB,EAAE,CAAC,qBAAdiD,eAAgB1B,SAAS;;IAEhE;;;;GAIC,GACD,MAAM0C,gBAAgBzG,QACpB,IAAO,CAAA;YACL,oEAAoE;YACpE,gDAAgD;YAChDuB,MAAM2C,MAAMY,KAAK,CAAChD,GAAG,CAAC,CAACP,OAAU,CAAA;oBAAEQ,OAAOR,KAAKiB,EAAE;oBAAEkE,OAAOhG,YAAYwD,OAAO3C,KAAKiB,EAAE;gBAAE,CAAA;QACxF,CAAA,GACA;QAAC0B;KAAM;IAET;;;;;GAKC,GACD,MAAM,CAACyC,WAAWC,aAAa,GAAG3G,SAAwB,EAAE;IAC5D,MAAM4G,SAAS7G,QAAQ;QACrB,MAAM,CAAC8G,MAAM,GAAGH;QAChB,MAAM5E,QAAQ+E,CAAAA,yBAAAA,MAAOX,IAAI,IAAGzD,kBAAkB,CAACoE,MAAMjF,KAAK,CAAC,GAAGkF;QAC9D,OAAOhF,UAAS+E,yBAAAA,MAAOX,IAAI,IAAG5G,aAAauE,MAAM/B,OAAO+E,MAAMX,IAAI,IAAIrC;IACxE,GAAG;QAACA;QAAM6C;KAAU;IACpB,MAAM,CAACK,MAAMC,QAAQ,GAAGhH,SAAS;IACjC,MAAM,CAACiH,UAAUC,YAAY,GAAGlH,SAAS/B;IACzC,MAAMmF,UAAUC,KAAKC,SAAS,CAAC;QAAEC,SAASN,KAAKM,OAAO;QAAEC,SAASP,KAAKO,OAAO;IAAC;IAC9E,MAAM2D,UAAUvC,MAAM/C,GAAG,CAAC,CAACP,OAASA,KAAKiB,EAAE,EAAE6E,IAAI,CAAC;IAClD,MAAMC,UAAUhE,KAAKC,SAAS,CAACoD;IAC/B,gEAAgE;IAChE5G,UAAU,IAAMkH,QAAQ,IAAI;QAACG;QAAS/D;QAASiE;KAAQ;IACvD,MAAMC,QAAQvH,QACZ,IAAM6G,OAAOW,KAAK,CAACR,OAAOE,UAAUF,OAAOE,WAAWA,WACtD;QAACL;QAAQG;QAAME;KAAS;IAE1B,MAAMO,gBAAgBC,KAAKC,IAAI,CAAClD,SAASmD,KAAK,GAAGnD,SAASyC,QAAQ;IAElE,MAAMW,eAAe,CAACjF;YACbA;eAAPC,QAAOD,mBAAAA,GAAG,CAAC,cAAc,YAAlBA,mBAAsB;;IAC/B,wDAAwD,GACxD,MAAMkF,eAAe,CAAClF,MACpBnC,kBAAkByD,OAAOtB,GAAG,CAAC,SAAS,EAAE,CAAC,UAAU,EAAEA,GAAG,CAAC,WAAW,EAAE;IACxE,MAAMmF,gBAAgB,CAAC9E;;YACrBiB;wBAAAA,oBAAAA,MAAMY,KAAK,CAACkD,IAAI,CAAC,CAACzG,OAASA,KAAKiB,EAAE,KAAKS,4BAAvCiB,kBAAgD+D,SAAS,mBAAI;;IAC/D,MAAMC,eAAe,CACnBjF,QACAgD,UACAkC;QAEA,MAAMF,YAAYF,cAAc9E;QAChC,OAAOgF,aAAaE,YAChBtL,WAAWE,MAAMqL,eAAe,EAAE;YAChCC,SAASnE,MAAMmE,OAAO;YACtBtD,MAAMkD;YACNhC;YACAkC,WAAWtF,OAAOsF;QACpB,KACA;IACN;IAEA,MAAMG,eAAe,OAAO1F;QAC1B,MAAM2F,YAAY,MAAMjE,QAAQ;YAC9BkE,OAAO;YACPC,aACE,CAAC,CAAC,EAAEZ,aAAajF,KAAK,uBAAuB,CAAC,GAC9C,GAAGlC,YAAYwD,OAAOtB,GAAG,CAAC,SAAS,EAAE,8BAA8B,CAAC,GACpE;YACF8F,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,WAAW;QAChB,MAAM3I,UACJH,IAAI2D,WAAW,SAASR,GAAG,CAAC,SAAS,EAAE,WAAWA,GAAG,CAAC,WAAW,GACjErC;IAEJ;IAEA,MAAMwI,aAAa,CAACnG;QAClB,MAAMoE,OAAOc,aAAalF;QAC1B,MAAMoG,SAASd,aACbtF,GAAG,CAAC,SAAS,EACbA,GAAG,CAAC,WAAW,EACfA,GAAG,CAAC,YAAY;QAElB,MAAMqG,iBACJ;QACF,OAAO;YACL;gBACEC,KAAK;gBACLxC,OAAO;gBACPyC,oBAAM,KAAC9L;oBAAQ6D,MAAMjE,mBAAmBiE,IAAI;oBAAEkI,MAAM;;gBACpDC,IAAI,EAAErC,eAAAA,OAAQD;gBACduC,UAAU,CAACtC;gBACXuC,gBAAgBN;YAClB;YACA;gBACEC,KAAK;gBACLxC,OAAO;gBACPyC,oBAAM,KAAC9L;oBAAQ6D,MAAMhE,iBAAiBgE,IAAI;oBAAEkI,MAAM;;gBAClDC,IAAI,EAAEL,iBAAAA,SAAUjC;gBAChBuC,UAAU,CAACN;gBACXO,gBAAgB3G,GAAG,CAAC,YAAY,GAC5BqG,iBACA;YACN;YACA;gBACEC,KAAK;gBACLxC,OAAO;gBACPyC,oBAAM,KAAC9L;oBAAQ6D,MAAMlE,iBAAiBkE,IAAI;oBAAEkI,MAAM;;gBAClDI,aAAa;gBACbC,SAAS,IAAM,KAAKnB,aAAa1F;YACnC;SACD;IACH;IAEA,MAAM8G,UAAwB;QAC5B;YACE7H,OAAO;YACP8H,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,aAAa,CAACC,QAAQnH,MAAQiF,aAAajF;YAC3CoH,YAAY,CAAC,EAAEpH,GAAG,EAAEb,KAAK,EAAE;gBACzB,MAAMsH,OAAOvB,aAAalF;gBAC1B,OAAOyG,qBACL,KAAClM;oBACCkM,MAAMA;oBACN,+DAA+D;oBAC/D,4CAA4C;oBAC5CI,SAAS,CAACQ,QACRA,MAAMC,eAAe;8BAGtBnI;qBAGHA;YAEJ;QACF;QACA;YACEF,OAAO;YACP8H,YAAY;YACZQ,OAAO;YACPL,aAAa,CAACC,QAAQnH,MAAQA,IAAIK,MAAM;YACxC+G,YAAY,CAAC,EAAEpH,GAAG,EAAE,GAAKA,IAAIsD,QAAQ;QACvC;QACA;YACE,mDAAmD;YACnDrE,OAAO;YACP8H,YAAY;YACZQ,OAAO;YACPL,aAAa,CAACC,QAAQnH,MACpBvC,mBAAmBuC,KAAKE,MAAM,KAAK,cAAc,cAAc;YACjEkH,YAAY,CAAC,EAAEjI,KAAK,EAAE,GACpBA,UAAU,4BACR,KAAClD;oBAAKuK,MAAK;oBAAQ1C,OAAM;mCAEzB,KAACpH;oBAAW8K,SAAQ;oBAAQxB,OAAM;8BAC/B;;QAGT;QACA/K,kBACE,CAAC+E,oBACC,KAACjF;gBAAe+I,OAAOmB,aAAajF;gBAAMyH,OAAOtB,WAAWnG;gBAE9D;YAAEuH,OAAO;QAAG;KAEf;IAED;;;;;;;6CAO2C,GAC3C,MAAM,CAACG,UAAUC,YAAY,GAAGtK,SAAS;IACzC,MAAM,CAACuK,cAAcC,gBAAgB,GAAGxK,SAAS;IACjD,MAAM,CAACyK,UAAUC,YAAY,GAAG1K,SAAS;IAEzC,MAAM2K,SAAS,OAAO3H;QACpB,IAAIyH,YAAY,CAACzH,QAAQ;QACzB0H,YAAY;QACZ,IAAI;YACF,MAAM,EAAE1E,QAAQ,EAAEkC,SAAS,EAAE,GAAG,MAAM7H,kBACpC2C,QACAsB,oBACAC;YAEF+F,YAAY;YACZ,MAAMvB,SAASd,aAAajF,QAAQgD,UAAUkC;YAC9C,IAAIa,QAAQ;gBACV,KAAK5E,OAAOyG,IAAI,CAAC7B;YACnB,OAAO;gBACL3E,gBACE,CAAC,oBAAoB,EAAE3D,YAAYwD,OAAOjB,QAAQ,eAAe,CAAC,GAChE,4BACF;oBAAEmH,SAAS;gBAAU;YAEzB;QACF,EAAE,OAAOU,OAAY;;YACnBC,QAAQD,KAAK,CAACA;YACdzG,wBAAgByG,yBAAAA,MAAOE,OAAO,mBAAI,gCAAgC;gBAChEZ,SAAS;YACX;QACF,SAAU;YACRO,YAAY;QACd;IACF;IAEA,MAAMM,YAAY;YAEA/G;QADhB,IAAIA,MAAMY,KAAK,CAAC1C,MAAM,KAAK,GAAG,OAAO,KAAKwI,OAAO1G,MAAMY,KAAK,CAAC,EAAE,CAACtC,EAAE;QAClEiI,iBAAgBvG,sBAAAA,MAAMgH,YAAY,YAAlBhH,sBAAsB;QACtCqG,YAAY;IACd;IAEA,MAAMY,UAAUjH,MAAMkH,UAAU,IAAIlH,MAAMY,KAAK,CAAC1C,MAAM,KAAK;IAE3D,qBACE,MAAChF;QACCiO,QAAQ;QACRC,MAAMxO,eAAe,kBAAkB;YAAEyO,QAAQ;QAA4B;QAC7EC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAAC/M;gBACCwK,MAAK;gBACLgB,SAAQ;gBACRd,UAAU6B,WAAWT;gBACrBjB,SAASwB;0BAERP,WAAW,cAAc;;QAGhC;;YAEC7F,MAAM/C,GAAG,CAAC,CAACP,qBACV,KAACwB;oBAECE,QAAQ1B,KAAKiB,EAAE;oBACfU,MAAMA;oBACNC,QAAQA;mBAHH5B,KAAKiB,EAAE;0BAMhB,MAACpD;gBAAMwM,SAAS;;oBAObnE,gBAAgB,KAAK,CAAC7C,0BACrB,KAACvF;wBACCwM,MAAM;wBACNzC,MAAK;wBACL1C,OAAM;wBACN3E,OAAO0C,SAASuC,IAAI;wBACpB8E,UAAU,CAAC7B,QAAUxF,SAASwC,OAAO,CAAC8E,OAAO9B,MAAM+B,MAAM,CAACjK,KAAK;wBAC/DkK,IAAI;4BAAEC,UAAU;wBAAI;kCAEnBC,MAAMC,IAAI,CAAC;4BAAEhK,QAAQqF;wBAAc,GAAG,CAAC4E,GAAGC,sBACzC,KAACnN;gCAAqB4C,OAAOuK;0CAC1B3L,iBAAiB;oCAChByL,MAAME,QAAQ7H,SAASyC,QAAQ,GAAG;oCAClCqF,IAAI7E,KAAK8E,GAAG,CAAC,AAACF,CAAAA,QAAQ,CAAA,IAAK7H,SAASyC,QAAQ,EAAEzC,SAASmD,KAAK;oCAC5DA,OAAOnD,SAASmD,KAAK;gCACvB;+BALa0E;yBASjB;oBACHnB,wBACC,KAAC7L;wBAAW8K,SAAQ;wBAAQxB,OAAM;kCAC/B,mEACC;yBAEF9E,KAAK1B,MAAM,KAAK,KAAK,CAAC+C,0BACxB,KAAC7F;wBAAW8K,SAAQ;wBAAQxB,OAAM;kCAC/B5E,UACG,+DACA,kEACA,kDACA;uCAGN;;0CACE,KAACzG;gCACC8D,QAAQD;gCACRoE,SAASlE;gCACTG,SAASiD,WAAWjD,OAAO;gCAC3BqK,UAAUpH,WAAW+H,UAAU;gCAC/BC,SAASjG;;0CAEX,KAAChJ;gCAAiB8H,SAASD;gCAAUqH,SAASzJ,KAAKyJ,OAAO;;0CAC1D,KAAC/O;gCACCgP,cAAW;gCACX9I,MAAMyD;gCACNmC,SAAS5L,sBACP4L,SACAtI,4BACAqF,eACAnF;gCAEFuL,WAAW1O;gCACX,8DAA8D;gCAC9D2O,UAAU;gCACV,8DAA8D;gCAC9D,wDAAwD;gCACxDC,YAAW;gCACXC,aAAatI,WAAWsI,WAAW;gCACnCC,qBAAqBvI,WAAWuI,mBAAmB;gCACnDC,WAAW;gCACXC,aAAY;gCACZxG,WAAWA;gCACXyG,mBAAmBxG;gCACnByG,aAAY;gCACZC,QAAQ,CAACC,KAAK3K;oCACZ,MAAMyG,OAAOvB,aAAalF;oCAC1B,IAAIyG,MAAMjF,OAAOyG,IAAI,CAACxB;gCACxB;;0CAEF,KAAC7L;gCACCwJ,MAAMA;gCACNE,UAAUA;gCACVsG,UAAUjG,MAAMnF,MAAM;gCACtBwF,OAAO9D,KAAK1B,MAAM;gCAClBqL,cAAcxG;gCACdyG,kBAAkBvG;;;;oBAIvBX,QAAQ1E,GAAG,CAAC,CAACP;wBACZ,MAAM8H,OAAO5I,kBAAkByD,OAAO3C,KAAKiB,EAAE,EAAE;wBAC/C,qBACE,MAAC7D;4BAAoBgP,UAAS;;gCAC3B,GAAGjN,YAAYwD,OAAO3C,KAAKiB,EAAE,EAAE,eAAe,CAAC,GAC9C,GAAG1B,qBAAqB,CAAC,EAAEqE,YAAY,uBAAuB,YAAY,EAAE,CAAC,GAC7E;gCACDkE,qBACC,KAAClM;oCAAQkM,MAAMA;8CAAO;qCACpB;;2BANM9H,KAAKiB,EAAE;oBASvB;;;0BAGF,MAAC1D;gBACC8O,MAAMtD;gBACNuD,SAASnD,WAAW3D,YAAY,IAAMwD,YAAY;gBAClDuD,SAAS;gBACT5B,UAAS;;kCAET,KAAChN;kCAAa;;kCACd,MAACF;;0CACC,KAACC;gCAAkBgN,IAAI;oCAAE8B,IAAI;gCAAE;0CAC5B,iEACC;;0CAEJ,KAACvN;gCACC0D,OAAOA;gCACPwC,OAAM;gCACN3E,OAAOyI;gCACPsB,UAAUrB;;;;kCAGd,MAAC1L;;0CACC,KAACH;gCAAO6K,SAAS,IAAMc,YAAY;gCAAQjB,UAAUoB;0CAClD;;0CAEH,KAAC9L;gCACCwL,SAAQ;gCACRd,UAAU,CAACkB,gBAAgBE;gCAC3BjB,SAAS,IAAM,KAAKmB,OAAOJ;0CAE1BE,WAAW,cAAc;;;;;;;;AAMtC;AACAvG,uBAAuBxB,WAAW,GAAG;AAErC,eAAesB,sBAAqB"}
@@ -323,7 +323,7 @@ function OrgSendingTables(props) {
323
323
  return /*#__PURE__*/ _jsxs(CardDisplay, {
324
324
  header: "Sending",
325
325
  help: pluginDocsHelp('emailCampaigns', {
326
- anchor: '#sending-domains'
326
+ anchor: '#two-ways-to-get-a-domain'
327
327
  }),
328
328
  contentGutterX: true,
329
329
  contentGutterY: true,