@aglyn/plugins-marketing 1.0.0-beta.229 → 1.0.0-beta.231
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +16 -15
- package/src/lib/components/campaign-conversions-card.js +23 -11
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-list-zones.d.ts +33 -0
- package/src/lib/components/campaign-list-zones.js +30 -0
- package/src/lib/components/campaign-list-zones.js.map +1 -0
- package/src/lib/components/campaign-report-card.js +20 -3
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +42 -13
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/experiment-zones.d.ts +11 -0
- package/src/lib/components/experiment-zones.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +5 -1
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/host-overlays-card.component.js +139 -24
- package/src/lib/components/host-overlays-card.component.js.map +1 -1
- package/src/lib/components/marketing-insight-zones.d.ts +28 -0
- package/src/lib/components/marketing-insight-zones.js +30 -0
- package/src/lib/components/marketing-insight-zones.js.map +1 -0
- package/src/lib/components/overlay-zones.d.ts +84 -0
- package/src/lib/components/overlay-zones.js +38 -0
- package/src/lib/components/overlay-zones.js.map +1 -0
- package/src/lib/components/site-runtime.js +10 -1
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/model/campaign-list-query.d.ts +2 -2
- package/src/lib/model/campaign-list-query.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/overlay-drafts.d.ts +97 -0
- package/src/lib/model/overlay-drafts.js +192 -0
- package/src/lib/model/overlay-drafts.js.map +1 -0
- package/src/lib/plugin.js +12 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/email-events.js +8 -0
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/marketing-ai-capabilities.d.ts +24 -0
- package/src/lib/server/marketing-ai-capabilities.js +225 -0
- package/src/lib/server/marketing-ai-capabilities.js.map +1 -0
- package/src/lib/server/marketing-drafts.d.ts +86 -0
- package/src/lib/server/marketing-drafts.js +354 -0
- package/src/lib/server/marketing-drafts.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +18 -0
- package/src/lib/server/marketing-figures.js +259 -1
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server/overlay-figures.d.ts +36 -0
- package/src/lib/server/overlay-figures.js +158 -0
- package/src/lib/server/overlay-figures.js.map +1 -0
- package/src/lib/server.js +10 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaigns-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, createResourceUid, pluginDocsHelp, Route } from '@aglyn/aglyn'\n/*\n * The MODULE, not the barrel, for the two PURE helpers — a spec that mocks\n * `@aglyn/tenant-feature-instance` wholesale to stage its Firestore hooks\n * would otherwise lose them, and neither is a hook.\n */\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { mdiDeleteOutline, mdiEyeOutline } 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 { hiddenFilterVisibility } 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 { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { collection, limit, query, setDoc, where } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n campaignListRows,\n campaignSiteIds,\n campaignVisibleTo,\n type CampaignAggregate,\n type CampaignListRow,\n type CampaignSend,\n type EmailCampaign,\n} from '../model/campaign-container'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport {\n CAMPAIGN_SITE_SEARCH_NOTICE,\n SINGLE_SENDS_BASE,\n campaignContainerSearchFields,\n campaignContainersListQuery,\n campaignContainersScope,\n campaignSendsScope,\n campaignSingleSendsListQuery,\n campaignSiteSearchClause,\n} from '../model/campaign-list-query'\nimport {\n campaignContainerDoc,\n campaignContainersCollection,\n campaignSendsCollection,\n} from './campaign-queries'\nimport {\n orgSiteName,\n orgSiteOptions,\n topicCatalogHostId,\n useMarketingOrgId,\n useMarketingOrgMount,\n type MarketingOrgMount,\n} from './marketing-org-mount'\nimport { useCampaignManageApi } from './use-campaign-send-api'\nimport { useCampaignTopicOptions } from './use-campaign-topic-options'\n\n/**\n * How many emails one read of a page's campaigns covers, for their figures.\n *\n * A ceiling on the ROLLUP read, not on the list: the list is paged by its\n * own query, and this reads the emails of the campaigns on the page on\n * screen, thirty campaigns to a read (`in` takes thirty ids), so the largest\n * page is two reads of this many. Past it, the figures say so.\n */\nconst ROLLUP_CEILING = 50\n\n/** Firestore's cap on an `in` clause's values. */\nconst IN_LIMIT = 30\n\n/** Which kind of row the table shows: the two are separate queries. */\ntype CampaignListKind = 'campaign' | 'single'\n\nconst formatDay = (ms: number | null): string =>\n ms === null ? '' : new Date(ms).toLocaleDateString()\n\n/** A rolled-up figure, or an em dash where nothing recorded one. */\nconst figure = (value: CampaignAggregate): string =>\n value.value === null ? '—' : value.value.toLocaleString()\n\nconst WINDOW_LABEL: Record<CampaignListRow['windowState'], string> = {\n undated: 'No dates',\n upcoming: 'Upcoming',\n running: 'Running',\n ended: 'Ended',\n}\n\n/*\n * What the campaigns grid's Filters panel offers, each clause and the search\n * on the list's Firestore query (AGL-3321, `campaignContainersListQuery` and\n * `campaignSingleSendsListQuery`). The Window column is derived from a\n * campaign's dates against the clock, which no query can ask, so it is not\n * offered; the one date range is Created, the list's own order.\n */\nconst CAMPAIGN_FILTER_HEADERS: Record<CampaignListKind, Readonly<Record<string, string>>> = {\n campaign: {\n name: 'Campaign',\n listIds: 'Lists',\n sitesLabel: 'Sites',\n createdAtMs: 'Created',\n },\n // A single send has no container; its `name` field filters by the subject.\n single: {\n name: 'Subject',\n sitesLabel: 'Sites',\n createdAtMs: 'Created',\n },\n}\nconst NO_CLAUSES: Record<CampaignListKind, ListFilterClause[]> = {\n campaign: [],\n single: [],\n}\n\n/**\n * THE CAMPAIGNS LIST.\n *\n * A campaign is a container — a name, a window, the lists it is aimed at, and\n * the emails sent inside it — so this is a table of campaigns and not of\n * messages. Composing is not here: a message belongs to a campaign, and the\n * campaign's own page is where one is written.\n *\n * The rows come from two collections. `emailCampaigns` holds the\n * containers. `campaigns` holds every send, including thousands written\n * before containers existed; those carry no container (`emailCampaignId`\n * null), and `campaignListRows` presents each as a campaign of one rather\n * than dropping it. The two are two queries, so the table lists one kind at a\n * time — Campaigns or Single sends — each paged by its own query (AGL-3321).\n * No send is re-filed, so no unsubscribe link and no pasted report URL stops\n * resolving.\n *\n * ## At two levels\n *\n * Both collections are the organization's. Under a site the card lists the\n * campaigns PLACED on that site and the sends sent AS it (see\n * `campaign-queries.ts` for why those filters are what make the read legal);\n * on the org hub, with `hostId` null, it lists every campaign with the sites\n * each one is placed on, and a new campaign is placed where its creator says.\n */\nexport function HostCampaignsCard(props: {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n /**\n * The marketing hub URL, when the caller already has it.\n *\n * Resolved from the host route when it does not: the inbox console page\n * embeds this card on a tab of its own, and its `basePath` names the INBOX\n * hub. Deriving one hub's URL from another's by string surgery is the kind\n * of link that breaks silently when a surface moves.\n */\n basePath?: string\n}) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const hubPath =\n basePath ??\n (orgSlug && subdomain\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug,\n host: subdomain,\n pluginSlug: 'marketing',\n })\n : null)\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n // A container is the org's, so deleting one from the org hub names the org.\n const manageApi = useCampaignManageApi(hostId, orgId)\n\n /*==========================================\n * TWO KINDS OF ROW, EACH ITS OWN QUERY (AGL-3321).\n *\n * A campaign is a CONTAINER (`emailCampaigns`); a single send is an email\n * filed under none (`campaigns` where `emailCampaignId == null`). The two\n * live in different collections, and one Firestore query reads one\n * collection — so a table that merged them could page neither, and every\n * filter would be matched over whatever window of each it happened to\n * hold. The toggle above the table picks the kind, and the table shows a\n * page of that kind's query: newest first on `createdAtMs`, every clause\n * and the search word on the query, paged by it. Same table, same\n * columns; a single send still reads as a campaign of one.\n *\n * Each kind keeps its own clauses (their Sites values differ: a campaign\n * is asked by scope token, a send by the site it was sent as); the search\n * words are the reader's and carry across.\n *=========================================*/\n const [kind, setKind] = useState<CampaignListKind>('campaign')\n const [clausesByKind, setClausesByKind] = useState(NO_CLAUSES)\n const [searchWords, setSearchWords] = useState<string[]>([])\n const setKindClauses = useCallback(\n (next: ListFilterClause[]) =>\n setClausesByKind((current) => ({ ...current, [kind]: next })),\n [kind],\n )\n const gridFilter = useListGridFilter({\n clauses: clausesByKind[kind],\n onChange: setKindClauses,\n selectFields: ['listIds', 'sitesLabel'],\n search: { words: searchWords, onChange: setSearchWords },\n })\n const containerDeclaration = campaignContainersListQuery(!hostId)\n const singleDeclaration = campaignSingleSendsListQuery(!hostId)\n const declaration = kind === 'campaign' ? containerDeclaration : singleDeclaration\n const filterHeaders = CAMPAIGN_FILTER_HEADERS[kind]\n /*\n * A site's campaigns are searched by the START of the name: a range beside\n * the site's scope clause, the one array clause the rules can prove for a\n * site collaborator (see `campaign-list-query.ts`). The org hub keeps the\n * word search.\n */\n const siteSearch =\n hostId && kind === 'campaign' ? campaignSiteSearchClause(gridFilter.searchWords) : null\n const containerPage = useListQuery<any>({\n collection:\n kind === 'campaign' ? campaignContainersCollection(firestore, orgId) : null,\n declaration: containerDeclaration,\n request: {\n clauses:\n kind === 'campaign'\n ? siteSearch\n ? [siteSearch, ...gridFilter.clauses]\n : gridFilter.clauses\n : [],\n search: hostId ? [] : gridFilter.searchWords,\n base: campaignContainersScope(hostId),\n },\n deps: [firestore, orgId, hostId, kind],\n idField: '$id',\n })\n const singlePage = useListQuery<any>({\n collection: kind === 'single' ? campaignSendsCollection(firestore, orgId) : null,\n declaration: singleDeclaration,\n request: {\n clauses: kind === 'single' ? gridFilter.clauses : [],\n search: gridFilter.searchWords,\n base: [SINGLE_SENDS_BASE, ...campaignSendsScope(hostId)],\n },\n deps: [firestore, orgId, hostId, kind],\n idField: '$id',\n })\n const page = kind === 'campaign' ? containerPage : singlePage\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n\n /*\n * THE FIGURES OF THE CAMPAIGNS ON THE PAGE ON SCREEN.\n *\n * A container stores no counters; its row sums the emails filed under it.\n * Those are read for the page's campaigns only — `emailCampaignId in` the\n * ids on screen, thirty to a read, and under a site the ones sent as it —\n * and never used to filter: the page is already the query's answer. Past\n * `ROLLUP_CEILING` emails a read stops, and the table says the figures\n * are partial.\n */\n const pageIds = useMemo(\n () =>\n kind === 'campaign'\n ? containerPage.rows.map((row: any) => String(row.$id))\n : [],\n [kind, containerPage.rows],\n )\n const rollupRead = (ids: string[]) => () => {\n const sends = campaignSendsCollection(firestore, orgId)\n if (!sends || !ids.length) return null\n return collectionCeiling(\n query(\n sends,\n where('emailCampaignId', 'in', ids),\n ...(hostId ? [where('hostId', '==', hostId)] : []),\n ),\n ROLLUP_CEILING,\n )\n }\n const firstIds = pageIds.slice(0, IN_LIMIT)\n const restIds = pageIds.slice(IN_LIMIT, IN_LIMIT * 2)\n const { data: firstSendDocs } = useFirestoreCollection<any>(\n rollupRead(firstIds),\n [firestore, orgId, hostId, firstIds.join(',')],\n { idField: '$id' },\n )\n const { data: restSendDocs } = useFirestoreCollection<any>(\n rollupRead(restIds),\n [firestore, orgId, hostId, restIds.join(',')],\n { idField: '$id' },\n )\n const firstSends = ceilingedWindow<any>(firstSendDocs, ROLLUP_CEILING)\n const restSends = ceilingedWindow<any>(restSendDocs, ROLLUP_CEILING)\n const rollupTruncated = firstSends.truncated || restSends.truncated\n\n // Org email lists, so a row can name what it is aimed at rather than\n // showing ids.\n const { data: listDocs } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The org's topic catalog, read only while the CREATE DRAWER is open.\n *\n * It fills one picker in that drawer and is drawn nowhere in the table, so\n * a reader who came to look at their campaigns must not pay for it — the\n * same rule the emails list already applies to the campaigns it offers its\n * own drawer, and the one the audiences section applies to a list's\n * members. It is the largest read on this section at 200 documents, and it\n * is the only one nothing on screen needs.\n */\n const { topics, source: topicSource } = useCampaignTopicOptions(\n topicCatalogHostId(hostId, orgMount),\n { enabled: createOpen },\n )\n const topicOptions = useMemo(\n () =>\n topics.map((topic) => ({\n value: topic.id,\n label: topic.name,\n })),\n [topics],\n )\n\n const listNames = useMemo(() => {\n const names = new Map<string, string>()\n for (const list of listDocs ?? []) {\n names.set(String(list.$id), String(list.name ?? list.$id))\n }\n return names\n }, [listDocs])\n\n const listOptions = useMemo(\n () =>\n (listDocs ?? []).map((list: any) => ({\n value: String(list.$id),\n label: String(list.name ?? list.$id),\n })),\n [listDocs],\n )\n\n /*\n * The page's rows, in the QUERY's order. `campaignListRows` builds each\n * row — a container with its emails summed, or a single send as a\n * campaign of one — and sorts by date, which is not this list's order, so\n * the page is put back in the order the query returned it.\n */\n const rows = useMemo(() => {\n const read = page.rows as any[]\n const order = new Map(read.map((row, at) => [String(row.$id), at]))\n const built =\n kind === 'campaign'\n ? campaignListRows(\n read as EmailCampaign[],\n [...firstSends.rows, ...restSends.rows] as CampaignSend[],\n Date.now(),\n )\n : campaignListRows([], read as CampaignSend[], Date.now())\n const byId = new Map(read.map((row) => [String(row.$id), row]))\n return built\n .filter((row) => order.has(row.id))\n .sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))\n .map((row) => ({\n ...row,\n $id: row.id,\n sitesLabel: orgMount\n ? campaignSitesLabel(orgMount, row, byId.get(row.id))\n : '',\n }))\n }, [kind, page.rows, firstSends.rows, restSends.rows, orgMount])\n\n /*\n * The choices the picked fields offer. A campaign's Sites are asked by\n * scope token — `org` is placed on every site — and a single send's by\n * the site it was sent as.\n */\n const filterOptions = useMemo(() => {\n if (!orgMount) return {}\n const sites = orgSiteOptions(orgMount)\n return kind === 'campaign'\n ? {\n listIds: listOptions,\n sitesLabel: [\n { value: 'org', label: 'Every site' },\n ...sites.map((site) => ({\n value: `host:${site.value}`,\n label: site.label,\n })),\n ],\n }\n : { sitesLabel: sites }\n }, [kind, listOptions, orgMount])\n\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n /** The campaign a delete is in flight for, so its row menu can say so. */\n const [deletingId, setDeletingId] = useState('')\n\n const openCampaign = useCallback(\n (id: string) => {\n if (hubPath) void router.push(`${hubPath}/campaigns/${id}`)\n },\n [hubPath, router],\n )\n\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n /*\n * `displayName` is the shared drawer's own field, and every artifact\n * create in the console collects a name under that key. The campaign\n * stores it as `name` — the container has one name and no second\n * internal label — so the mapping happens here rather than by giving\n * this one drawer a field the others do not have.\n */\n const name = String(values.displayName ?? '').trim()\n if (!name) return\n const startAtMs = values.startAt ? Date.parse(String(values.startAt)) : null\n const endAtMs = values.endAt ? Date.parse(String(values.endAt)) : null\n // Refused here rather than stored and rendered as a campaign that ends\n // before it starts, which no window state describes.\n if (startAtMs !== null && endAtMs !== null && endAtMs < startAtMs) {\n return setCreateError('The end date is before the start date')\n }\n /*\n * Held until the org is known. A container has one home, and a write\n * that raced the site's org lookup would have nowhere to go.\n */\n if (!orgId) {\n return setCreateError('This workspace is still loading — try again')\n }\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n /*\n * Where it is placed. From a site's hub, that site — the creator is\n * standing on it, and a campaign that appeared on every sibling site\n * would be a decision nobody made. From the org hub, the sites picked,\n * and every site when none are.\n */\n const visibleTo = campaignVisibleTo(\n hostId\n ? [hostId]\n : Array.isArray(values.siteIds)\n ? values.siteIds.map(String)\n : null,\n )\n try {\n await setDoc(campaignContainerDoc(firestore, orgId, id), {\n name,\n // What the list's search reads (AGL-3321), from the name.\n ...campaignContainerSearchFields(name),\n ...(Number.isFinite(startAtMs) && startAtMs !== null\n ? { startAtMs }\n : {}),\n ...(Number.isFinite(endAtMs) && endAtMs !== null ? { endAtMs } : {}),\n listIds: Array.isArray(values.listIds)\n ? values.listIds.map(String)\n : [],\n ...(values.topicId ? { topicId: String(values.topicId) } : {}),\n visibleTo,\n createdAtMs: Date.now(),\n createdBy: String((user as any)?.uid ?? ''),\n })\n setCreateOpen(false)\n openCampaign(id)\n } catch (error) {\n console.error(error)\n setCreateError('The campaign could not be created')\n enqueueSnackbar('Could not create the campaign', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setCreating(false)\n }\n },\n [creating, firestore, hostId, orgId, user, enqueueSnackbar, openCampaign],\n )\n\n const campaignHref = useCallback(\n (id: string) => (hubPath ? `${hubPath}/campaigns/${id}` : null),\n [hubPath],\n )\n\n /*==========================================\n * DELETING A CAMPAIGN FROM ITS ROW.\n *\n * The container goes and its emails stay — the route detaches them first,\n * so each one keeps its id, its report and its unsubscribe links, and\n * reappears in this very table as a \"Single send\". That is not a\n * convenience: a send id is inside the HMAC of every unsubscribe footer\n * already delivered, so a delete that took the sends with it would break\n * opt-outs that must go on working.\n *\n * The confirmation names the count and says what the delete does NOT do.\n * Deleting a campaign cancels nothing — a scheduled email inside it still\n * goes out — and a merchant who reads the button as \"stop this campaign\"\n * has to be told before they press it.\n *\n * EDITING is deliberately not offered here. A record is edited on its own\n * page in this console; the campaign's page carries the edit drawer, and\n * this row's first entry is the way to it.\n *=========================================*/\n const handleDelete = useCallback(\n async (row: CampaignListRow) => {\n if (deletingId) return\n const emails = row.sends.length\n /*\n * Emails with mail still to go: waiting for their time, AND part way\n * through an audience larger than one batch. Counting only `scheduled`\n * would miss the case that needs the warning most — an email delivering\n * right now is stored as `scheduled` too, and `rollup.scheduled` is the\n * narrower reading that leaves it out.\n */\n const unfinished = row.rollup.scheduled + row.rollup.sending\n const agreed = await confirm({\n title: `Delete ${row.name}?`,\n description:\n (emails\n ? `The ${emails.toLocaleString()} ` +\n `${emails === 1 ? 'email' : 'emails'} in it are kept — each ` +\n 'keeps its own report and its unsubscribe links, and they stay ' +\n 'in this list as single sends. '\n : 'It holds no emails. ') +\n (unfinished\n ? `${unfinished.toLocaleString()} of them ${\n unfinished === 1 ? 'is' : 'are'\n } still going out or still due, and deleting the campaign does ` +\n 'not stop that — the sender picks each one up from its own ' +\n 'record. Stop a send from its own page. '\n : '') +\n 'Any pages, forms and contacts assigned to it come out of it and ' +\n 'are otherwise untouched. Only the campaign itself goes.',\n confirmationText: 'Delete campaign',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeletingId(row.id)\n try {\n const { response, payload } = await manageApi({\n action: 'deleteCampaign',\n campaignId: row.id,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The campaign could not be deleted',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Campaign deleted', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The campaign could not be deleted', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeletingId('')\n }\n },\n [confirm, deletingId, enqueueSnackbar, manageApi],\n )\n\n const columns = useMemo(\n () => [\n {\n field: 'name',\n headerName: filterHeaders.name,\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: any) => (\n <Stack\n direction=\"row\"\n spacing={1}\n /*\n `minWidth: 0` on the row AND on the name: a flex child will not\n shrink below its content without it, so the chip beside it is\n what gets clipped instead of the name being ellipsed.\n */\n sx={{ alignItems: 'center', minWidth: 0, width: 1 }}\n >\n {/*\n The name is a real anchor as well as the row being clickable, so\n a campaign can be middle-clicked into a tab or copied out of the\n context menu — affordances a row handler cannot offer. The grid's\n own row click fires too, so the link stops the event rather than\n pushing the same route twice.\n */}\n {campaignHref(row.id) ? (\n <AppLink\n href={campaignHref(row.id) as string}\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n style={{ minWidth: 0 }}\n >\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n </AppLink>\n ) : (\n <Typography variant=\"body2\" noWrap sx={{ minWidth: 0 }}>\n {row.name}\n </Typography>\n )}\n {row.legacy ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Single send\"\n // A label that reads \"Single sen\" is worse than a shorter name.\n sx={{ flexShrink: 0 }}\n title={\n 'Sent before campaigns grouped their emails. Its report ' +\n 'and its unsubscribe links are unchanged.'\n }\n />\n ) : null}\n </Stack>\n ),\n },\n /*\n WHERE IT IS PLACED, on the org hub only. Under a site every row is on\n that site by construction, so the column would say one thing thirty\n times.\n */\n ...(orgMount\n ? [\n {\n field: 'sitesLabel',\n headerName: 'Sites',\n width: 170,\n sortable: false,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.sitesLabel}\n </Typography>\n ),\n },\n ]\n : []),\n {\n field: 'windowState',\n headerName: 'Window',\n width: 170,\n renderCell: ({ row }: any) => {\n const start = formatDay(row.startAtMs)\n const end = formatDay(row.endAtMs)\n const range =\n start && end && start !== end\n ? `${start} – ${end}`\n : start || end || ''\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {WINDOW_LABEL[row.windowState as CampaignListRow['windowState']]}\n </Typography>\n {range ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {range}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'listIds',\n headerName: 'Lists',\n width: 140,\n sortable: false,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.listIds.length\n ? row.listIds\n .map((id: string) => listNames.get(id) ?? id)\n .join(', ')\n : '—'}\n </Typography>\n ),\n },\n /*\n THE FOUR COUNTS, RIGHT-ALIGNED HEAD AND BODY.\n A figure is read by its last digit, so a column of them lines up on\n the right or it does not line up at all. `headerAlign` has to be said\n as well as `align`: a grid column defaults its header to the column\n type's alignment rather than to the cell's, which is how these came to\n sit with left headers over figures nobody could compare down the page.\n\n EVERY ONE OF THEM MEASURES THE MAIL, and the headings say so. What a\n campaign CAUSED — the submissions, leads, contacts and bookings\n credited to it — is not a column here, because there is no figure to\n read: a container has no conversion counter of its own, so a column\n would be an aggregation per row for a campaign and a subcollection\n document per row for a single send, on a list that costs two queries\n however many campaigns it holds. Headings that named the campaign over\n figures that all described its mail were the cheaper mistake, so the\n headings name the mail and the note under the table says where the\n outcome figures are.\n */\n {\n field: 'emails',\n headerName: 'Emails',\n width: 128,\n align: 'right',\n headerAlign: 'right',\n /*\n THE COUNT, AND WHAT IS STILL HAPPENING TO IT.\n\n `sending` is called out apart from `scheduled` because the two are\n opposite readings of the same stored status: an email delivering an\n audience larger than one batch sits as `scheduled` between runs, and\n rolling it in with the ones that have mailed nobody reports a\n campaign that is half delivered as a campaign that has not started.\n\n The note sits UNDER the figure rather than beside it — the column\n would otherwise have to be wide enough for the longest sentence, and\n the figure is what the column is read for.\n */\n renderCell: ({ row }: any) => {\n const { sends, sending, scheduled } = row.rollup\n const note = [\n sending ? `${sending} sending` : '',\n scheduled ? `${scheduled} scheduled` : '',\n ]\n .filter(Boolean)\n .join(', ')\n return (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">{String(sends)}</Typography>\n {note ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {note}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'sent',\n headerName: 'Mail sent',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.sent)}</Typography>\n ),\n },\n {\n field: 'opens',\n headerName: 'Mail opens',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.opens)}</Typography>\n ),\n },\n {\n field: 'clicks',\n headerName: 'Mail clicks',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.clicks)}</Typography>\n ),\n },\n /*\n THE TRAILING CLUSTER, through the grid's own actions column.\n `listActionsColumn` is what every other grid list in the console\n renders, so the reader cannot tell which family of table they are\n standing in — the hand-rolled tables on this surface reach the same\n `RowActionsMenu` from the other direction.\n\n Opening the campaign is restated here even though the row click does\n the same thing, for the reason the screens table restates its own: the\n menu is where a row's actions are NAMED, and an action absent from it\n reads as an action the row does not have.\n\n A \"Single send\" row is a SEND with no container — the table adopts it\n so a merchant's history is not missing — and there is no container\n under it to delete. Its entry is shown disabled with that reason\n rather than hidden, because a control that vanishes and one that does\n not apply look identical from the outside.\n */\n listActionsColumn(\n (row: any) => (\n <ListRowActions\n label={String(row.name ?? row.id)}\n items={[\n {\n key: 'details',\n label: 'Open campaign',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: campaignHref(row.id) ?? undefined,\n disabled: !campaignHref(row.id),\n disabledReason: campaignHref(row.id)\n ? undefined\n : 'This site’s console URL has not resolved yet',\n },\n {\n key: 'delete',\n label: 'Delete campaign',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: row.legacy || Boolean(deletingId),\n disabledReason: row.legacy\n ? 'This is a single send, not a campaign. Open it to ' +\n 'manage the email itself.'\n : 'A campaign is being deleted',\n onClick: () => void handleDelete(row as CampaignListRow),\n },\n ]}\n />\n ),\n { width: 72 },\n ),\n ],\n [listNames, campaignHref, deletingId, handleDelete, orgMount, filterHeaders],\n )\n\n /** Nothing of this kind stored at all — not a filter that matched nothing. */\n const none =\n page.status !== 'loading' && !rows.length && !filtering && page.page === 0\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#campaigns-group-emails',\n excerpt:\n 'A campaign groups the emails you send to a set of lists over a ' +\n 'window of dates. Open one to write and send an email inside it.',\n })}\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create campaign'}\n </Button>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={kind}\n onChange={(_event, next: CampaignListKind | null) => {\n if (next) setKind(next)\n }}\n aria-label=\"Which rows the table lists\"\n sx={{ alignSelf: 'flex-start' }}\n >\n <ToggleButton value=\"campaign\">{'Campaigns'}</ToggleButton>\n <ToggleButton value=\"single\">{'Single sends'}</ToggleButton>\n </ToggleButtonGroup>\n <ListFilterChips\n fields={declaration.fields}\n headers={filterHeaders}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(page.plan.refused, {\n fields: declaration.fields,\n headers: filterHeaders,\n options: filterOptions,\n })}\n notices={\n siteSearch\n ? [CAMPAIGN_SITE_SEARCH_NOTICE, ...page.plan.notices]\n : page.plan.notices\n }\n />\n <ListTable\n rows={rows}\n columns={listFilterGridColumns(\n columns as any,\n declaration.fields,\n filterOptions,\n filterHeaders,\n )}\n onOpen={(id) => openCampaign(id)}\n loading={page.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the list's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n hideFooter\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n declaration.fields,\n (columns as Array<{ field: string }>).map((column) => column.field),\n ),\n },\n }}\n noRowsLabel={\n none\n ? kind === 'campaign'\n ? 'No campaigns yet'\n : 'No single sends'\n : kind === 'campaign'\n ? 'No campaigns match these filters'\n : 'No single sends match these filters'\n }\n noRowsDescription={\n none\n ? kind === 'campaign'\n ? 'A campaign groups the emails you send to a set of lists.'\n : 'An email filed under no campaign is listed here.'\n : undefined\n }\n noRowsAction={\n none && kind === 'campaign' ? (\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create campaign'}\n </Button>\n ) : undefined\n }\n />\n <ListPagination\n page={page.page}\n pageSize={page.pageSize}\n rowCount={rows.length}\n hasMore={page.hasMore}\n onPageChange={page.setPage}\n onPageSizeChange={page.setPageSize}\n />\n {/*\n WHAT THIS TABLE MEASURES, said rather than inferred from the column\n names. Every figure in it is a fact about the campaign's mail, and a\n reader comparing rows is comparing mailings — the conversions and the\n destinations credited to a campaign are on the campaign's own page,\n because counting them for every row is a read per row on a list whose\n whole cost is two queries.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These figures measure each campaign’s mail. What a campaign ' +\n 'caused — the form submissions, leads, contacts and bookings ' +\n 'credited to it — and the pages it sent people to are on the ' +\n 'campaign’s own page.'}\n </Typography>\n {rollupTruncated ? (\n <Alert severity=\"info\">\n {`The campaigns on this page hold more than ${ROLLUP_CEILING} ` +\n 'emails between them, so their figures count the first ' +\n `${ROLLUP_CEILING} read.`}\n </Alert>\n ) : null}\n </Stack>\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. What a campaign collects beyond a name\n is its window, the lists it is aimed at and the stream its emails open\n on; the description box is left off because the container stores none,\n and a field the writer discards is worse than one never offered.\n */}\n {/* Draws nothing: the zone the topic catalog's owner answers through. */}\n {topicSource}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create campaign\"\n submitLabel=\"Create campaign\"\n includeDescription={false}\n onSubmit={handleCreate}\n extraFields={campaignFields(\n listOptions,\n topicOptions,\n orgMount ? orgSiteOptions(orgMount) : null,\n )}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n )\n}\n/**\n * What a campaign collects beyond its name.\n *\n * Data-driven-forms field descriptors, built from the org's own lists and\n * topics so the drawer offers what this workspace actually has. Dates are\n * plain `date` inputs: a campaign window is a day at each end, and a time of\n * day would be a precision the model does not carry.\n */\nfunction campaignFields(\n lists: Array<{ value: string; label: string }>,\n topics: Array<{ value: string; label: string }>,\n /** The org's sites, on the org hub only — a site hub places on itself. */\n sites: Array<{ value: string; label: string }> | null,\n): any[] {\n return [\n ...(sites\n ? [\n {\n component: 'select',\n name: 'siteIds',\n label: 'Sites',\n multiple: true,\n initialValue: [],\n helperText:\n 'The sites this campaign is offered on. Leave empty for every site.',\n disableDefaultOption: true,\n options: sites,\n },\n ]\n : []),\n {\n component: 'text-field',\n name: 'startAt',\n label: 'Starts',\n type: 'date',\n helperText: 'When the campaign window opens',\n },\n {\n component: 'text-field',\n name: 'endAt',\n label: 'Ends',\n type: 'date',\n helperText: 'Leave empty for an open-ended campaign',\n InputLabelProps: { shrink: true },\n },\n {\n component: 'select',\n name: 'listIds',\n label: 'Lists',\n multiple: true,\n initialValue: [],\n // A campaign with no list is legitimate: its emails can go to leads, to\n // site members, or to a segment. The lists are what it is AIMED at.\n helperText: 'The lists this campaign is aimed at',\n disableDefaultOption: true,\n options: lists,\n },\n {\n component: 'select',\n name: 'topicId',\n label: 'Topic',\n helperText: 'The stream its emails open on — each one can change it',\n disableDefaultOption: true,\n options: topics,\n },\n ]\n}\n\n/**\n * The Sites cell: where a campaign is offered.\n *\n * A container reads its `visibleTo`; a single send, which has no container,\n * is on the one site it was sent as. An empty placement is said rather than\n * left blank, because a blank cell reads as \"every site\" and it means none.\n */\nfunction campaignSitesLabel(\n mount: MarketingOrgMount,\n row: CampaignListRow,\n container: EmailCampaign | undefined,\n): string {\n if (row.legacy) return orgSiteName(mount, row.sends[0]?.hostId) || '—'\n const ids = campaignSiteIds(container)\n if (ids === null) return 'Every site'\n if (!ids.length) return 'No site'\n return ids.map((id) => orgSiteName(mount, id)).join(', ')\n}\n\nHostCampaignsCard.displayName = 'HostCampaignsCard'\n\nexport default HostCampaignsCard\n"],"names":["buildRoute","createResourceUid","pluginDocsHelp","Route","ceilingedWindow","collectionCeiling","useListQuery","mdiDeleteOutline","mdiEyeOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Alert","Button","Chip","Stack","ToggleButton","ToggleButtonGroup","Typography","collection","limit","query","setDoc","where","useCallback","useMemo","useState","useRouter","useConsoleHostRoute","useFirestore","useFirestoreCollection","useUser","campaignListRows","campaignSiteIds","campaignVisibleTo","CreateArtifactDrawer","CAMPAIGN_SITE_SEARCH_NOTICE","SINGLE_SENDS_BASE","campaignContainerSearchFields","campaignContainersListQuery","campaignContainersScope","campaignSendsScope","campaignSingleSendsListQuery","campaignSiteSearchClause","campaignContainerDoc","campaignContainersCollection","campaignSendsCollection","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","useCampaignTopicOptions","ROLLUP_CEILING","IN_LIMIT","formatDay","ms","Date","toLocaleDateString","figure","value","toLocaleString","WINDOW_LABEL","undated","upcoming","running","ended","CAMPAIGN_FILTER_HEADERS","campaign","name","listIds","sitesLabel","createdAtMs","single","NO_CLAUSES","HostCampaignsCard","props","hostId","basePath","firestore","router","orgSlug","subdomain","hubPath","HOST_PLUGIN","host","pluginSlug","data","user","enqueueSnackbar","confirm","orgMount","orgId","manageApi","kind","setKind","clausesByKind","setClausesByKind","searchWords","setSearchWords","setKindClauses","next","current","gridFilter","clauses","onChange","selectFields","search","words","containerDeclaration","singleDeclaration","declaration","filterHeaders","siteSearch","containerPage","request","base","deps","idField","singlePage","page","filtering","length","some","word","trim","pageIds","rows","map","row","String","$id","rollupRead","ids","sends","firstIds","slice","restIds","firstSendDocs","join","restSendDocs","firstSends","restSends","rollupTruncated","truncated","listDocs","createOpen","setCreateOpen","topics","source","topicSource","enabled","topicOptions","topic","id","label","listNames","names","Map","list","set","listOptions","read","order","at","built","now","byId","filter","has","sort","a","b","get","campaignSitesLabel","filterOptions","sites","site","createError","setCreateError","creating","setCreating","deletingId","setDeletingId","openCampaign","push","handleCreate","values","displayName","startAtMs","startAt","parse","endAtMs","endAt","visibleTo","Array","isArray","siteIds","Number","isFinite","topicId","createdBy","uid","error","console","variant","allowDuplicate","campaignHref","handleDelete","emails","unfinished","rollup","scheduled","sending","agreed","title","description","confirmationText","then","catch","response","payload","action","campaignId","ok","persist","columns","field","headerName","flex","minWidth","renderCell","direction","spacing","sx","alignItems","width","href","onClick","event","stopPropagation","style","noWrap","legacy","size","flexShrink","sortable","start","end","range","windowState","color","align","headerAlign","note","Boolean","sent","opens","clicks","items","key","icon","path","undefined","disabled","disabledReason","destructive","none","status","header","help","anchor","excerpt","HeaderProps","contentGutterX","contentGutterY","exclusive","_event","aria-label","alignSelf","fields","headers","setClauses","options","refused","plan","notices","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","hideFooter","initialState","columnVisibilityModel","column","noRowsLabel","noRowsDescription","noRowsAction","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","severity","open","onClose","submitLabel","includeDescription","onSubmit","extraFields","campaignFields","errorSlot","mt","mb","lists","component","multiple","initialValue","helperText","disableDefaultOption","type","InputLabelProps","shrink","mount","container"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaigns-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, createResourceUid, pluginDocsHelp, Route } from '@aglyn/aglyn'\n/*\n * The MODULE, not the barrel, for the two PURE helpers — a spec that mocks\n * `@aglyn/tenant-feature-instance` wholesale to stage its Firestore hooks\n * would otherwise lose them, and neither is a hook.\n */\nimport {\n ceilingedWindow,\n collectionCeiling,\n} from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { mdiDeleteOutline, mdiEyeOutline } 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 { hiddenFilterVisibility } 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 { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { collection, limit, query, setDoc, where } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n campaignListRows,\n campaignSiteIds,\n campaignVisibleTo,\n type CampaignAggregate,\n type CampaignListRow,\n type CampaignSend,\n type EmailCampaign,\n} from '../model/campaign-container'\nimport { CreateArtifactDrawer } from '@aglyn/shared-ui-jsx-forms'\nimport {\n CAMPAIGN_SITE_SEARCH_NOTICE,\n SINGLE_SENDS_BASE,\n campaignContainerSearchFields,\n campaignContainersListQuery,\n campaignContainersScope,\n campaignSendsScope,\n campaignSingleSendsListQuery,\n campaignSiteSearchClause,\n} from '../model/campaign-list-query'\nimport {\n campaignContainerDoc,\n campaignContainersCollection,\n campaignSendsCollection,\n} from './campaign-queries'\nimport {\n orgSiteName,\n orgSiteOptions,\n topicCatalogHostId,\n useMarketingOrgId,\n useMarketingOrgMount,\n type MarketingOrgMount,\n} from './marketing-org-mount'\nimport { useCampaignManageApi } from './use-campaign-send-api'\nimport { HostCampaignsZone } from './campaign-list-zones'\nimport { useCampaignTopicOptions } from './use-campaign-topic-options'\n\n/**\n * How many emails one read of a page's campaigns covers, for their figures.\n *\n * A ceiling on the ROLLUP read, not on the list: the list is paged by its\n * own query, and this reads the emails of the campaigns on the page on\n * screen, thirty campaigns to a read (`in` takes thirty ids), so the largest\n * page is two reads of this many. Past it, the figures say so.\n */\nconst ROLLUP_CEILING = 50\n\n/** Firestore's cap on an `in` clause's values. */\nconst IN_LIMIT = 30\n\n/** Which kind of row the table shows: the two are separate queries. */\ntype CampaignListKind = 'campaign' | 'single'\n\nconst formatDay = (ms: number | null): string =>\n ms === null ? '' : new Date(ms).toLocaleDateString()\n\n/** A rolled-up figure, or an em dash where nothing recorded one. */\nconst figure = (value: CampaignAggregate): string =>\n value.value === null ? '—' : value.value.toLocaleString()\n\nconst WINDOW_LABEL: Record<CampaignListRow['windowState'], string> = {\n undated: 'No dates',\n upcoming: 'Upcoming',\n running: 'Running',\n ended: 'Ended',\n}\n\n/*\n * What the campaigns grid's Filters panel offers, each clause and the search\n * on the list's Firestore query (AGL-3321, `campaignContainersListQuery` and\n * `campaignSingleSendsListQuery`). The Window column is derived from a\n * campaign's dates against the clock, which no query can ask, so it is not\n * offered; the one date range is Created, the list's own order.\n */\nconst CAMPAIGN_FILTER_HEADERS: Record<CampaignListKind, Readonly<Record<string, string>>> = {\n campaign: {\n name: 'Campaign',\n listIds: 'Lists',\n sitesLabel: 'Sites',\n createdAtMs: 'Created',\n },\n // A single send has no container; its `name` field filters by the subject.\n single: {\n name: 'Subject',\n sitesLabel: 'Sites',\n createdAtMs: 'Created',\n },\n}\nconst NO_CLAUSES: Record<CampaignListKind, ListFilterClause[]> = {\n campaign: [],\n single: [],\n}\n\n/**\n * THE CAMPAIGNS LIST.\n *\n * A campaign is a container — a name, a window, the lists it is aimed at, and\n * the emails sent inside it — so this is a table of campaigns and not of\n * messages. Composing is not here: a message belongs to a campaign, and the\n * campaign's own page is where one is written.\n *\n * The rows come from two collections. `emailCampaigns` holds the\n * containers. `campaigns` holds every send, including thousands written\n * before containers existed; those carry no container (`emailCampaignId`\n * null), and `campaignListRows` presents each as a campaign of one rather\n * than dropping it. The two are two queries, so the table lists one kind at a\n * time — Campaigns or Single sends — each paged by its own query (AGL-3321).\n * No send is re-filed, so no unsubscribe link and no pasted report URL stops\n * resolving.\n *\n * ## At two levels\n *\n * Both collections are the organization's. Under a site the card lists the\n * campaigns PLACED on that site and the sends sent AS it (see\n * `campaign-queries.ts` for why those filters are what make the read legal);\n * on the org hub, with `hostId` null, it lists every campaign with the sites\n * each one is placed on, and a new campaign is placed where its creator says.\n */\nexport function HostCampaignsCard(props: {\n /** The site, or `null` on the org Marketing hub. */\n hostId: string | null\n /**\n * The marketing hub URL, when the caller already has it.\n *\n * Resolved from the host route when it does not: the inbox console page\n * embeds this card on a tab of its own, and its `basePath` names the INBOX\n * hub. Deriving one hub's URL from another's by string surgery is the kind\n * of link that breaks silently when a surface moves.\n */\n basePath?: string\n}) {\n const { hostId, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { orgSlug, subdomain } = useConsoleHostRoute(hostId)\n const hubPath =\n basePath ??\n (orgSlug && subdomain\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug,\n host: subdomain,\n pluginSlug: 'marketing',\n })\n : null)\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const orgMount = useMarketingOrgMount()\n const { orgId } = useMarketingOrgId(hostId)\n // A container is the org's, so deleting one from the org hub names the org.\n const manageApi = useCampaignManageApi(hostId, orgId)\n\n /*==========================================\n * TWO KINDS OF ROW, EACH ITS OWN QUERY (AGL-3321).\n *\n * A campaign is a CONTAINER (`emailCampaigns`); a single send is an email\n * filed under none (`campaigns` where `emailCampaignId == null`). The two\n * live in different collections, and one Firestore query reads one\n * collection — so a table that merged them could page neither, and every\n * filter would be matched over whatever window of each it happened to\n * hold. The toggle above the table picks the kind, and the table shows a\n * page of that kind's query: newest first on `createdAtMs`, every clause\n * and the search word on the query, paged by it. Same table, same\n * columns; a single send still reads as a campaign of one.\n *\n * Each kind keeps its own clauses (their Sites values differ: a campaign\n * is asked by scope token, a send by the site it was sent as); the search\n * words are the reader's and carry across.\n *=========================================*/\n const [kind, setKind] = useState<CampaignListKind>('campaign')\n const [clausesByKind, setClausesByKind] = useState(NO_CLAUSES)\n const [searchWords, setSearchWords] = useState<string[]>([])\n const setKindClauses = useCallback(\n (next: ListFilterClause[]) =>\n setClausesByKind((current) => ({ ...current, [kind]: next })),\n [kind],\n )\n const gridFilter = useListGridFilter({\n clauses: clausesByKind[kind],\n onChange: setKindClauses,\n selectFields: ['listIds', 'sitesLabel'],\n search: { words: searchWords, onChange: setSearchWords },\n })\n const containerDeclaration = campaignContainersListQuery(!hostId)\n const singleDeclaration = campaignSingleSendsListQuery(!hostId)\n const declaration = kind === 'campaign' ? containerDeclaration : singleDeclaration\n const filterHeaders = CAMPAIGN_FILTER_HEADERS[kind]\n /*\n * A site's campaigns are searched by the START of the name: a range beside\n * the site's scope clause, the one array clause the rules can prove for a\n * site collaborator (see `campaign-list-query.ts`). The org hub keeps the\n * word search.\n */\n const siteSearch =\n hostId && kind === 'campaign' ? campaignSiteSearchClause(gridFilter.searchWords) : null\n const containerPage = useListQuery<any>({\n collection:\n kind === 'campaign' ? campaignContainersCollection(firestore, orgId) : null,\n declaration: containerDeclaration,\n request: {\n clauses:\n kind === 'campaign'\n ? siteSearch\n ? [siteSearch, ...gridFilter.clauses]\n : gridFilter.clauses\n : [],\n search: hostId ? [] : gridFilter.searchWords,\n base: campaignContainersScope(hostId),\n },\n deps: [firestore, orgId, hostId, kind],\n idField: '$id',\n })\n const singlePage = useListQuery<any>({\n collection: kind === 'single' ? campaignSendsCollection(firestore, orgId) : null,\n declaration: singleDeclaration,\n request: {\n clauses: kind === 'single' ? gridFilter.clauses : [],\n search: gridFilter.searchWords,\n base: [SINGLE_SENDS_BASE, ...campaignSendsScope(hostId)],\n },\n deps: [firestore, orgId, hostId, kind],\n idField: '$id',\n })\n const page = kind === 'campaign' ? containerPage : singlePage\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n\n /*\n * THE FIGURES OF THE CAMPAIGNS ON THE PAGE ON SCREEN.\n *\n * A container stores no counters; its row sums the emails filed under it.\n * Those are read for the page's campaigns only — `emailCampaignId in` the\n * ids on screen, thirty to a read, and under a site the ones sent as it —\n * and never used to filter: the page is already the query's answer. Past\n * `ROLLUP_CEILING` emails a read stops, and the table says the figures\n * are partial.\n */\n const pageIds = useMemo(\n () =>\n kind === 'campaign'\n ? containerPage.rows.map((row: any) => String(row.$id))\n : [],\n [kind, containerPage.rows],\n )\n const rollupRead = (ids: string[]) => () => {\n const sends = campaignSendsCollection(firestore, orgId)\n if (!sends || !ids.length) return null\n return collectionCeiling(\n query(\n sends,\n where('emailCampaignId', 'in', ids),\n ...(hostId ? [where('hostId', '==', hostId)] : []),\n ),\n ROLLUP_CEILING,\n )\n }\n const firstIds = pageIds.slice(0, IN_LIMIT)\n const restIds = pageIds.slice(IN_LIMIT, IN_LIMIT * 2)\n const { data: firstSendDocs } = useFirestoreCollection<any>(\n rollupRead(firstIds),\n [firestore, orgId, hostId, firstIds.join(',')],\n { idField: '$id' },\n )\n const { data: restSendDocs } = useFirestoreCollection<any>(\n rollupRead(restIds),\n [firestore, orgId, hostId, restIds.join(',')],\n { idField: '$id' },\n )\n const firstSends = ceilingedWindow<any>(firstSendDocs, ROLLUP_CEILING)\n const restSends = ceilingedWindow<any>(restSendDocs, ROLLUP_CEILING)\n const rollupTruncated = firstSends.truncated || restSends.truncated\n\n // Org email lists, so a row can name what it is aimed at rather than\n // showing ids.\n const { data: listDocs } = useFirestoreCollection<any>(\n () =>\n orgId\n ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50))\n : null,\n [firestore, orgId],\n { idField: '$id' },\n )\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The org's topic catalog, read only while the CREATE DRAWER is open.\n *\n * It fills one picker in that drawer and is drawn nowhere in the table, so\n * a reader who came to look at their campaigns must not pay for it — the\n * same rule the emails list already applies to the campaigns it offers its\n * own drawer, and the one the audiences section applies to a list's\n * members. It is the largest read on this section at 200 documents, and it\n * is the only one nothing on screen needs.\n */\n const { topics, source: topicSource } = useCampaignTopicOptions(\n topicCatalogHostId(hostId, orgMount),\n { enabled: createOpen },\n )\n const topicOptions = useMemo(\n () =>\n topics.map((topic) => ({\n value: topic.id,\n label: topic.name,\n })),\n [topics],\n )\n\n const listNames = useMemo(() => {\n const names = new Map<string, string>()\n for (const list of listDocs ?? []) {\n names.set(String(list.$id), String(list.name ?? list.$id))\n }\n return names\n }, [listDocs])\n\n const listOptions = useMemo(\n () =>\n (listDocs ?? []).map((list: any) => ({\n value: String(list.$id),\n label: String(list.name ?? list.$id),\n })),\n [listDocs],\n )\n\n /*\n * The page's rows, in the QUERY's order. `campaignListRows` builds each\n * row — a container with its emails summed, or a single send as a\n * campaign of one — and sorts by date, which is not this list's order, so\n * the page is put back in the order the query returned it.\n */\n const rows = useMemo(() => {\n const read = page.rows as any[]\n const order = new Map(read.map((row, at) => [String(row.$id), at]))\n const built =\n kind === 'campaign'\n ? campaignListRows(\n read as EmailCampaign[],\n [...firstSends.rows, ...restSends.rows] as CampaignSend[],\n Date.now(),\n )\n : campaignListRows([], read as CampaignSend[], Date.now())\n const byId = new Map(read.map((row) => [String(row.$id), row]))\n return built\n .filter((row) => order.has(row.id))\n .sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))\n .map((row) => ({\n ...row,\n $id: row.id,\n sitesLabel: orgMount\n ? campaignSitesLabel(orgMount, row, byId.get(row.id))\n : '',\n }))\n }, [kind, page.rows, firstSends.rows, restSends.rows, orgMount])\n\n /*\n * The choices the picked fields offer. A campaign's Sites are asked by\n * scope token — `org` is placed on every site — and a single send's by\n * the site it was sent as.\n */\n const filterOptions = useMemo(() => {\n if (!orgMount) return {}\n const sites = orgSiteOptions(orgMount)\n return kind === 'campaign'\n ? {\n listIds: listOptions,\n sitesLabel: [\n { value: 'org', label: 'Every site' },\n ...sites.map((site) => ({\n value: `host:${site.value}`,\n label: site.label,\n })),\n ],\n }\n : { sitesLabel: sites }\n }, [kind, listOptions, orgMount])\n\n const [createError, setCreateError] = useState<string | null>(null)\n const [creating, setCreating] = useState(false)\n /** The campaign a delete is in flight for, so its row menu can say so. */\n const [deletingId, setDeletingId] = useState('')\n\n const openCampaign = useCallback(\n (id: string) => {\n if (hubPath) void router.push(`${hubPath}/campaigns/${id}`)\n },\n [hubPath, router],\n )\n\n const handleCreate = useCallback(\n async (values: Record<string, any>) => {\n if (creating) return\n /*\n * `displayName` is the shared drawer's own field, and every artifact\n * create in the console collects a name under that key. The campaign\n * stores it as `name` — the container has one name and no second\n * internal label — so the mapping happens here rather than by giving\n * this one drawer a field the others do not have.\n */\n const name = String(values.displayName ?? '').trim()\n if (!name) return\n const startAtMs = values.startAt ? Date.parse(String(values.startAt)) : null\n const endAtMs = values.endAt ? Date.parse(String(values.endAt)) : null\n // Refused here rather than stored and rendered as a campaign that ends\n // before it starts, which no window state describes.\n if (startAtMs !== null && endAtMs !== null && endAtMs < startAtMs) {\n return setCreateError('The end date is before the start date')\n }\n /*\n * Held until the org is known. A container has one home, and a write\n * that raced the site's org lookup would have nowhere to go.\n */\n if (!orgId) {\n return setCreateError('This workspace is still loading — try again')\n }\n setCreating(true)\n setCreateError(null)\n const id = createResourceUid()\n /*\n * Where it is placed. From a site's hub, that site — the creator is\n * standing on it, and a campaign that appeared on every sibling site\n * would be a decision nobody made. From the org hub, the sites picked,\n * and every site when none are.\n */\n const visibleTo = campaignVisibleTo(\n hostId\n ? [hostId]\n : Array.isArray(values.siteIds)\n ? values.siteIds.map(String)\n : null,\n )\n try {\n await setDoc(campaignContainerDoc(firestore, orgId, id), {\n name,\n // What the list's search reads (AGL-3321), from the name.\n ...campaignContainerSearchFields(name),\n ...(Number.isFinite(startAtMs) && startAtMs !== null\n ? { startAtMs }\n : {}),\n ...(Number.isFinite(endAtMs) && endAtMs !== null ? { endAtMs } : {}),\n listIds: Array.isArray(values.listIds)\n ? values.listIds.map(String)\n : [],\n ...(values.topicId ? { topicId: String(values.topicId) } : {}),\n visibleTo,\n createdAtMs: Date.now(),\n createdBy: String((user as any)?.uid ?? ''),\n })\n setCreateOpen(false)\n openCampaign(id)\n } catch (error) {\n console.error(error)\n setCreateError('The campaign could not be created')\n enqueueSnackbar('Could not create the campaign', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setCreating(false)\n }\n },\n [creating, firestore, hostId, orgId, user, enqueueSnackbar, openCampaign],\n )\n\n const campaignHref = useCallback(\n (id: string) => (hubPath ? `${hubPath}/campaigns/${id}` : null),\n [hubPath],\n )\n\n /*==========================================\n * DELETING A CAMPAIGN FROM ITS ROW.\n *\n * The container goes and its emails stay — the route detaches them first,\n * so each one keeps its id, its report and its unsubscribe links, and\n * reappears in this very table as a \"Single send\". That is not a\n * convenience: a send id is inside the HMAC of every unsubscribe footer\n * already delivered, so a delete that took the sends with it would break\n * opt-outs that must go on working.\n *\n * The confirmation names the count and says what the delete does NOT do.\n * Deleting a campaign cancels nothing — a scheduled email inside it still\n * goes out — and a merchant who reads the button as \"stop this campaign\"\n * has to be told before they press it.\n *\n * EDITING is deliberately not offered here. A record is edited on its own\n * page in this console; the campaign's page carries the edit drawer, and\n * this row's first entry is the way to it.\n *=========================================*/\n const handleDelete = useCallback(\n async (row: CampaignListRow) => {\n if (deletingId) return\n const emails = row.sends.length\n /*\n * Emails with mail still to go: waiting for their time, AND part way\n * through an audience larger than one batch. Counting only `scheduled`\n * would miss the case that needs the warning most — an email delivering\n * right now is stored as `scheduled` too, and `rollup.scheduled` is the\n * narrower reading that leaves it out.\n */\n const unfinished = row.rollup.scheduled + row.rollup.sending\n const agreed = await confirm({\n title: `Delete ${row.name}?`,\n description:\n (emails\n ? `The ${emails.toLocaleString()} ` +\n `${emails === 1 ? 'email' : 'emails'} in it are kept — each ` +\n 'keeps its own report and its unsubscribe links, and they stay ' +\n 'in this list as single sends. '\n : 'It holds no emails. ') +\n (unfinished\n ? `${unfinished.toLocaleString()} of them ${\n unfinished === 1 ? 'is' : 'are'\n } still going out or still due, and deleting the campaign does ` +\n 'not stop that — the sender picks each one up from its own ' +\n 'record. Stop a send from its own page. '\n : '') +\n 'Any pages, forms and contacts assigned to it come out of it and ' +\n 'are otherwise untouched. Only the campaign itself goes.',\n confirmationText: 'Delete campaign',\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeletingId(row.id)\n try {\n const { response, payload } = await manageApi({\n action: 'deleteCampaign',\n campaignId: row.id,\n })\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The campaign could not be deleted',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n enqueueSnackbar('Campaign deleted', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The campaign could not be deleted', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeletingId('')\n }\n },\n [confirm, deletingId, enqueueSnackbar, manageApi],\n )\n\n const columns = useMemo(\n () => [\n {\n field: 'name',\n headerName: filterHeaders.name,\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: any) => (\n <Stack\n direction=\"row\"\n spacing={1}\n /*\n `minWidth: 0` on the row AND on the name: a flex child will not\n shrink below its content without it, so the chip beside it is\n what gets clipped instead of the name being ellipsed.\n */\n sx={{ alignItems: 'center', minWidth: 0, width: 1 }}\n >\n {/*\n The name is a real anchor as well as the row being clickable, so\n a campaign can be middle-clicked into a tab or copied out of the\n context menu — affordances a row handler cannot offer. The grid's\n own row click fires too, so the link stops the event rather than\n pushing the same route twice.\n */}\n {campaignHref(row.id) ? (\n <AppLink\n href={campaignHref(row.id) as string}\n onClick={(event: { stopPropagation: () => void }) =>\n event.stopPropagation()\n }\n style={{ minWidth: 0 }}\n >\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n </AppLink>\n ) : (\n <Typography variant=\"body2\" noWrap sx={{ minWidth: 0 }}>\n {row.name}\n </Typography>\n )}\n {row.legacy ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Single send\"\n // A label that reads \"Single sen\" is worse than a shorter name.\n sx={{ flexShrink: 0 }}\n title={\n 'Sent before campaigns grouped their emails. Its report ' +\n 'and its unsubscribe links are unchanged.'\n }\n />\n ) : null}\n </Stack>\n ),\n },\n /*\n WHERE IT IS PLACED, on the org hub only. Under a site every row is on\n that site by construction, so the column would say one thing thirty\n times.\n */\n ...(orgMount\n ? [\n {\n field: 'sitesLabel',\n headerName: 'Sites',\n width: 170,\n sortable: false,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.sitesLabel}\n </Typography>\n ),\n },\n ]\n : []),\n {\n field: 'windowState',\n headerName: 'Window',\n width: 170,\n renderCell: ({ row }: any) => {\n const start = formatDay(row.startAtMs)\n const end = formatDay(row.endAtMs)\n const range =\n start && end && start !== end\n ? `${start} – ${end}`\n : start || end || ''\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {WINDOW_LABEL[row.windowState as CampaignListRow['windowState']]}\n </Typography>\n {range ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {range}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'listIds',\n headerName: 'Lists',\n width: 140,\n sortable: false,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.listIds.length\n ? row.listIds\n .map((id: string) => listNames.get(id) ?? id)\n .join(', ')\n : '—'}\n </Typography>\n ),\n },\n /*\n THE FOUR COUNTS, RIGHT-ALIGNED HEAD AND BODY.\n A figure is read by its last digit, so a column of them lines up on\n the right or it does not line up at all. `headerAlign` has to be said\n as well as `align`: a grid column defaults its header to the column\n type's alignment rather than to the cell's, which is how these came to\n sit with left headers over figures nobody could compare down the page.\n\n EVERY ONE OF THEM MEASURES THE MAIL, and the headings say so. What a\n campaign CAUSED — the submissions, leads, contacts and bookings\n credited to it — is not a column here, because there is no figure to\n read: a container has no conversion counter of its own, so a column\n would be an aggregation per row for a campaign and a subcollection\n document per row for a single send, on a list that costs two queries\n however many campaigns it holds. Headings that named the campaign over\n figures that all described its mail were the cheaper mistake, so the\n headings name the mail and the note under the table says where the\n outcome figures are.\n */\n {\n field: 'emails',\n headerName: 'Emails',\n width: 128,\n align: 'right',\n headerAlign: 'right',\n /*\n THE COUNT, AND WHAT IS STILL HAPPENING TO IT.\n\n `sending` is called out apart from `scheduled` because the two are\n opposite readings of the same stored status: an email delivering an\n audience larger than one batch sits as `scheduled` between runs, and\n rolling it in with the ones that have mailed nobody reports a\n campaign that is half delivered as a campaign that has not started.\n\n The note sits UNDER the figure rather than beside it — the column\n would otherwise have to be wide enough for the longest sentence, and\n the figure is what the column is read for.\n */\n renderCell: ({ row }: any) => {\n const { sends, sending, scheduled } = row.rollup\n const note = [\n sending ? `${sending} sending` : '',\n scheduled ? `${scheduled} scheduled` : '',\n ]\n .filter(Boolean)\n .join(', ')\n return (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">{String(sends)}</Typography>\n {note ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {note}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'sent',\n headerName: 'Mail sent',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.sent)}</Typography>\n ),\n },\n {\n field: 'opens',\n headerName: 'Mail opens',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.opens)}</Typography>\n ),\n },\n {\n field: 'clicks',\n headerName: 'Mail clicks',\n width: 112,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">{figure(row.rollup.clicks)}</Typography>\n ),\n },\n /*\n THE TRAILING CLUSTER, through the grid's own actions column.\n `listActionsColumn` is what every other grid list in the console\n renders, so the reader cannot tell which family of table they are\n standing in — the hand-rolled tables on this surface reach the same\n `RowActionsMenu` from the other direction.\n\n Opening the campaign is restated here even though the row click does\n the same thing, for the reason the screens table restates its own: the\n menu is where a row's actions are NAMED, and an action absent from it\n reads as an action the row does not have.\n\n A \"Single send\" row is a SEND with no container — the table adopts it\n so a merchant's history is not missing — and there is no container\n under it to delete. Its entry is shown disabled with that reason\n rather than hidden, because a control that vanishes and one that does\n not apply look identical from the outside.\n */\n listActionsColumn(\n (row: any) => (\n <ListRowActions\n label={String(row.name ?? row.id)}\n items={[\n {\n key: 'details',\n label: 'Open campaign',\n icon: <MdiIcon path={mdiEyeOutline.path} size={0.8} />,\n href: campaignHref(row.id) ?? undefined,\n disabled: !campaignHref(row.id),\n disabledReason: campaignHref(row.id)\n ? undefined\n : 'This site’s console URL has not resolved yet',\n },\n {\n key: 'delete',\n label: 'Delete campaign',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: row.legacy || Boolean(deletingId),\n disabledReason: row.legacy\n ? 'This is a single send, not a campaign. Open it to ' +\n 'manage the email itself.'\n : 'A campaign is being deleted',\n onClick: () => void handleDelete(row as CampaignListRow),\n },\n ]}\n />\n ),\n { width: 72 },\n ),\n ],\n [listNames, campaignHref, deletingId, handleDelete, orgMount, filterHeaders],\n )\n\n /** Nothing of this kind stored at all — not a filter that matched nothing. */\n const none =\n page.status !== 'loading' && !rows.length && !filtering && page.page === 0\n\n /*\n * Other ways to start a campaign, from plugins (AGL-3603): the\n * `hostCampaigns` zone this plugin declares, drawn through the shell's own\n * gated slot. On the organization's hub it is handed the sites a campaign\n * could be placed on, since there is no one site to take.\n */\n const campaignsZone = (\n <HostCampaignsZone\n hostId={hostId}\n orgId={orgId}\n sites={\n hostId\n ? []\n : (orgMount?.hosts ?? []).map((host) => ({ id: host.id, name: host.name }))\n }\n />\n )\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#campaigns-group-emails',\n excerpt:\n 'A campaign groups the emails you send to a set of lists over a ' +\n 'window of dates. Open one to write and send an email inside it.',\n })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {campaignsZone}\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={creating}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {creating ? 'Creating…' : 'Create campaign'}\n </Button>\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={kind}\n onChange={(_event, next: CampaignListKind | null) => {\n if (next) setKind(next)\n }}\n aria-label=\"Which rows the table lists\"\n sx={{ alignSelf: 'flex-start' }}\n >\n <ToggleButton value=\"campaign\">{'Campaigns'}</ToggleButton>\n <ToggleButton value=\"single\">{'Single sends'}</ToggleButton>\n </ToggleButtonGroup>\n <ListFilterChips\n fields={declaration.fields}\n headers={filterHeaders}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(page.plan.refused, {\n fields: declaration.fields,\n headers: filterHeaders,\n options: filterOptions,\n })}\n notices={\n siteSearch\n ? [CAMPAIGN_SITE_SEARCH_NOTICE, ...page.plan.notices]\n : page.plan.notices\n }\n />\n <ListTable\n rows={rows}\n columns={listFilterGridColumns(\n columns as any,\n declaration.fields,\n filterOptions,\n filterHeaders,\n )}\n onOpen={(id) => openCampaign(id)}\n loading={page.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the list's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n hideFooter\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n declaration.fields,\n (columns as Array<{ field: string }>).map((column) => column.field),\n ),\n },\n }}\n noRowsLabel={\n none\n ? kind === 'campaign'\n ? 'No campaigns yet'\n : 'No single sends'\n : kind === 'campaign'\n ? 'No campaigns match these filters'\n : 'No single sends match these filters'\n }\n noRowsDescription={\n none\n ? kind === 'campaign'\n ? 'A campaign groups the emails you send to a set of lists.'\n : 'An email filed under no campaign is listed here.'\n : undefined\n }\n noRowsAction={\n none && kind === 'campaign' ? (\n <Stack direction=\"row\" spacing={1}>\n {/* Other ways to start a campaign, the header's zone again. */}\n {campaignsZone}\n <Button variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'Create campaign'}\n </Button>\n </Stack>\n ) : undefined\n }\n />\n <ListPagination\n page={page.page}\n pageSize={page.pageSize}\n rowCount={rows.length}\n hasMore={page.hasMore}\n onPageChange={page.setPage}\n onPageSizeChange={page.setPageSize}\n />\n {/*\n WHAT THIS TABLE MEASURES, said rather than inferred from the column\n names. Every figure in it is a fact about the campaign's mail, and a\n reader comparing rows is comparing mailings — the conversions and the\n destinations credited to a campaign are on the campaign's own page,\n because counting them for every row is a read per row on a list whose\n whole cost is two queries.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'These figures measure each campaign’s mail. What a campaign ' +\n 'caused — the form submissions, leads, contacts and bookings ' +\n 'credited to it — and the pages it sent people to are on the ' +\n 'campaign’s own page.'}\n </Typography>\n {rollupTruncated ? (\n <Alert severity=\"info\">\n {`The campaigns on this page hold more than ${ROLLUP_CEILING} ` +\n 'emails between them, so their figures count the first ' +\n `${ROLLUP_CEILING} read.`}\n </Alert>\n ) : null}\n </Stack>\n {/*\n The console's own create drawer, from the shared library rather than a\n second one that looks like it. What a campaign collects beyond a name\n is its window, the lists it is aimed at and the stream its emails open\n on; the description box is left off because the container stores none,\n and a field the writer discards is worse than one never offered.\n */}\n {/* Draws nothing: the zone the topic catalog's owner answers through. */}\n {topicSource}\n <CreateArtifactDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Create campaign\"\n submitLabel=\"Create campaign\"\n includeDescription={false}\n onSubmit={handleCreate}\n extraFields={campaignFields(\n listOptions,\n topicOptions,\n orgMount ? orgSiteOptions(orgMount) : null,\n )}\n errorSlot={\n createError ? (\n <Alert severity=\"error\" sx={{ mt: 2, mb: 1 }}>\n {createError}\n </Alert>\n ) : null\n }\n />\n </CardDisplay>\n )\n}\n/**\n * What a campaign collects beyond its name.\n *\n * Data-driven-forms field descriptors, built from the org's own lists and\n * topics so the drawer offers what this workspace actually has. Dates are\n * plain `date` inputs: a campaign window is a day at each end, and a time of\n * day would be a precision the model does not carry.\n */\nfunction campaignFields(\n lists: Array<{ value: string; label: string }>,\n topics: Array<{ value: string; label: string }>,\n /** The org's sites, on the org hub only — a site hub places on itself. */\n sites: Array<{ value: string; label: string }> | null,\n): any[] {\n return [\n ...(sites\n ? [\n {\n component: 'select',\n name: 'siteIds',\n label: 'Sites',\n multiple: true,\n initialValue: [],\n helperText:\n 'The sites this campaign is offered on. Leave empty for every site.',\n disableDefaultOption: true,\n options: sites,\n },\n ]\n : []),\n {\n component: 'text-field',\n name: 'startAt',\n label: 'Starts',\n type: 'date',\n helperText: 'When the campaign window opens',\n },\n {\n component: 'text-field',\n name: 'endAt',\n label: 'Ends',\n type: 'date',\n helperText: 'Leave empty for an open-ended campaign',\n InputLabelProps: { shrink: true },\n },\n {\n component: 'select',\n name: 'listIds',\n label: 'Lists',\n multiple: true,\n initialValue: [],\n // A campaign with no list is legitimate: its emails can go to leads, to\n // site members, or to a segment. The lists are what it is AIMED at.\n helperText: 'The lists this campaign is aimed at',\n disableDefaultOption: true,\n options: lists,\n },\n {\n component: 'select',\n name: 'topicId',\n label: 'Topic',\n helperText: 'The stream its emails open on — each one can change it',\n disableDefaultOption: true,\n options: topics,\n },\n ]\n}\n\n/**\n * The Sites cell: where a campaign is offered.\n *\n * A container reads its `visibleTo`; a single send, which has no container,\n * is on the one site it was sent as. An empty placement is said rather than\n * left blank, because a blank cell reads as \"every site\" and it means none.\n */\nfunction campaignSitesLabel(\n mount: MarketingOrgMount,\n row: CampaignListRow,\n container: EmailCampaign | undefined,\n): string {\n if (row.legacy) return orgSiteName(mount, row.sends[0]?.hostId) || '—'\n const ids = campaignSiteIds(container)\n if (ids === null) return 'Every site'\n if (!ids.length) return 'No site'\n return ids.map((id) => orgSiteName(mount, id)).join(', ')\n}\n\nHostCampaignsCard.displayName = 'HostCampaignsCard'\n\nexport default HostCampaignsCard\n"],"names":["buildRoute","createResourceUid","pluginDocsHelp","Route","ceilingedWindow","collectionCeiling","useListQuery","mdiDeleteOutline","mdiEyeOutline","AppLink","CardDisplay","MdiIcon","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Alert","Button","Chip","Stack","ToggleButton","ToggleButtonGroup","Typography","collection","limit","query","setDoc","where","useCallback","useMemo","useState","useRouter","useConsoleHostRoute","useFirestore","useFirestoreCollection","useUser","campaignListRows","campaignSiteIds","campaignVisibleTo","CreateArtifactDrawer","CAMPAIGN_SITE_SEARCH_NOTICE","SINGLE_SENDS_BASE","campaignContainerSearchFields","campaignContainersListQuery","campaignContainersScope","campaignSendsScope","campaignSingleSendsListQuery","campaignSiteSearchClause","campaignContainerDoc","campaignContainersCollection","campaignSendsCollection","orgSiteName","orgSiteOptions","topicCatalogHostId","useMarketingOrgId","useMarketingOrgMount","useCampaignManageApi","HostCampaignsZone","useCampaignTopicOptions","ROLLUP_CEILING","IN_LIMIT","formatDay","ms","Date","toLocaleDateString","figure","value","toLocaleString","WINDOW_LABEL","undated","upcoming","running","ended","CAMPAIGN_FILTER_HEADERS","campaign","name","listIds","sitesLabel","createdAtMs","single","NO_CLAUSES","HostCampaignsCard","props","hostId","basePath","firestore","router","orgSlug","subdomain","hubPath","HOST_PLUGIN","host","pluginSlug","data","user","enqueueSnackbar","confirm","orgMount","orgId","manageApi","kind","setKind","clausesByKind","setClausesByKind","searchWords","setSearchWords","setKindClauses","next","current","gridFilter","clauses","onChange","selectFields","search","words","containerDeclaration","singleDeclaration","declaration","filterHeaders","siteSearch","containerPage","request","base","deps","idField","singlePage","page","filtering","length","some","word","trim","pageIds","rows","map","row","String","$id","rollupRead","ids","sends","firstIds","slice","restIds","firstSendDocs","join","restSendDocs","firstSends","restSends","rollupTruncated","truncated","listDocs","createOpen","setCreateOpen","topics","source","topicSource","enabled","topicOptions","topic","id","label","listNames","names","Map","list","set","listOptions","read","order","at","built","now","byId","filter","has","sort","a","b","get","campaignSitesLabel","filterOptions","sites","site","createError","setCreateError","creating","setCreating","deletingId","setDeletingId","openCampaign","push","handleCreate","values","displayName","startAtMs","startAt","parse","endAtMs","endAt","visibleTo","Array","isArray","siteIds","Number","isFinite","topicId","createdBy","uid","error","console","variant","allowDuplicate","campaignHref","handleDelete","emails","unfinished","rollup","scheduled","sending","agreed","title","description","confirmationText","then","catch","response","payload","action","campaignId","ok","persist","columns","field","headerName","flex","minWidth","renderCell","direction","spacing","sx","alignItems","width","href","onClick","event","stopPropagation","style","noWrap","legacy","size","flexShrink","sortable","start","end","range","windowState","color","align","headerAlign","note","Boolean","sent","opens","clicks","items","key","icon","path","undefined","disabled","disabledReason","destructive","none","status","campaignsZone","hosts","header","help","anchor","excerpt","HeaderProps","contentGutterX","contentGutterY","exclusive","_event","aria-label","alignSelf","fields","headers","setClauses","options","refused","plan","notices","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","hideFooter","initialState","columnVisibilityModel","column","noRowsLabel","noRowsDescription","noRowsAction","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","severity","open","onClose","submitLabel","includeDescription","onSubmit","extraFields","campaignFields","errorSlot","mt","mb","lists","component","multiple","initialValue","helperText","disableDefaultOption","type","InputLabelProps","shrink","mount","container"],"mappings":"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"}
|
|
@@ -37,6 +37,13 @@ export interface MarketingExperimentVariantDraft {
|
|
|
37
37
|
/** A screen or section variant's copy is its screen version, so it carries none here. */
|
|
38
38
|
subject: string;
|
|
39
39
|
body: string;
|
|
40
|
+
/**
|
|
41
|
+
* A draft screen version a widget MADE for a page or section variant
|
|
42
|
+
* (AGL-3603), handed back through `proposeVariants` to be pinned unsaved.
|
|
43
|
+
* Never handed TO a widget: the version a variant already pins is the
|
|
44
|
+
* test's shape, not copy.
|
|
45
|
+
*/
|
|
46
|
+
versionId?: string;
|
|
40
47
|
}
|
|
41
48
|
/** What {@link EXPERIMENT_VARIANTS_ZONE} hands each widget. */
|
|
42
49
|
export interface MarketingExperimentVariantsZoneProps {
|
|
@@ -48,6 +55,10 @@ export interface MarketingExperimentVariantsZoneProps {
|
|
|
48
55
|
target: ExperimentTarget;
|
|
49
56
|
/** The conversion event the test counts, as the goal names it. */
|
|
50
57
|
goal: string;
|
|
58
|
+
/** A page or section test's page; empty until one is picked, and for an email (AGL-3603). */
|
|
59
|
+
screenId: string;
|
|
60
|
+
/** A section test's element; empty until one is picked, and for a page or an email. */
|
|
61
|
+
nodeId: string;
|
|
51
62
|
variants: MarketingExperimentVariantDraft[];
|
|
52
63
|
/**
|
|
53
64
|
* Fills the editor's variant fields with what a widget proposes, unsaved.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/experiment-zones.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsoleWidgetSlotRenderer,\n useConsoleWidgetSlot,\n} from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport type { ExperimentTarget, ExperimentVariant } from '../model/experiments'\n\n/**\n * The two zones the A/B testing card hosts (AGL-2914).\n *\n * A/B testing is this plugin's: the experiment document, its variants, the\n * traffic split, the counters and the comparison. What it never had was\n * anyone to help WRITE the variants or READ the result in words, and the\n * plugin that could do both may not be imported here any more than this one\n * may be imported there.\n *\n * So the card hosts two positions and says what each hands a widget. A\n * widget is drawn by the console shell's own renderer, through the context it\n * hands down, so every gate a console page's slot applies — the plugins this\n * workspace has, the entitlement the widget declares, the permission its\n * reader must hold — applies here unchanged. Outside the shell the context is\n * `null` and a zone draws nothing, which is also what a workspace with no\n * widget for the zone gets: the card renders exactly as it did before these\n * positions existed.\n *\n * ## A widget proposes; this card writes\n *\n * {@link EXPERIMENT_VARIANTS_ZONE} hands a widget the experiment being\n * edited and a `proposeVariants` callback. The callback fills the editor's\n * OWN fields, unsaved, and the dialog's Save is the only write — the same\n * guarded write a typed variant takes. Nothing a widget returns reaches\n * Firestore, starts a test or moves a visitor.\n *\n * {@link EXPERIMENT_RESULT_ZONE} hands a widget the test whose results are\n * open, by the name the results figures label it with, so a widget that\n * explains a result is looking at the same test the reader is. It takes no\n * callback: there is nothing to apply to an explanation.\n */\n\n/** One variant as the editor holds it, for a widget writing alternatives to it. */\nexport interface MarketingExperimentVariantDraft {\n id: string\n name: string\n /** A screen or section variant's copy is its screen version, so it carries none here. */\n subject: string\n body: string\n}\n\n/** What {@link EXPERIMENT_VARIANTS_ZONE} hands each widget. */\nexport interface MarketingExperimentVariantsZoneProps {\n hostId: string\n /** The experiment's id once it has been saved; empty while it is new. */\n experimentId: string\n /** The experiment's name as the editor holds it. */\n name: string\n target: ExperimentTarget\n /** The conversion event the test counts, as the goal names it. */\n goal: string\n variants: MarketingExperimentVariantDraft[]\n /**\n * Fills the editor's variant fields with what a widget proposes, unsaved.\n * `key` identifies the proposal so a widget can tell which of its own the\n * editor is showing. Variants past the ones the editor holds are ignored:\n * the editor's list is the test's shape and a widget does not change it.\n */\n proposeVariants: (\n variants: readonly MarketingExperimentVariantDraft[],\n key: string,\n ) => void\n}\n\n/** What {@link EXPERIMENT_RESULT_ZONE} hands each widget. */\nexport interface MarketingExperimentResultZoneProps {\n hostId: string\n experimentId: string\n /** The test's name as the results figures label it, which is how a reader names one. */\n test: string\n}\n\nexport const EXPERIMENT_VARIANTS_ZONE =\n definePluginZone<MarketingExperimentVariantsZoneProps>('experimentVariants')\n\nexport const EXPERIMENT_RESULT_ZONE =\n definePluginZone<MarketingExperimentResultZoneProps>('experimentResult')\n\n/**\n * Draws a hosted zone through the shell's renderer, or nothing at all when\n * there is no shell. One component for both zones: they differ only in the\n * id they name and the props they carry, and a zone that drew a heading or a\n * wrapper of its own would change the card for every workspace that has no\n * widget for it.\n */\nfunction HostedZone(props: {\n slot: string\n zone: Record<string, unknown>\n Slot: ConsoleWidgetSlotRenderer\n}) {\n const { slot, zone, Slot } = props\n return <Slot slot={slot} {...zone} />\n}\n\nexport function ExperimentVariantsZone(props: MarketingExperimentVariantsZoneProps) {\n const Slot = useConsoleWidgetSlot()\n return Slot ? (\n <HostedZone slot={EXPERIMENT_VARIANTS_ZONE.id} zone={{ ...props }} Slot={Slot} />\n ) : null\n}\nExperimentVariantsZone.displayName = 'ExperimentVariantsZone'\n\nexport function ExperimentResultZone(props: MarketingExperimentResultZoneProps) {\n const Slot = useConsoleWidgetSlot()\n return Slot ? (\n <HostedZone slot={EXPERIMENT_RESULT_ZONE.id} zone={{ ...props }} Slot={Slot} />\n ) : null\n}\nExperimentResultZone.displayName = 'ExperimentResultZone'\n\n/** The editor's variants as the zone hands them over. */\nexport function marketingExperimentVariantDrafts(\n variants: readonly ExperimentVariant[] | undefined,\n): MarketingExperimentVariantDraft[] {\n return (variants ?? []).map((variant) => ({\n id: variant.id,\n name: variant.name ?? '',\n subject: variant.subject ?? '',\n body: variant.body ?? '',\n }))\n}\n"],"names":["useConsoleWidgetSlot","definePluginZone","EXPERIMENT_VARIANTS_ZONE","EXPERIMENT_RESULT_ZONE","HostedZone","props","slot","zone","Slot","ExperimentVariantsZone","id","displayName","ExperimentResultZone","marketingExperimentVariantDrafts","variants","map","variant","name","subject","body"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,oBAAoB,QACf,qDAAoD;AAC3D,SAASC,gBAAgB,QAAQ,2CAA0C;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/experiment-zones.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsoleWidgetSlotRenderer,\n useConsoleWidgetSlot,\n} from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport type { ExperimentTarget, ExperimentVariant } from '../model/experiments'\n\n/**\n * The two zones the A/B testing card hosts (AGL-2914).\n *\n * A/B testing is this plugin's: the experiment document, its variants, the\n * traffic split, the counters and the comparison. What it never had was\n * anyone to help WRITE the variants or READ the result in words, and the\n * plugin that could do both may not be imported here any more than this one\n * may be imported there.\n *\n * So the card hosts two positions and says what each hands a widget. A\n * widget is drawn by the console shell's own renderer, through the context it\n * hands down, so every gate a console page's slot applies — the plugins this\n * workspace has, the entitlement the widget declares, the permission its\n * reader must hold — applies here unchanged. Outside the shell the context is\n * `null` and a zone draws nothing, which is also what a workspace with no\n * widget for the zone gets: the card renders exactly as it did before these\n * positions existed.\n *\n * ## A widget proposes; this card writes\n *\n * {@link EXPERIMENT_VARIANTS_ZONE} hands a widget the experiment being\n * edited and a `proposeVariants` callback. The callback fills the editor's\n * OWN fields, unsaved, and the dialog's Save is the only write — the same\n * guarded write a typed variant takes. Nothing a widget returns reaches\n * Firestore, starts a test or moves a visitor.\n *\n * {@link EXPERIMENT_RESULT_ZONE} hands a widget the test whose results are\n * open, by the name the results figures label it with, so a widget that\n * explains a result is looking at the same test the reader is. It takes no\n * callback: there is nothing to apply to an explanation.\n */\n\n/** One variant as the editor holds it, for a widget writing alternatives to it. */\nexport interface MarketingExperimentVariantDraft {\n id: string\n name: string\n /** A screen or section variant's copy is its screen version, so it carries none here. */\n subject: string\n body: string\n /**\n * A draft screen version a widget MADE for a page or section variant\n * (AGL-3603), handed back through `proposeVariants` to be pinned unsaved.\n * Never handed TO a widget: the version a variant already pins is the\n * test's shape, not copy.\n */\n versionId?: string\n}\n\n/** What {@link EXPERIMENT_VARIANTS_ZONE} hands each widget. */\nexport interface MarketingExperimentVariantsZoneProps {\n hostId: string\n /** The experiment's id once it has been saved; empty while it is new. */\n experimentId: string\n /** The experiment's name as the editor holds it. */\n name: string\n target: ExperimentTarget\n /** The conversion event the test counts, as the goal names it. */\n goal: string\n /** A page or section test's page; empty until one is picked, and for an email (AGL-3603). */\n screenId: string\n /** A section test's element; empty until one is picked, and for a page or an email. */\n nodeId: string\n variants: MarketingExperimentVariantDraft[]\n /**\n * Fills the editor's variant fields with what a widget proposes, unsaved.\n * `key` identifies the proposal so a widget can tell which of its own the\n * editor is showing. Variants past the ones the editor holds are ignored:\n * the editor's list is the test's shape and a widget does not change it.\n */\n proposeVariants: (\n variants: readonly MarketingExperimentVariantDraft[],\n key: string,\n ) => void\n}\n\n/** What {@link EXPERIMENT_RESULT_ZONE} hands each widget. */\nexport interface MarketingExperimentResultZoneProps {\n hostId: string\n experimentId: string\n /** The test's name as the results figures label it, which is how a reader names one. */\n test: string\n}\n\nexport const EXPERIMENT_VARIANTS_ZONE =\n definePluginZone<MarketingExperimentVariantsZoneProps>('experimentVariants')\n\nexport const EXPERIMENT_RESULT_ZONE =\n definePluginZone<MarketingExperimentResultZoneProps>('experimentResult')\n\n/**\n * Draws a hosted zone through the shell's renderer, or nothing at all when\n * there is no shell. One component for both zones: they differ only in the\n * id they name and the props they carry, and a zone that drew a heading or a\n * wrapper of its own would change the card for every workspace that has no\n * widget for it.\n */\nfunction HostedZone(props: {\n slot: string\n zone: Record<string, unknown>\n Slot: ConsoleWidgetSlotRenderer\n}) {\n const { slot, zone, Slot } = props\n return <Slot slot={slot} {...zone} />\n}\n\nexport function ExperimentVariantsZone(props: MarketingExperimentVariantsZoneProps) {\n const Slot = useConsoleWidgetSlot()\n return Slot ? (\n <HostedZone slot={EXPERIMENT_VARIANTS_ZONE.id} zone={{ ...props }} Slot={Slot} />\n ) : null\n}\nExperimentVariantsZone.displayName = 'ExperimentVariantsZone'\n\nexport function ExperimentResultZone(props: MarketingExperimentResultZoneProps) {\n const Slot = useConsoleWidgetSlot()\n return Slot ? (\n <HostedZone slot={EXPERIMENT_RESULT_ZONE.id} zone={{ ...props }} Slot={Slot} />\n ) : null\n}\nExperimentResultZone.displayName = 'ExperimentResultZone'\n\n/** The editor's variants as the zone hands them over. */\nexport function marketingExperimentVariantDrafts(\n variants: readonly ExperimentVariant[] | undefined,\n): MarketingExperimentVariantDraft[] {\n return (variants ?? []).map((variant) => ({\n id: variant.id,\n name: variant.name ?? '',\n subject: variant.subject ?? '',\n body: variant.body ?? '',\n }))\n}\n"],"names":["useConsoleWidgetSlot","definePluginZone","EXPERIMENT_VARIANTS_ZONE","EXPERIMENT_RESULT_ZONE","HostedZone","props","slot","zone","Slot","ExperimentVariantsZone","id","displayName","ExperimentResultZone","marketingExperimentVariantDrafts","variants","map","variant","name","subject","body"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,oBAAoB,QACf,qDAAoD;AAC3D,SAASC,gBAAgB,QAAQ,2CAA0C;AAsF3E,OAAO,MAAMC,2BACXD,iBAAuD,sBAAqB;AAE9E,OAAO,MAAME,yBACXF,iBAAqD,oBAAmB;AAE1E;;;;;;CAMC,GACD,SAASG,WAAWC,KAInB;IACC,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAEC,IAAI,EAAE,GAAGH;IAC7B,qBAAO,KAACG;QAAKF,MAAMA;OAAUC;AAC/B;AAEA,OAAO,SAASE,uBAAuBJ,KAA2C;IAChF,MAAMG,OAAOR;IACb,OAAOQ,qBACL,KAACJ;QAAWE,MAAMJ,yBAAyBQ,EAAE;QAAEH,MAAM,aAAKF;QAASG,MAAMA;SACvE;AACN;AACAC,uBAAuBE,WAAW,GAAG;AAErC,OAAO,SAASC,qBAAqBP,KAAyC;IAC5E,MAAMG,OAAOR;IACb,OAAOQ,qBACL,KAACJ;QAAWE,MAAMH,uBAAuBO,EAAE;QAAEH,MAAM,aAAKF;QAASG,MAAMA;SACrE;AACN;AACAI,qBAAqBD,WAAW,GAAG;AAEnC,uDAAuD,GACvD,OAAO,SAASE,iCACdC,QAAkD;IAElD,OAAO,CAACA,mBAAAA,WAAY,EAAE,EAAEC,GAAG,CAAC,CAACC;YAErBA,eACGA,kBACHA;eAJkC;YACxCN,IAAIM,QAAQN,EAAE;YACdO,IAAI,GAAED,gBAAAA,QAAQC,IAAI,YAAZD,gBAAgB;YACtBE,OAAO,GAAEF,mBAAAA,QAAQE,OAAO,YAAfF,mBAAmB;YAC5BG,IAAI,GAAEH,gBAAAA,QAAQG,IAAI,YAAZH,gBAAgB;QACxB;;AACF"}
|
|
@@ -87,7 +87,7 @@ const EXPERIMENT_FILTER_OPTIONS = {
|
|
|
87
87
|
* and read exposures/conversions per variant. Business tier
|
|
88
88
|
* (`ab-testing`).
|
|
89
89
|
*/ export function HostExperimentsCard(props) {
|
|
90
|
-
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _editor_$id, _editor_name, _editor_target, _ref6, _ref7, _results_experiment_$id, _results_experiment_$id1;
|
|
90
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _editor_$id, _editor_name, _editor_target, _ref6, _editor_screenId, _editor_nodeId, _ref7, _results_experiment_$id, _results_experiment_$id1;
|
|
91
91
|
var _editor_goal, _editor_goal1, _results_experiment_name;
|
|
92
92
|
const { hostId } = props;
|
|
93
93
|
const firestore = useFirestore();
|
|
@@ -214,6 +214,8 @@ const EXPERIMENT_FILTER_OPTIONS = {
|
|
|
214
214
|
subject: draft.subject
|
|
215
215
|
} : {}, draft.body ? {
|
|
216
216
|
body: draft.body
|
|
217
|
+
} : {}, draft.versionId && previous.target !== 'email' ? {
|
|
218
|
+
versionId: draft.versionId
|
|
217
219
|
} : {});
|
|
218
220
|
})
|
|
219
221
|
}) : previous);
|
|
@@ -854,6 +856,8 @@ const EXPERIMENT_FILTER_OPTIONS = {
|
|
|
854
856
|
name: (_editor_name = editor.name) != null ? _editor_name : '',
|
|
855
857
|
target: (_editor_target = editor.target) != null ? _editor_target : 'screen',
|
|
856
858
|
goal: (_ref6 = (_editor_goal1 = editor.goal) == null ? void 0 : _editor_goal1.event) != null ? _ref6 : '',
|
|
859
|
+
screenId: editor.target === 'email' ? '' : (_editor_screenId = editor.screenId) != null ? _editor_screenId : '',
|
|
860
|
+
nodeId: editor.target === 'section' ? (_editor_nodeId = editor.nodeId) != null ? _editor_nodeId : '' : '',
|
|
857
861
|
variants: marketingExperimentVariantDrafts(editor.variants),
|
|
858
862
|
proposeVariants: proposeVariants
|
|
859
863
|
}) : null
|