@aglyn/plugins-marketing 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -15
- package/src/lib/components/campaign-composer.js +14 -6
- package/src/lib/components/campaign-composer.js.map +1 -1
- package/src/lib/components/campaign-conversions-card.js +6 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +107 -37
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-members-section.d.ts +16 -4
- package/src/lib/components/campaign-members-section.js +171 -105
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +11 -0
- package/src/lib/components/campaign-queries.js +13 -0
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaigns-card.d.ts +8 -6
- package/src/lib/components/campaigns-card.js +296 -128
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.js +4 -3
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +16 -5
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-recipients-card.js +3 -1
- package/src/lib/components/email-recipients-card.js.map +1 -1
- package/src/lib/components/emails-list-card.d.ts +11 -15
- package/src/lib/components/emails-list-card.js +142 -143
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +110 -78
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/org-experiments-card.js +7 -3
- package/src/lib/components/org-experiments-card.js.map +1 -1
- package/src/lib/model/campaign-list-query.d.ts +99 -0
- package/src/lib/model/campaign-list-query.js +373 -0
- package/src/lib/model/campaign-list-query.js.map +1 -0
- package/src/lib/model/campaign-members-query.d.ts +37 -0
- package/src/lib/model/campaign-members-query.js +120 -0
- package/src/lib/model/campaign-members-query.js.map +1 -0
- package/src/lib/model/experiment-list-query.d.ts +22 -0
- package/src/lib/model/experiment-list-query.js +85 -0
- package/src/lib/model/experiment-list-query.js.map +1 -0
- package/src/lib/server/campaign-form-flags.d.ts +35 -0
- package/src/lib/server/campaign-form-flags.js +52 -0
- package/src/lib/server/campaign-form-flags.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +1 -1
- package/src/lib/server/campaign-manage.js +42 -20
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +42 -1
- package/src/lib/server/campaign-send.js +247 -16
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +13 -1
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/components/email-send-filter.d.ts +0 -32
- package/src/lib/components/email-send-filter.js +0 -55
- package/src/lib/components/email-send-filter.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-queries.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostScopeToken, scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n orgCampaignSendsPath,\n orgCampaignSequenceReportsPath,\n orgEmailCampaignsPath,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport {\n collection,\n doc,\n query,\n where,\n type DocumentReference,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * WHERE A CAMPAIGN AND ITS SENDS ARE READ FROM, at either level of the hub.\n *\n * Both collections belong to the organization: containers at\n * `orgs/{orgId}/emailCampaigns`, sends at `orgs/{orgId}/campaigns`, each\n * send's rollups under it at `…/reports/{name}`. Spelled once here so no card\n * rebuilds a path from memory, and so the one rule that makes a site hub's\n * read legal is in one place:\n *\n * **A site hub's LIST reads are filtered by scope.** The rules let an\n * org-wide member read every document and a site collaborator only those\n * whose `visibleTo` names a scope they hold. A query the rules cannot prove\n * document by document is refused whole, so a site hub asks exactly for what\n * that site may see — a container placed on every site (`'org'`) or on this\n * one (`'host:{id}'`), and a send sent AS this site. An org-wide member loses\n * nothing by the filter at a site: those are the campaigns the site shows.\n *\n * The ORG hub is admitted only to org-wide members, so its reads are\n * unfiltered.\n *\n * One `array-contains-any` per query is Firestore's limit, so no caller adds\n * a second array clause to these; per-document reads need no filter at all.\n */\n\n/**\n * `orgs/{orgId}/emailCampaigns`, narrowed to one site's placements when\n * `hostId` names a site; `null` until the org is known.\n */\nexport function campaignContainersQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const containers = collection(firestore, ...orgEmailCampaignsPath(orgId))\n return hostId\n ? query(\n containers,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n )\n : containers\n}\n\n/**\n * `orgs/{orgId}/campaigns` — the sends — narrowed to the ones sent as one\n * site when `hostId` names a site; `null` until the org is known.\n */\nexport function campaignSendsQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const sends = collection(firestore, ...orgCampaignSendsPath(orgId))\n return hostId\n ? query(\n sends,\n where('visibleTo', 'array-contains-any', [hostScopeToken(hostId)]),\n )\n : sends\n}\n\n/** One container, `orgs/{orgId}/emailCampaigns/{id}`. */\nexport function campaignContainerDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgEmailCampaignsPath(orgId), campaignId)\n}\n\n/** One send, `orgs/{orgId}/campaigns/{sendId}`. */\nexport function campaignSendDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSendsPath(orgId), sendId)\n}\n\n/** One of a send's rollups, `orgs/{orgId}/campaigns/{sendId}/reports/{name}`. */\nexport function campaignSendReportDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n report: 'links' | 'revenue' | 'conversions' | 'reached',\n): DocumentReference {\n return doc(\n firestore,\n ...orgCampaignSendsPath(orgId),\n sendId,\n 'reports',\n report,\n )\n}\n\n/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */\nexport function campaignSequenceReportDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSequenceReportsPath(orgId), campaignId)\n}\n"],"names":["hostScopeToken","scopeTokensForHost","orgCampaignSendsPath","orgCampaignSequenceReportsPath","orgEmailCampaignsPath","collection","doc","query","where","campaignContainersQuery","firestore","orgId","hostId","containers","campaignSendsQuery","sends","campaignContainerDoc","campaignId","campaignSendDoc","sendId","campaignSendReportDoc","report","campaignSequenceReportDoc"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,EAAEC,kBAAkB,QAAQ,sCAAqC;AACxF,SACEC,oBAAoB,EACpBC,8BAA8B,EAC9BC,qBAAqB,QAChB,4DAA2D;AAClE,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,KAAK,QAIA,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED;;;CAGC,GACD,OAAO,SAASC,wBACdC,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,aAAaR,WAAWK,cAAcN,sBAAsBO;IAClE,OAAOC,SACHL,MACEM,YACAL,MAAM,aAAa,sBAAsBP,mBAAmBW,YAE9DC;AACN;AAEA;;;CAGC,GACD,OAAO,SAASC,mBACdJ,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAMI,QAAQV,WAAWK,cAAcR,qBAAqBS;IAC5D,OAAOC,SACHL,MACEQ,OACAP,MAAM,aAAa,sBAAsB;QAACR,eAAeY;KAAQ,KAEnEG;AACN;AAEA,uDAAuD,GACvD,OAAO,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/campaign-queries.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostScopeToken, scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n orgCampaignSendsPath,\n orgCampaignSequenceReportsPath,\n orgEmailCampaignsPath,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport {\n collection,\n doc,\n query,\n where,\n type DocumentReference,\n type Firestore,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * WHERE A CAMPAIGN AND ITS SENDS ARE READ FROM, at either level of the hub.\n *\n * Both collections belong to the organization: containers at\n * `orgs/{orgId}/emailCampaigns`, sends at `orgs/{orgId}/campaigns`, each\n * send's rollups under it at `…/reports/{name}`. Spelled once here so no card\n * rebuilds a path from memory, and so the one rule that makes a site hub's\n * read legal is in one place:\n *\n * **A site hub's LIST reads are filtered by scope.** The rules let an\n * org-wide member read every document and a site collaborator only those\n * whose `visibleTo` names a scope they hold. A query the rules cannot prove\n * document by document is refused whole, so a site hub asks exactly for what\n * that site may see — a container placed on every site (`'org'`) or on this\n * one (`'host:{id}'`), and a send sent AS this site. An org-wide member loses\n * nothing by the filter at a site: those are the campaigns the site shows.\n *\n * The ORG hub is admitted only to org-wide members, so its reads are\n * unfiltered.\n *\n * One `array-contains-any` per query is Firestore's limit, so no caller adds\n * a second array clause to these; per-document reads need no filter at all.\n */\n\n/**\n * `orgs/{orgId}/emailCampaigns`, narrowed to one site's placements when\n * `hostId` names a site; `null` until the org is known.\n */\nexport function campaignContainersQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const containers = collection(firestore, ...orgEmailCampaignsPath(orgId))\n return hostId\n ? query(\n containers,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n )\n : containers\n}\n\n/**\n * `orgs/{orgId}/campaigns` — the sends — narrowed to the ones sent as one\n * site when `hostId` names a site; `null` until the org is known.\n */\nexport function campaignSendsQuery(\n firestore: Firestore,\n orgId: string | null,\n hostId: string | null,\n): Query | null {\n if (!orgId) return null\n const sends = collection(firestore, ...orgCampaignSendsPath(orgId))\n return hostId\n ? query(\n sends,\n where('visibleTo', 'array-contains-any', [hostScopeToken(hostId)]),\n )\n : sends\n}\n\n/**\n * The two collections UNSCOPED, for a paged list that puts its scope on its\n * query plan (AGL-3321): `campaignSendsScope` / `campaignContainersScope` in\n * `model/campaign-list-query.ts` carry the site's clause there, beside the\n * list's own. A site hub's sends are asked `hostId == {site}` rather than by\n * `visibleTo`, so search keeps the query's one array clause; the rules\n * admit that read for the site's collaborators too.\n */\nexport function campaignSendsCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgCampaignSendsPath(orgId)) : null\n}\n\n/** `orgs/{orgId}/emailCampaigns`, unscoped — see {@link campaignSendsCollection}. */\nexport function campaignContainersCollection(\n firestore: Firestore,\n orgId: string | null,\n): Query | null {\n return orgId ? collection(firestore, ...orgEmailCampaignsPath(orgId)) : null\n}\n\n/** One container, `orgs/{orgId}/emailCampaigns/{id}`. */\nexport function campaignContainerDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgEmailCampaignsPath(orgId), campaignId)\n}\n\n/** One send, `orgs/{orgId}/campaigns/{sendId}`. */\nexport function campaignSendDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSendsPath(orgId), sendId)\n}\n\n/** One of a send's rollups, `orgs/{orgId}/campaigns/{sendId}/reports/{name}`. */\nexport function campaignSendReportDoc(\n firestore: Firestore,\n orgId: string,\n sendId: string,\n report: 'links' | 'revenue' | 'conversions' | 'reached',\n): DocumentReference {\n return doc(\n firestore,\n ...orgCampaignSendsPath(orgId),\n sendId,\n 'reports',\n report,\n )\n}\n\n/** A container's sequence rollup, `orgs/{orgId}/campaignSequenceReports/{id}`. */\nexport function campaignSequenceReportDoc(\n firestore: Firestore,\n orgId: string,\n campaignId: string,\n): DocumentReference {\n return doc(firestore, ...orgCampaignSequenceReportsPath(orgId), campaignId)\n}\n"],"names":["hostScopeToken","scopeTokensForHost","orgCampaignSendsPath","orgCampaignSequenceReportsPath","orgEmailCampaignsPath","collection","doc","query","where","campaignContainersQuery","firestore","orgId","hostId","containers","campaignSendsQuery","sends","campaignSendsCollection","campaignContainersCollection","campaignContainerDoc","campaignId","campaignSendDoc","sendId","campaignSendReportDoc","report","campaignSequenceReportDoc"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,EAAEC,kBAAkB,QAAQ,sCAAqC;AACxF,SACEC,oBAAoB,EACpBC,8BAA8B,EAC9BC,qBAAqB,QAChB,4DAA2D;AAClE,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,KAAK,QAIA,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED;;;CAGC,GACD,OAAO,SAASC,wBACdC,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,aAAaR,WAAWK,cAAcN,sBAAsBO;IAClE,OAAOC,SACHL,MACEM,YACAL,MAAM,aAAa,sBAAsBP,mBAAmBW,YAE9DC;AACN;AAEA;;;CAGC,GACD,OAAO,SAASC,mBACdJ,SAAoB,EACpBC,KAAoB,EACpBC,MAAqB;IAErB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAMI,QAAQV,WAAWK,cAAcR,qBAAqBS;IAC5D,OAAOC,SACHL,MACEQ,OACAP,MAAM,aAAa,sBAAsB;QAACR,eAAeY;KAAQ,KAEnEG;AACN;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdN,SAAoB,EACpBC,KAAoB;IAEpB,OAAOA,QAAQN,WAAWK,cAAcR,qBAAqBS,UAAU;AACzE;AAEA,mFAAmF,GACnF,OAAO,SAASM,6BACdP,SAAoB,EACpBC,KAAoB;IAEpB,OAAOA,QAAQN,WAAWK,cAAcN,sBAAsBO,UAAU;AAC1E;AAEA,uDAAuD,GACvD,OAAO,SAASO,qBACdR,SAAoB,EACpBC,KAAa,EACbQ,UAAkB;IAElB,OAAOb,IAAII,cAAcN,sBAAsBO,QAAQQ;AACzD;AAEA,iDAAiD,GACjD,OAAO,SAASC,gBACdV,SAAoB,EACpBC,KAAa,EACbU,MAAc;IAEd,OAAOf,IAAII,cAAcR,qBAAqBS,QAAQU;AACxD;AAEA,+EAA+E,GAC/E,OAAO,SAASC,sBACdZ,SAAoB,EACpBC,KAAa,EACbU,MAAc,EACdE,MAAuD;IAEvD,OAAOjB,IACLI,cACGR,qBAAqBS,QACxBU,QACA,WACAE;AAEJ;AAEA,gFAAgF,GAChF,OAAO,SAASC,0BACdd,SAAoB,EACpBC,KAAa,EACbQ,UAAkB;IAElB,OAAOb,IAAII,cAAcP,+BAA+BQ,QAAQQ;AAClE"}
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* messages. Composing is not here: a message belongs to a campaign, and the
|
|
7
7
|
* campaign's own page is where one is written.
|
|
8
8
|
*
|
|
9
|
-
* The rows come from two collections
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* The rows come from two collections. `emailCampaigns` holds the
|
|
10
|
+
* containers. `campaigns` holds every send, including thousands written
|
|
11
|
+
* before containers existed; those carry no container (`emailCampaignId`
|
|
12
|
+
* null), and `campaignListRows` presents each as a campaign of one rather
|
|
13
|
+
* than dropping it. The two are two queries, so the table lists one kind at a
|
|
14
|
+
* time — Campaigns or Single sends — each paged by its own query (AGL-3321).
|
|
15
|
+
* No send is re-filed, so no unsubscribe link and no pasted report URL stops
|
|
16
|
+
* resolving.
|
|
15
17
|
*
|
|
16
18
|
* ## At two levels
|
|
17
19
|
*
|
|
@@ -22,32 +22,38 @@ import { buildRoute, createResourceUid, pluginDocsHelp, Route } from "@aglyn/agl
|
|
|
22
22
|
* `@aglyn/tenant-feature-instance` wholesale to stage its Firestore hooks
|
|
23
23
|
* would otherwise lose them, and neither is a hook.
|
|
24
24
|
*/ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
25
|
+
import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
25
26
|
import { mdiDeleteOutline, mdiEyeOutline } from "@aglyn/shared-data-mdi";
|
|
26
27
|
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
27
28
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
29
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
30
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
28
31
|
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
32
|
+
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
33
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
34
|
+
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
31
35
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
32
|
-
import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
|
|
33
|
-
import { collection, limit, query, setDoc } from "firebase/firestore";
|
|
36
|
+
import { Alert, Button, Chip, Stack, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
|
37
|
+
import { collection, limit, query, setDoc, where } from "firebase/firestore";
|
|
34
38
|
import { useCallback, useMemo, useState } from "react";
|
|
35
39
|
import { useRouter } from "next/navigation";
|
|
36
40
|
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
|
|
37
41
|
import { campaignListRows, campaignSiteIds, campaignVisibleTo } from "@aglyn/shared-ui-email-campaigns/model";
|
|
38
42
|
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
39
|
-
import {
|
|
43
|
+
import { CAMPAIGN_SITE_SEARCH_NOTICE, SINGLE_SENDS_BASE, campaignContainerSearchFields, campaignContainersListQuery, campaignContainersScope, campaignSendsScope, campaignSingleSendsListQuery, campaignSiteSearchClause } from "../model/campaign-list-query.js";
|
|
44
|
+
import { campaignContainerDoc, campaignContainersCollection, campaignSendsCollection } from "./campaign-queries.js";
|
|
40
45
|
import { orgSiteName, orgSiteOptions, topicCatalogHostId, useMarketingOrgId, useMarketingOrgMount } from "./marketing-org-mount.js";
|
|
41
46
|
import { useCampaignManageApi } from "./use-campaign-send-api.js";
|
|
42
47
|
import { useCampaignTopicOptions } from "./use-campaign-topic-options.js";
|
|
43
48
|
/**
|
|
44
|
-
* How many
|
|
49
|
+
* How many emails one read of a page's campaigns covers, for their figures.
|
|
45
50
|
*
|
|
46
|
-
* A
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
* A ceiling on the ROLLUP read, not on the list: the list is paged by its
|
|
52
|
+
* own query, and this reads the emails of the campaigns on the page on
|
|
53
|
+
* screen, thirty campaigns to a read (`in` takes thirty ids), so the largest
|
|
54
|
+
* page is two reads of this many. Past it, the figures say so.
|
|
55
|
+
*/ const ROLLUP_CEILING = 50;
|
|
56
|
+
/** Firestore's cap on an `in` clause's values. */ const IN_LIMIT = 30;
|
|
51
57
|
const formatDay = (ms)=>ms === null ? '' : new Date(ms).toLocaleDateString();
|
|
52
58
|
/** A rolled-up figure, or an em dash where nothing recorded one. */ const figure = (value)=>value.value === null ? '—' : value.value.toLocaleString();
|
|
53
59
|
const WINDOW_LABEL = {
|
|
@@ -57,44 +63,29 @@ const WINDOW_LABEL = {
|
|
|
57
63
|
ended: 'Ended'
|
|
58
64
|
};
|
|
59
65
|
/*
|
|
60
|
-
* What the campaigns grid's Filters panel offers
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
inMemoryListField('sitesLabel', 'text')
|
|
72
|
-
];
|
|
73
|
-
const CAMPAIGN_FILTER_HEADERS = {
|
|
74
|
-
name: 'Campaign',
|
|
75
|
-
windowState: 'Window',
|
|
76
|
-
listIds: 'Lists',
|
|
77
|
-
kind: 'Kind',
|
|
78
|
-
sitesLabel: 'Sites'
|
|
79
|
-
};
|
|
80
|
-
const CAMPAIGN_KIND_OPTIONS = [
|
|
81
|
-
{
|
|
82
|
-
value: 'campaign',
|
|
83
|
-
label: 'Campaign'
|
|
66
|
+
* What the campaigns grid's Filters panel offers, each clause and the search
|
|
67
|
+
* on the list's Firestore query (AGL-3321, `campaignContainersListQuery` and
|
|
68
|
+
* `campaignSingleSendsListQuery`). The Window column is derived from a
|
|
69
|
+
* campaign's dates against the clock, which no query can ask, so it is not
|
|
70
|
+
* offered; the one date range is Created, the list's own order.
|
|
71
|
+
*/ const CAMPAIGN_FILTER_HEADERS = {
|
|
72
|
+
campaign: {
|
|
73
|
+
name: 'Campaign',
|
|
74
|
+
listIds: 'Lists',
|
|
75
|
+
sitesLabel: 'Sites',
|
|
76
|
+
createdAtMs: 'Created'
|
|
84
77
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
// A single send has no container; its `name` field filters by the subject.
|
|
79
|
+
single: {
|
|
80
|
+
name: 'Subject',
|
|
81
|
+
sitesLabel: 'Sites',
|
|
82
|
+
createdAtMs: 'Created'
|
|
88
83
|
}
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/** What the quick search reads on a campaign row. */ const CAMPAIGN_SEARCH_FIELDS = [
|
|
95
|
-
'name',
|
|
96
|
-
'sitesLabel'
|
|
97
|
-
];
|
|
84
|
+
};
|
|
85
|
+
const NO_CLAUSES = {
|
|
86
|
+
campaign: [],
|
|
87
|
+
single: []
|
|
88
|
+
};
|
|
98
89
|
/**
|
|
99
90
|
* THE CAMPAIGNS LIST.
|
|
100
91
|
*
|
|
@@ -103,12 +94,14 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
103
94
|
* messages. Composing is not here: a message belongs to a campaign, and the
|
|
104
95
|
* campaign's own page is where one is written.
|
|
105
96
|
*
|
|
106
|
-
* The rows come from two collections
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
97
|
+
* The rows come from two collections. `emailCampaigns` holds the
|
|
98
|
+
* containers. `campaigns` holds every send, including thousands written
|
|
99
|
+
* before containers existed; those carry no container (`emailCampaignId`
|
|
100
|
+
* null), and `campaignListRows` presents each as a campaign of one rather
|
|
101
|
+
* than dropping it. The two are two queries, so the table lists one kind at a
|
|
102
|
+
* time — Campaigns or Single sends — each paged by its own query (AGL-3321).
|
|
103
|
+
* No send is re-filed, so no unsubscribe link and no pasted report URL stops
|
|
104
|
+
* resolving.
|
|
112
105
|
*
|
|
113
106
|
* ## At two levels
|
|
114
107
|
*
|
|
@@ -134,42 +127,133 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
134
127
|
const { orgId } = useMarketingOrgId(hostId);
|
|
135
128
|
// A container is the org's, so deleting one from the org hub names the org.
|
|
136
129
|
const manageApi = useCampaignManageApi(hostId, orgId);
|
|
137
|
-
|
|
138
|
-
*
|
|
130
|
+
/*==========================================
|
|
131
|
+
* TWO KINDS OF ROW, EACH ITS OWN QUERY (AGL-3321).
|
|
139
132
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
133
|
+
* A campaign is a CONTAINER (`emailCampaigns`); a single send is an email
|
|
134
|
+
* filed under none (`campaigns` where `emailCampaignId == null`). The two
|
|
135
|
+
* live in different collections, and one Firestore query reads one
|
|
136
|
+
* collection — so a table that merged them could page neither, and every
|
|
137
|
+
* filter would be matched over whatever window of each it happened to
|
|
138
|
+
* hold. The toggle above the table picks the kind, and the table shows a
|
|
139
|
+
* page of that kind's query: newest first on `createdAtMs`, every clause
|
|
140
|
+
* and the search word on the query, paged by it. Same table, same
|
|
141
|
+
* columns; a single send still reads as a campaign of one.
|
|
144
142
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
* Each kind keeps its own clauses (their Sites values differ: a campaign
|
|
144
|
+
* is asked by scope token, a send by the site it was sent as); the search
|
|
145
|
+
* words are the reader's and carry across.
|
|
146
|
+
*=========================================*/ const [kind, setKind] = useState('campaign');
|
|
147
|
+
const [clausesByKind, setClausesByKind] = useState(NO_CLAUSES);
|
|
148
|
+
const [searchWords, setSearchWords] = useState([]);
|
|
149
|
+
const setKindClauses = useCallback((next)=>setClausesByKind((current)=>_extends({}, current, {
|
|
150
|
+
[kind]: next
|
|
151
|
+
})), [
|
|
152
|
+
kind
|
|
153
|
+
]);
|
|
154
|
+
const gridFilter = useListGridFilter({
|
|
155
|
+
clauses: clausesByKind[kind],
|
|
156
|
+
onChange: setKindClauses,
|
|
157
|
+
selectFields: [
|
|
158
|
+
'listIds',
|
|
159
|
+
'sitesLabel'
|
|
160
|
+
],
|
|
161
|
+
search: {
|
|
162
|
+
words: searchWords,
|
|
163
|
+
onChange: setSearchWords
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const containerDeclaration = campaignContainersListQuery(!hostId);
|
|
167
|
+
const singleDeclaration = campaignSingleSendsListQuery(!hostId);
|
|
168
|
+
const declaration = kind === 'campaign' ? containerDeclaration : singleDeclaration;
|
|
169
|
+
const filterHeaders = CAMPAIGN_FILTER_HEADERS[kind];
|
|
170
|
+
/*
|
|
171
|
+
* A site's campaigns are searched by the START of the name: a range beside
|
|
172
|
+
* the site's scope clause, the one array clause the rules can prove for a
|
|
173
|
+
* site collaborator (see `campaign-list-query.ts`). The org hub keeps the
|
|
174
|
+
* word search.
|
|
175
|
+
*/ const siteSearch = hostId && kind === 'campaign' ? campaignSiteSearchClause(gridFilter.searchWords) : null;
|
|
176
|
+
const containerPage = useListQuery({
|
|
177
|
+
collection: kind === 'campaign' ? campaignContainersCollection(firestore, orgId) : null,
|
|
178
|
+
declaration: containerDeclaration,
|
|
179
|
+
request: {
|
|
180
|
+
clauses: kind === 'campaign' ? siteSearch ? [
|
|
181
|
+
siteSearch,
|
|
182
|
+
...gridFilter.clauses
|
|
183
|
+
] : gridFilter.clauses : [],
|
|
184
|
+
search: hostId ? [] : gridFilter.searchWords,
|
|
185
|
+
base: campaignContainersScope(hostId)
|
|
186
|
+
},
|
|
187
|
+
deps: [
|
|
188
|
+
firestore,
|
|
189
|
+
orgId,
|
|
190
|
+
hostId,
|
|
191
|
+
kind
|
|
192
|
+
],
|
|
193
|
+
idField: '$id'
|
|
194
|
+
});
|
|
195
|
+
const singlePage = useListQuery({
|
|
196
|
+
collection: kind === 'single' ? campaignSendsCollection(firestore, orgId) : null,
|
|
197
|
+
declaration: singleDeclaration,
|
|
198
|
+
request: {
|
|
199
|
+
clauses: kind === 'single' ? gridFilter.clauses : [],
|
|
200
|
+
search: gridFilter.searchWords,
|
|
201
|
+
base: [
|
|
202
|
+
SINGLE_SENDS_BASE,
|
|
203
|
+
...campaignSendsScope(hostId)
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
deps: [
|
|
207
|
+
firestore,
|
|
208
|
+
orgId,
|
|
209
|
+
hostId,
|
|
210
|
+
kind
|
|
211
|
+
],
|
|
212
|
+
idField: '$id'
|
|
213
|
+
});
|
|
214
|
+
const page = kind === 'campaign' ? containerPage : singlePage;
|
|
215
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
216
|
+
/*
|
|
217
|
+
* THE FIGURES OF THE CAMPAIGNS ON THE PAGE ON SCREEN.
|
|
218
|
+
*
|
|
219
|
+
* A container stores no counters; its row sums the emails filed under it.
|
|
220
|
+
* Those are read for the page's campaigns only — `emailCampaignId in` the
|
|
221
|
+
* ids on screen, thirty to a read, and under a site the ones sent as it —
|
|
222
|
+
* and never used to filter: the page is already the query's answer. Past
|
|
223
|
+
* `ROLLUP_CEILING` emails a read stops, and the table says the figures
|
|
224
|
+
* are partial.
|
|
225
|
+
*/ const pageIds = useMemo(()=>kind === 'campaign' ? containerPage.rows.map((row)=>String(row.$id)) : [], [
|
|
226
|
+
kind,
|
|
227
|
+
containerPage.rows
|
|
228
|
+
]);
|
|
229
|
+
const rollupRead = (ids)=>()=>{
|
|
230
|
+
const sends = campaignSendsCollection(firestore, orgId);
|
|
231
|
+
if (!sends || !ids.length) return null;
|
|
232
|
+
return collectionCeiling(query(sends, where('emailCampaignId', 'in', ids), ...hostId ? [
|
|
233
|
+
where('hostId', '==', hostId)
|
|
234
|
+
] : []), ROLLUP_CEILING);
|
|
235
|
+
};
|
|
236
|
+
const firstIds = pageIds.slice(0, IN_LIMIT);
|
|
237
|
+
const restIds = pageIds.slice(IN_LIMIT, IN_LIMIT * 2);
|
|
238
|
+
const { data: firstSendDocs } = useFirestoreCollection(rollupRead(firstIds), [
|
|
155
239
|
firestore,
|
|
156
240
|
orgId,
|
|
157
|
-
hostId
|
|
241
|
+
hostId,
|
|
242
|
+
firstIds.join(',')
|
|
158
243
|
], {
|
|
159
244
|
idField: '$id'
|
|
160
245
|
});
|
|
161
|
-
const {
|
|
162
|
-
const { data: campaignDocs } = useFirestoreCollection(()=>{
|
|
163
|
-
const containers = campaignContainersQuery(firestore, orgId, hostId);
|
|
164
|
-
return containers ? collectionCeiling(containers, CONTAINER_CEILING) : null;
|
|
165
|
-
}, [
|
|
246
|
+
const { data: restSendDocs } = useFirestoreCollection(rollupRead(restIds), [
|
|
166
247
|
firestore,
|
|
167
248
|
orgId,
|
|
168
|
-
hostId
|
|
249
|
+
hostId,
|
|
250
|
+
restIds.join(',')
|
|
169
251
|
], {
|
|
170
252
|
idField: '$id'
|
|
171
253
|
});
|
|
172
|
-
const
|
|
254
|
+
const firstSends = ceilingedWindow(firstSendDocs, ROLLUP_CEILING);
|
|
255
|
+
const restSends = ceilingedWindow(restSendDocs, ROLLUP_CEILING);
|
|
256
|
+
const rollupTruncated = firstSends.truncated || restSends.truncated;
|
|
173
257
|
// Org email lists, so a row can name what it is aimed at rather than
|
|
174
258
|
// showing ids.
|
|
175
259
|
const { data: listDocs } = useFirestoreCollection(()=>orgId ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50)) : null, [
|
|
@@ -216,40 +300,66 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
216
300
|
}), [
|
|
217
301
|
listDocs
|
|
218
302
|
]);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
303
|
+
/*
|
|
304
|
+
* The page's rows, in the QUERY's order. `campaignListRows` builds each
|
|
305
|
+
* row — a container with its emails summed, or a single send as a
|
|
306
|
+
* campaign of one — and sorts by date, which is not this list's order, so
|
|
307
|
+
* the page is put back in the order the query returned it.
|
|
308
|
+
*/ const rows = useMemo(()=>{
|
|
309
|
+
const read = page.rows;
|
|
310
|
+
const order = new Map(read.map((row, at)=>[
|
|
311
|
+
String(row.$id),
|
|
312
|
+
at
|
|
224
313
|
]));
|
|
225
|
-
|
|
314
|
+
const built = kind === 'campaign' ? campaignListRows(read, [
|
|
315
|
+
...firstSends.rows,
|
|
316
|
+
...restSends.rows
|
|
317
|
+
], Date.now()) : campaignListRows([], read, Date.now());
|
|
318
|
+
const byId = new Map(read.map((row)=>[
|
|
319
|
+
String(row.$id),
|
|
320
|
+
row
|
|
321
|
+
]));
|
|
322
|
+
return built.filter((row)=>order.has(row.id)).sort((a, b)=>{
|
|
323
|
+
var _order_get, _order_get1;
|
|
324
|
+
return ((_order_get = order.get(a.id)) != null ? _order_get : 0) - ((_order_get1 = order.get(b.id)) != null ? _order_get1 : 0);
|
|
325
|
+
}).map((row)=>_extends({}, row, {
|
|
226
326
|
$id: row.id,
|
|
227
|
-
kind: row.legacy ? 'single' : 'campaign',
|
|
228
327
|
sitesLabel: orgMount ? campaignSitesLabel(orgMount, row, byId.get(row.id)) : ''
|
|
229
328
|
}));
|
|
230
329
|
}, [
|
|
231
|
-
|
|
232
|
-
|
|
330
|
+
kind,
|
|
331
|
+
page.rows,
|
|
332
|
+
firstSends.rows,
|
|
333
|
+
restSends.rows,
|
|
233
334
|
orgMount
|
|
234
335
|
]);
|
|
235
|
-
|
|
236
|
-
|
|
336
|
+
/*
|
|
337
|
+
* The choices the picked fields offer. A campaign's Sites are asked by
|
|
338
|
+
* scope token — `org` is placed on every site — and a single send's by
|
|
339
|
+
* the site it was sent as.
|
|
340
|
+
*/ const filterOptions = useMemo(()=>{
|
|
341
|
+
if (!orgMount) return {};
|
|
342
|
+
const sites = orgSiteOptions(orgMount);
|
|
343
|
+
return kind === 'campaign' ? {
|
|
237
344
|
listIds: listOptions,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
345
|
+
sitesLabel: [
|
|
346
|
+
{
|
|
347
|
+
value: 'org',
|
|
348
|
+
label: 'Every site'
|
|
349
|
+
},
|
|
350
|
+
...sites.map((site)=>({
|
|
351
|
+
value: `host:${site.value}`,
|
|
352
|
+
label: site.label
|
|
353
|
+
}))
|
|
354
|
+
]
|
|
355
|
+
} : {
|
|
356
|
+
sitesLabel: sites
|
|
357
|
+
};
|
|
358
|
+
}, [
|
|
359
|
+
kind,
|
|
360
|
+
listOptions,
|
|
244
361
|
orgMount
|
|
245
362
|
]);
|
|
246
|
-
const listFilter = useListRowsFilter({
|
|
247
|
-
rows,
|
|
248
|
-
fields: filterFields,
|
|
249
|
-
options: filterOptions,
|
|
250
|
-
headers: CAMPAIGN_FILTER_HEADERS,
|
|
251
|
-
search: CAMPAIGN_SEARCH_FIELDS
|
|
252
|
-
});
|
|
253
363
|
const [createError, setCreateError] = useState(null);
|
|
254
364
|
const [creating, setCreating] = useState(false);
|
|
255
365
|
/** The campaign a delete is in flight for, so its row menu can say so. */ const [deletingId, setDeletingId] = useState('');
|
|
@@ -286,11 +396,19 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
286
396
|
setCreating(true);
|
|
287
397
|
setCreateError(null);
|
|
288
398
|
const id = createResourceUid();
|
|
399
|
+
/*
|
|
400
|
+
* Where it is placed. From a site's hub, that site — the creator is
|
|
401
|
+
* standing on it, and a campaign that appeared on every sibling site
|
|
402
|
+
* would be a decision nobody made. From the org hub, the sites picked,
|
|
403
|
+
* and every site when none are.
|
|
404
|
+
*/ const visibleTo = campaignVisibleTo(hostId ? [
|
|
405
|
+
hostId
|
|
406
|
+
] : Array.isArray(values.siteIds) ? values.siteIds.map(String) : null);
|
|
289
407
|
try {
|
|
290
408
|
var _ref;
|
|
291
409
|
await setDoc(campaignContainerDoc(firestore, orgId, id), _extends({
|
|
292
410
|
name
|
|
293
|
-
}, Number.isFinite(startAtMs) && startAtMs !== null ? {
|
|
411
|
+
}, campaignContainerSearchFields(name), Number.isFinite(startAtMs) && startAtMs !== null ? {
|
|
294
412
|
startAtMs
|
|
295
413
|
} : {}, Number.isFinite(endAtMs) && endAtMs !== null ? {
|
|
296
414
|
endAtMs
|
|
@@ -299,14 +417,7 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
299
417
|
}, values.topicId ? {
|
|
300
418
|
topicId: String(values.topicId)
|
|
301
419
|
} : {}, {
|
|
302
|
-
|
|
303
|
-
* Where it is placed. From a site's hub, that site — the creator is
|
|
304
|
-
* standing on it, and a campaign that appeared on every sibling
|
|
305
|
-
* site would be a decision nobody made. From the org hub, the sites
|
|
306
|
-
* picked, and every site when none are.
|
|
307
|
-
*/ visibleTo: campaignVisibleTo(hostId ? [
|
|
308
|
-
hostId
|
|
309
|
-
] : Array.isArray(values.siteIds) ? values.siteIds.map(String) : null),
|
|
420
|
+
visibleTo,
|
|
310
421
|
createdAtMs: Date.now(),
|
|
311
422
|
createdBy: String((_ref = user == null ? void 0 : user.uid) != null ? _ref : '')
|
|
312
423
|
}));
|
|
@@ -403,7 +514,7 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
403
514
|
const columns = useMemo(()=>[
|
|
404
515
|
{
|
|
405
516
|
field: 'name',
|
|
406
|
-
headerName:
|
|
517
|
+
headerName: filterHeaders.name,
|
|
407
518
|
flex: 1,
|
|
408
519
|
minWidth: 180,
|
|
409
520
|
renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -661,8 +772,10 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
661
772
|
campaignHref,
|
|
662
773
|
deletingId,
|
|
663
774
|
handleDelete,
|
|
664
|
-
orgMount
|
|
775
|
+
orgMount,
|
|
776
|
+
filterHeaders
|
|
665
777
|
]);
|
|
778
|
+
/** Nothing of this kind stored at all — not a filter that matched nothing. */ const none = page.status !== 'loading' && !rows.length && !filtering && page.page === 0;
|
|
666
779
|
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
667
780
|
header: 'Campaigns',
|
|
668
781
|
help: pluginDocsHelp('emailCampaigns', {
|
|
@@ -687,35 +800,90 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
687
800
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
688
801
|
spacing: 1.5,
|
|
689
802
|
children: [
|
|
690
|
-
/*#__PURE__*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
803
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
804
|
+
exclusive: true,
|
|
805
|
+
size: "small",
|
|
806
|
+
value: kind,
|
|
807
|
+
onChange: (_event, next)=>{
|
|
808
|
+
if (next) setKind(next);
|
|
809
|
+
},
|
|
810
|
+
"aria-label": "Which rows the table lists",
|
|
811
|
+
sx: {
|
|
812
|
+
alignSelf: 'flex-start'
|
|
813
|
+
},
|
|
814
|
+
children: [
|
|
815
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
816
|
+
value: "campaign",
|
|
817
|
+
children: 'Campaigns'
|
|
818
|
+
}),
|
|
819
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
820
|
+
value: "single",
|
|
821
|
+
children: 'Single sends'
|
|
822
|
+
})
|
|
823
|
+
]
|
|
824
|
+
}),
|
|
825
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
826
|
+
fields: declaration.fields,
|
|
827
|
+
headers: filterHeaders,
|
|
828
|
+
clauses: gridFilter.clauses,
|
|
829
|
+
onChange: gridFilter.setClauses,
|
|
830
|
+
options: filterOptions
|
|
831
|
+
}),
|
|
832
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
833
|
+
refused: listQueryRefusals(page.plan.refused, {
|
|
834
|
+
fields: declaration.fields,
|
|
835
|
+
headers: filterHeaders,
|
|
836
|
+
options: filterOptions
|
|
837
|
+
}),
|
|
838
|
+
notices: siteSearch ? [
|
|
839
|
+
CAMPAIGN_SITE_SEARCH_NOTICE,
|
|
840
|
+
...page.plan.notices
|
|
841
|
+
] : page.plan.notices
|
|
842
|
+
}),
|
|
843
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
844
|
+
rows: rows,
|
|
845
|
+
columns: listFilterGridColumns(columns, declaration.fields, filterOptions, filterHeaders),
|
|
846
|
+
onOpen: (id)=>openCampaign(id),
|
|
847
|
+
loading: page.status === 'loading',
|
|
848
|
+
/*
|
|
849
|
+
* The panel and the search are the grid's; every clause and the
|
|
850
|
+
* search word are on the list's query (AGL-3321), and the grid
|
|
851
|
+
* neither filters nor sorts the page it is handed.
|
|
852
|
+
*/ filterMode: "server",
|
|
853
|
+
filterModel: gridFilter.filterModel,
|
|
854
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
855
|
+
quickFilter: true,
|
|
856
|
+
disableColumnSorting: true,
|
|
857
|
+
hideFooter: true,
|
|
696
858
|
initialState: {
|
|
697
859
|
columns: {
|
|
698
|
-
columnVisibilityModel:
|
|
699
|
-
kind: false
|
|
700
|
-
}
|
|
860
|
+
columnVisibilityModel: hiddenFilterVisibility(declaration.fields, columns.map((column)=>column.field))
|
|
701
861
|
}
|
|
702
862
|
},
|
|
703
|
-
noRowsLabel:
|
|
704
|
-
noRowsDescription:
|
|
705
|
-
noRowsAction:
|
|
863
|
+
noRowsLabel: none ? kind === 'campaign' ? 'No campaigns yet' : 'No single sends' : kind === 'campaign' ? 'No campaigns match these filters' : 'No single sends match these filters',
|
|
864
|
+
noRowsDescription: none ? kind === 'campaign' ? 'A campaign groups the emails you send to a set of lists.' : 'An email filed under no campaign is listed here.' : undefined,
|
|
865
|
+
noRowsAction: none && kind === 'campaign' ? /*#__PURE__*/ _jsx(Button, {
|
|
706
866
|
variant: "contained",
|
|
707
867
|
onClick: ()=>setCreateOpen(true),
|
|
708
868
|
children: 'Create campaign'
|
|
709
|
-
})
|
|
710
|
-
})
|
|
869
|
+
}) : undefined
|
|
870
|
+
}),
|
|
871
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
872
|
+
page: page.page,
|
|
873
|
+
pageSize: page.pageSize,
|
|
874
|
+
rowCount: rows.length,
|
|
875
|
+
hasMore: page.hasMore,
|
|
876
|
+
onPageChange: page.setPage,
|
|
877
|
+
onPageSizeChange: page.setPageSize
|
|
878
|
+
}),
|
|
711
879
|
/*#__PURE__*/ _jsx(Typography, {
|
|
712
880
|
variant: "caption",
|
|
713
881
|
color: "text.secondary",
|
|
714
882
|
children: 'These figures measure each campaign’s mail. What a campaign ' + 'caused — the form submissions, leads, contacts and bookings ' + 'credited to it — and the pages it sent people to are on the ' + 'campaign’s own page.'
|
|
715
883
|
}),
|
|
716
|
-
|
|
884
|
+
rollupTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
717
885
|
severity: "info",
|
|
718
|
-
children: `
|
|
886
|
+
children: `The campaigns on this page hold more than ${ROLLUP_CEILING} ` + 'emails between them, so their figures count the first ' + `${ROLLUP_CEILING} read.`
|
|
719
887
|
}) : null
|
|
720
888
|
]
|
|
721
889
|
}),
|