@aglyn/plugins-marketing 1.0.0-beta.198 → 1.0.0-beta.200
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-detail-card.js +106 -36
- package/src/lib/components/campaign-detail-card.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 +283 -125
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/emails-list-card.d.ts +11 -15
- package/src/lib/components/emails-list-card.js +139 -142
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +107 -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/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-manage.d.ts +1 -1
- package/src/lib/server/campaign-manage.js +30 -18
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +7 -0
- package/src/lib/server/campaign-send.js +17 -5
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +3 -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
|
@@ -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,21 @@ 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
|
-
_extends({}, inMemoryListField('listIds', 'select'), {
|
|
67
|
-
tokensPath: 'listIds',
|
|
68
|
-
verbatimTokens: true
|
|
69
|
-
}),
|
|
70
|
-
inMemoryListField('kind', 'select'),
|
|
71
|
-
inMemoryListField('sitesLabel', 'text')
|
|
72
|
-
];
|
|
73
|
-
const CAMPAIGN_FILTER_HEADERS = {
|
|
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 = {
|
|
74
72
|
name: 'Campaign',
|
|
75
|
-
windowState: 'Window',
|
|
76
73
|
listIds: 'Lists',
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
sitesLabel: 'Sites',
|
|
75
|
+
createdAtMs: 'Created'
|
|
76
|
+
};
|
|
77
|
+
const NO_CLAUSES = {
|
|
78
|
+
campaign: [],
|
|
79
|
+
single: []
|
|
79
80
|
};
|
|
80
|
-
const CAMPAIGN_KIND_OPTIONS = [
|
|
81
|
-
{
|
|
82
|
-
value: 'campaign',
|
|
83
|
-
label: 'Campaign'
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
value: 'single',
|
|
87
|
-
label: 'Single send'
|
|
88
|
-
}
|
|
89
|
-
];
|
|
90
|
-
const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
91
|
-
value: state,
|
|
92
|
-
label: WINDOW_LABEL[state]
|
|
93
|
-
}));
|
|
94
|
-
/** What the quick search reads on a campaign row. */ const CAMPAIGN_SEARCH_FIELDS = [
|
|
95
|
-
'name',
|
|
96
|
-
'sitesLabel'
|
|
97
|
-
];
|
|
98
81
|
/**
|
|
99
82
|
* THE CAMPAIGNS LIST.
|
|
100
83
|
*
|
|
@@ -103,12 +86,14 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
103
86
|
* messages. Composing is not here: a message belongs to a campaign, and the
|
|
104
87
|
* campaign's own page is where one is written.
|
|
105
88
|
*
|
|
106
|
-
* The rows come from two collections
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
89
|
+
* The rows come from two collections. `emailCampaigns` holds the
|
|
90
|
+
* containers. `campaigns` holds every send, including thousands written
|
|
91
|
+
* before containers existed; those carry no container (`emailCampaignId`
|
|
92
|
+
* null), and `campaignListRows` presents each as a campaign of one rather
|
|
93
|
+
* than dropping it. The two are two queries, so the table lists one kind at a
|
|
94
|
+
* time — Campaigns or Single sends — each paged by its own query (AGL-3321).
|
|
95
|
+
* No send is re-filed, so no unsubscribe link and no pasted report URL stops
|
|
96
|
+
* resolving.
|
|
112
97
|
*
|
|
113
98
|
* ## At two levels
|
|
114
99
|
*
|
|
@@ -134,42 +119,132 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
134
119
|
const { orgId } = useMarketingOrgId(hostId);
|
|
135
120
|
// A container is the org's, so deleting one from the org hub names the org.
|
|
136
121
|
const manageApi = useCampaignManageApi(hostId, orgId);
|
|
137
|
-
|
|
138
|
-
*
|
|
122
|
+
/*==========================================
|
|
123
|
+
* TWO KINDS OF ROW, EACH ITS OWN QUERY (AGL-3321).
|
|
139
124
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
125
|
+
* A campaign is a CONTAINER (`emailCampaigns`); a single send is an email
|
|
126
|
+
* filed under none (`campaigns` where `emailCampaignId == null`). The two
|
|
127
|
+
* live in different collections, and one Firestore query reads one
|
|
128
|
+
* collection — so a table that merged them could page neither, and every
|
|
129
|
+
* filter would be matched over whatever window of each it happened to
|
|
130
|
+
* hold. The toggle above the table picks the kind, and the table shows a
|
|
131
|
+
* page of that kind's query: newest first on `createdAtMs`, every clause
|
|
132
|
+
* and the search word on the query, paged by it. Same table, same
|
|
133
|
+
* columns; a single send still reads as a campaign of one.
|
|
144
134
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
135
|
+
* Each kind keeps its own clauses (their Sites values differ: a campaign
|
|
136
|
+
* is asked by scope token, a send by the site it was sent as); the search
|
|
137
|
+
* words are the reader's and carry across.
|
|
138
|
+
*=========================================*/ const [kind, setKind] = useState('campaign');
|
|
139
|
+
const [clausesByKind, setClausesByKind] = useState(NO_CLAUSES);
|
|
140
|
+
const [searchWords, setSearchWords] = useState([]);
|
|
141
|
+
const setKindClauses = useCallback((next)=>setClausesByKind((current)=>_extends({}, current, {
|
|
142
|
+
[kind]: next
|
|
143
|
+
})), [
|
|
144
|
+
kind
|
|
145
|
+
]);
|
|
146
|
+
const gridFilter = useListGridFilter({
|
|
147
|
+
clauses: clausesByKind[kind],
|
|
148
|
+
onChange: setKindClauses,
|
|
149
|
+
selectFields: [
|
|
150
|
+
'listIds',
|
|
151
|
+
'sitesLabel'
|
|
152
|
+
],
|
|
153
|
+
search: {
|
|
154
|
+
words: searchWords,
|
|
155
|
+
onChange: setSearchWords
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const containerDeclaration = campaignContainersListQuery(!hostId);
|
|
159
|
+
const singleDeclaration = campaignSingleSendsListQuery(!hostId);
|
|
160
|
+
const declaration = kind === 'campaign' ? containerDeclaration : singleDeclaration;
|
|
161
|
+
/*
|
|
162
|
+
* A site's campaigns are searched by the START of the name: a range beside
|
|
163
|
+
* the site's scope clause, the one array clause the rules can prove for a
|
|
164
|
+
* site collaborator (see `campaign-list-query.ts`). The org hub keeps the
|
|
165
|
+
* word search.
|
|
166
|
+
*/ const siteSearch = hostId && kind === 'campaign' ? campaignSiteSearchClause(gridFilter.searchWords) : null;
|
|
167
|
+
const containerPage = useListQuery({
|
|
168
|
+
collection: kind === 'campaign' ? campaignContainersCollection(firestore, orgId) : null,
|
|
169
|
+
declaration: containerDeclaration,
|
|
170
|
+
request: {
|
|
171
|
+
clauses: kind === 'campaign' ? siteSearch ? [
|
|
172
|
+
siteSearch,
|
|
173
|
+
...gridFilter.clauses
|
|
174
|
+
] : gridFilter.clauses : [],
|
|
175
|
+
search: hostId ? [] : gridFilter.searchWords,
|
|
176
|
+
base: campaignContainersScope(hostId)
|
|
177
|
+
},
|
|
178
|
+
deps: [
|
|
179
|
+
firestore,
|
|
180
|
+
orgId,
|
|
181
|
+
hostId,
|
|
182
|
+
kind
|
|
183
|
+
],
|
|
184
|
+
idField: '$id'
|
|
185
|
+
});
|
|
186
|
+
const singlePage = useListQuery({
|
|
187
|
+
collection: kind === 'single' ? campaignSendsCollection(firestore, orgId) : null,
|
|
188
|
+
declaration: singleDeclaration,
|
|
189
|
+
request: {
|
|
190
|
+
clauses: kind === 'single' ? gridFilter.clauses : [],
|
|
191
|
+
search: gridFilter.searchWords,
|
|
192
|
+
base: [
|
|
193
|
+
SINGLE_SENDS_BASE,
|
|
194
|
+
...campaignSendsScope(hostId)
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
deps: [
|
|
198
|
+
firestore,
|
|
199
|
+
orgId,
|
|
200
|
+
hostId,
|
|
201
|
+
kind
|
|
202
|
+
],
|
|
203
|
+
idField: '$id'
|
|
204
|
+
});
|
|
205
|
+
const page = kind === 'campaign' ? containerPage : singlePage;
|
|
206
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
207
|
+
/*
|
|
208
|
+
* THE FIGURES OF THE CAMPAIGNS ON THE PAGE ON SCREEN.
|
|
209
|
+
*
|
|
210
|
+
* A container stores no counters; its row sums the emails filed under it.
|
|
211
|
+
* Those are read for the page's campaigns only — `emailCampaignId in` the
|
|
212
|
+
* ids on screen, thirty to a read, and under a site the ones sent as it —
|
|
213
|
+
* and never used to filter: the page is already the query's answer. Past
|
|
214
|
+
* `ROLLUP_CEILING` emails a read stops, and the table says the figures
|
|
215
|
+
* are partial.
|
|
216
|
+
*/ const pageIds = useMemo(()=>kind === 'campaign' ? containerPage.rows.map((row)=>String(row.$id)) : [], [
|
|
217
|
+
kind,
|
|
218
|
+
containerPage.rows
|
|
219
|
+
]);
|
|
220
|
+
const rollupRead = (ids)=>()=>{
|
|
221
|
+
const sends = campaignSendsCollection(firestore, orgId);
|
|
222
|
+
if (!sends || !ids.length) return null;
|
|
223
|
+
return collectionCeiling(query(sends, where('emailCampaignId', 'in', ids), ...hostId ? [
|
|
224
|
+
where('hostId', '==', hostId)
|
|
225
|
+
] : []), ROLLUP_CEILING);
|
|
226
|
+
};
|
|
227
|
+
const firstIds = pageIds.slice(0, IN_LIMIT);
|
|
228
|
+
const restIds = pageIds.slice(IN_LIMIT, IN_LIMIT * 2);
|
|
229
|
+
const { data: firstSendDocs } = useFirestoreCollection(rollupRead(firstIds), [
|
|
155
230
|
firestore,
|
|
156
231
|
orgId,
|
|
157
|
-
hostId
|
|
232
|
+
hostId,
|
|
233
|
+
firstIds.join(',')
|
|
158
234
|
], {
|
|
159
235
|
idField: '$id'
|
|
160
236
|
});
|
|
161
|
-
const {
|
|
162
|
-
const { data: campaignDocs } = useFirestoreCollection(()=>{
|
|
163
|
-
const containers = campaignContainersQuery(firestore, orgId, hostId);
|
|
164
|
-
return containers ? collectionCeiling(containers, CONTAINER_CEILING) : null;
|
|
165
|
-
}, [
|
|
237
|
+
const { data: restSendDocs } = useFirestoreCollection(rollupRead(restIds), [
|
|
166
238
|
firestore,
|
|
167
239
|
orgId,
|
|
168
|
-
hostId
|
|
240
|
+
hostId,
|
|
241
|
+
restIds.join(',')
|
|
169
242
|
], {
|
|
170
243
|
idField: '$id'
|
|
171
244
|
});
|
|
172
|
-
const
|
|
245
|
+
const firstSends = ceilingedWindow(firstSendDocs, ROLLUP_CEILING);
|
|
246
|
+
const restSends = ceilingedWindow(restSendDocs, ROLLUP_CEILING);
|
|
247
|
+
const rollupTruncated = firstSends.truncated || restSends.truncated;
|
|
173
248
|
// Org email lists, so a row can name what it is aimed at rather than
|
|
174
249
|
// showing ids.
|
|
175
250
|
const { data: listDocs } = useFirestoreCollection(()=>orgId ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50)) : null, [
|
|
@@ -216,40 +291,66 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
216
291
|
}), [
|
|
217
292
|
listDocs
|
|
218
293
|
]);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
/*
|
|
295
|
+
* The page's rows, in the QUERY's order. `campaignListRows` builds each
|
|
296
|
+
* row — a container with its emails summed, or a single send as a
|
|
297
|
+
* campaign of one — and sorts by date, which is not this list's order, so
|
|
298
|
+
* the page is put back in the order the query returned it.
|
|
299
|
+
*/ const rows = useMemo(()=>{
|
|
300
|
+
const read = page.rows;
|
|
301
|
+
const order = new Map(read.map((row, at)=>[
|
|
302
|
+
String(row.$id),
|
|
303
|
+
at
|
|
304
|
+
]));
|
|
305
|
+
const built = kind === 'campaign' ? campaignListRows(read, [
|
|
306
|
+
...firstSends.rows,
|
|
307
|
+
...restSends.rows
|
|
308
|
+
], Date.now()) : campaignListRows([], read, Date.now());
|
|
309
|
+
const byId = new Map(read.map((row)=>[
|
|
310
|
+
String(row.$id),
|
|
311
|
+
row
|
|
224
312
|
]));
|
|
225
|
-
return
|
|
313
|
+
return built.filter((row)=>order.has(row.id)).sort((a, b)=>{
|
|
314
|
+
var _order_get, _order_get1;
|
|
315
|
+
return ((_order_get = order.get(a.id)) != null ? _order_get : 0) - ((_order_get1 = order.get(b.id)) != null ? _order_get1 : 0);
|
|
316
|
+
}).map((row)=>_extends({}, row, {
|
|
226
317
|
$id: row.id,
|
|
227
|
-
kind: row.legacy ? 'single' : 'campaign',
|
|
228
318
|
sitesLabel: orgMount ? campaignSitesLabel(orgMount, row, byId.get(row.id)) : ''
|
|
229
319
|
}));
|
|
230
320
|
}, [
|
|
231
|
-
|
|
232
|
-
|
|
321
|
+
kind,
|
|
322
|
+
page.rows,
|
|
323
|
+
firstSends.rows,
|
|
324
|
+
restSends.rows,
|
|
233
325
|
orgMount
|
|
234
326
|
]);
|
|
235
|
-
|
|
236
|
-
|
|
327
|
+
/*
|
|
328
|
+
* The choices the picked fields offer. A campaign's Sites are asked by
|
|
329
|
+
* scope token — `org` is placed on every site — and a single send's by
|
|
330
|
+
* the site it was sent as.
|
|
331
|
+
*/ const filterOptions = useMemo(()=>{
|
|
332
|
+
if (!orgMount) return {};
|
|
333
|
+
const sites = orgSiteOptions(orgMount);
|
|
334
|
+
return kind === 'campaign' ? {
|
|
237
335
|
listIds: listOptions,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
336
|
+
sitesLabel: [
|
|
337
|
+
{
|
|
338
|
+
value: 'org',
|
|
339
|
+
label: 'Every site'
|
|
340
|
+
},
|
|
341
|
+
...sites.map((site)=>({
|
|
342
|
+
value: `host:${site.value}`,
|
|
343
|
+
label: site.label
|
|
344
|
+
}))
|
|
345
|
+
]
|
|
346
|
+
} : {
|
|
347
|
+
sitesLabel: sites
|
|
348
|
+
};
|
|
349
|
+
}, [
|
|
350
|
+
kind,
|
|
351
|
+
listOptions,
|
|
244
352
|
orgMount
|
|
245
353
|
]);
|
|
246
|
-
const listFilter = useListRowsFilter({
|
|
247
|
-
rows,
|
|
248
|
-
fields: filterFields,
|
|
249
|
-
options: filterOptions,
|
|
250
|
-
headers: CAMPAIGN_FILTER_HEADERS,
|
|
251
|
-
search: CAMPAIGN_SEARCH_FIELDS
|
|
252
|
-
});
|
|
253
354
|
const [createError, setCreateError] = useState(null);
|
|
254
355
|
const [creating, setCreating] = useState(false);
|
|
255
356
|
/** The campaign a delete is in flight for, so its row menu can say so. */ const [deletingId, setDeletingId] = useState('');
|
|
@@ -286,11 +387,19 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
286
387
|
setCreating(true);
|
|
287
388
|
setCreateError(null);
|
|
288
389
|
const id = createResourceUid();
|
|
390
|
+
/*
|
|
391
|
+
* Where it is placed. From a site's hub, that site — the creator is
|
|
392
|
+
* standing on it, and a campaign that appeared on every sibling site
|
|
393
|
+
* would be a decision nobody made. From the org hub, the sites picked,
|
|
394
|
+
* and every site when none are.
|
|
395
|
+
*/ const visibleTo = campaignVisibleTo(hostId ? [
|
|
396
|
+
hostId
|
|
397
|
+
] : Array.isArray(values.siteIds) ? values.siteIds.map(String) : null);
|
|
289
398
|
try {
|
|
290
399
|
var _ref;
|
|
291
400
|
await setDoc(campaignContainerDoc(firestore, orgId, id), _extends({
|
|
292
401
|
name
|
|
293
|
-
}, Number.isFinite(startAtMs) && startAtMs !== null ? {
|
|
402
|
+
}, campaignContainerSearchFields(name), Number.isFinite(startAtMs) && startAtMs !== null ? {
|
|
294
403
|
startAtMs
|
|
295
404
|
} : {}, Number.isFinite(endAtMs) && endAtMs !== null ? {
|
|
296
405
|
endAtMs
|
|
@@ -299,14 +408,7 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
299
408
|
}, values.topicId ? {
|
|
300
409
|
topicId: String(values.topicId)
|
|
301
410
|
} : {}, {
|
|
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),
|
|
411
|
+
visibleTo,
|
|
310
412
|
createdAtMs: Date.now(),
|
|
311
413
|
createdBy: String((_ref = user == null ? void 0 : user.uid) != null ? _ref : '')
|
|
312
414
|
}));
|
|
@@ -663,6 +765,7 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
663
765
|
handleDelete,
|
|
664
766
|
orgMount
|
|
665
767
|
]);
|
|
768
|
+
/** 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
769
|
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
667
770
|
header: 'Campaigns',
|
|
668
771
|
help: pluginDocsHelp('emailCampaigns', {
|
|
@@ -687,35 +790,90 @@ const CAMPAIGN_WINDOW_OPTIONS = Object.keys(WINDOW_LABEL).map((state)=>({
|
|
|
687
790
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
688
791
|
spacing: 1.5,
|
|
689
792
|
children: [
|
|
690
|
-
/*#__PURE__*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
793
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
794
|
+
exclusive: true,
|
|
795
|
+
size: "small",
|
|
796
|
+
value: kind,
|
|
797
|
+
onChange: (_event, next)=>{
|
|
798
|
+
if (next) setKind(next);
|
|
799
|
+
},
|
|
800
|
+
"aria-label": "Which rows the table lists",
|
|
801
|
+
sx: {
|
|
802
|
+
alignSelf: 'flex-start'
|
|
803
|
+
},
|
|
804
|
+
children: [
|
|
805
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
806
|
+
value: "campaign",
|
|
807
|
+
children: 'Campaigns'
|
|
808
|
+
}),
|
|
809
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
810
|
+
value: "single",
|
|
811
|
+
children: 'Single sends'
|
|
812
|
+
})
|
|
813
|
+
]
|
|
814
|
+
}),
|
|
815
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
816
|
+
fields: declaration.fields,
|
|
817
|
+
headers: CAMPAIGN_FILTER_HEADERS,
|
|
818
|
+
clauses: gridFilter.clauses,
|
|
819
|
+
onChange: gridFilter.setClauses,
|
|
820
|
+
options: filterOptions
|
|
821
|
+
}),
|
|
822
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
823
|
+
refused: listQueryRefusals(page.plan.refused, {
|
|
824
|
+
fields: declaration.fields,
|
|
825
|
+
headers: CAMPAIGN_FILTER_HEADERS,
|
|
826
|
+
options: filterOptions
|
|
827
|
+
}),
|
|
828
|
+
notices: siteSearch ? [
|
|
829
|
+
CAMPAIGN_SITE_SEARCH_NOTICE,
|
|
830
|
+
...page.plan.notices
|
|
831
|
+
] : page.plan.notices
|
|
832
|
+
}),
|
|
833
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
834
|
+
rows: rows,
|
|
835
|
+
columns: listFilterGridColumns(columns, declaration.fields, filterOptions, CAMPAIGN_FILTER_HEADERS),
|
|
836
|
+
onOpen: (id)=>openCampaign(id),
|
|
837
|
+
loading: page.status === 'loading',
|
|
838
|
+
/*
|
|
839
|
+
* The panel and the search are the grid's; every clause and the
|
|
840
|
+
* search word are on the list's query (AGL-3321), and the grid
|
|
841
|
+
* neither filters nor sorts the page it is handed.
|
|
842
|
+
*/ filterMode: "server",
|
|
843
|
+
filterModel: gridFilter.filterModel,
|
|
844
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
845
|
+
quickFilter: true,
|
|
846
|
+
disableColumnSorting: true,
|
|
847
|
+
hideFooter: true,
|
|
696
848
|
initialState: {
|
|
697
849
|
columns: {
|
|
698
|
-
columnVisibilityModel:
|
|
699
|
-
kind: false
|
|
700
|
-
}
|
|
850
|
+
columnVisibilityModel: hiddenFilterVisibility(declaration.fields, columns.map((column)=>column.field))
|
|
701
851
|
}
|
|
702
852
|
},
|
|
703
|
-
noRowsLabel:
|
|
704
|
-
noRowsDescription:
|
|
705
|
-
noRowsAction:
|
|
853
|
+
noRowsLabel: none ? kind === 'campaign' ? 'No campaigns yet' : 'No single sends' : kind === 'campaign' ? 'No campaigns match these filters' : 'No single sends match these filters',
|
|
854
|
+
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,
|
|
855
|
+
noRowsAction: none && kind === 'campaign' ? /*#__PURE__*/ _jsx(Button, {
|
|
706
856
|
variant: "contained",
|
|
707
857
|
onClick: ()=>setCreateOpen(true),
|
|
708
858
|
children: 'Create campaign'
|
|
709
|
-
})
|
|
710
|
-
})
|
|
859
|
+
}) : undefined
|
|
860
|
+
}),
|
|
861
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
862
|
+
page: page.page,
|
|
863
|
+
pageSize: page.pageSize,
|
|
864
|
+
rowCount: rows.length,
|
|
865
|
+
hasMore: page.hasMore,
|
|
866
|
+
onPageChange: page.setPage,
|
|
867
|
+
onPageSizeChange: page.setPageSize
|
|
868
|
+
}),
|
|
711
869
|
/*#__PURE__*/ _jsx(Typography, {
|
|
712
870
|
variant: "caption",
|
|
713
871
|
color: "text.secondary",
|
|
714
872
|
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
873
|
}),
|
|
716
|
-
|
|
874
|
+
rollupTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
717
875
|
severity: "info",
|
|
718
|
-
children: `
|
|
876
|
+
children: `The campaigns on this page hold more than ${ROLLUP_CEILING} ` + 'emails between them, so their figures count the first ' + `${ROLLUP_CEILING} read.`
|
|
719
877
|
}) : null
|
|
720
878
|
]
|
|
721
879
|
}),
|