@aglyn/plugins-marketing 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { buildRoute, createResourceUid, pluginDocsHelp, Route } from "@aglyn/aglyn";
|
|
20
|
+
/*
|
|
21
|
+
* The MODULE, not the barrel, for the two PURE helpers — a spec that mocks
|
|
22
|
+
* `@aglyn/tenant-feature-instance` wholesale to stage its Firestore hooks
|
|
23
|
+
* would otherwise lose them, and neither is a hook.
|
|
24
|
+
*/ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
25
|
+
import { mdiDeleteOutline, mdiEyeOutline } from "@aglyn/shared-data-mdi";
|
|
26
|
+
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
27
|
+
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
28
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
29
|
+
import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
|
|
30
|
+
import { collection, doc, limit, query, setDoc } from "firebase/firestore";
|
|
31
|
+
import { useCallback, useMemo, useState } from "react";
|
|
32
|
+
import { useRouter } from "next/navigation";
|
|
33
|
+
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, useOrgDataScope, useUser } from "@aglyn/tenant-feature-instance";
|
|
34
|
+
import { campaignListRows } from "@aglyn/shared-ui-email-campaigns/model";
|
|
35
|
+
import { CreateArtifactDrawer } from "@aglyn/shared-ui-jsx-forms";
|
|
36
|
+
import { useCampaignManageApi } from "./use-campaign-send-api.js";
|
|
37
|
+
import { useCampaignTopicOptions } from "./use-campaign-topic-options.js";
|
|
38
|
+
/**
|
|
39
|
+
* How many sends the list reads.
|
|
40
|
+
*
|
|
41
|
+
* A CEILING, not a page size — see the query, which explains why this list
|
|
42
|
+
* cannot be paged by the server until a send carries one date field every
|
|
43
|
+
* writer stamps.
|
|
44
|
+
*/ const CAMPAIGN_CEILING = 30;
|
|
45
|
+
/** How many campaign containers the list reads. */ const CONTAINER_CEILING = 50;
|
|
46
|
+
const formatDay = (ms)=>ms === null ? '' : new Date(ms).toLocaleDateString();
|
|
47
|
+
/** A rolled-up figure, or an em dash where nothing recorded one. */ const figure = (value)=>value.value === null ? '—' : value.value.toLocaleString();
|
|
48
|
+
const WINDOW_LABEL = {
|
|
49
|
+
undated: 'No dates',
|
|
50
|
+
upcoming: 'Upcoming',
|
|
51
|
+
running: 'Running',
|
|
52
|
+
ended: 'Ended'
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* THE CAMPAIGNS LIST.
|
|
56
|
+
*
|
|
57
|
+
* A campaign is a container — a name, a window, the lists it is aimed at, and
|
|
58
|
+
* the emails sent inside it — so this is a table of campaigns and not of
|
|
59
|
+
* messages. Composing is not here: a message belongs to a campaign, and the
|
|
60
|
+
* campaign's own page is where one is written.
|
|
61
|
+
*
|
|
62
|
+
* The rows come from two collections, and the second is why this reads
|
|
63
|
+
* without a migration. `emailCampaigns` holds the containers.
|
|
64
|
+
* `campaigns` holds every send, including thousands written before containers
|
|
65
|
+
* existed; those carry no container id, and `campaignListRows` presents each
|
|
66
|
+
* as a campaign of one rather than dropping it. Nothing is rewritten, so no
|
|
67
|
+
* unsubscribe link and no pasted report URL stops resolving.
|
|
68
|
+
*/ export function HostCampaignsCard(props) {
|
|
69
|
+
const { hostId, basePath } = props;
|
|
70
|
+
const firestore = useFirestore();
|
|
71
|
+
const router = useRouter();
|
|
72
|
+
const { orgSlug, subdomain } = useConsoleHostRoute(hostId);
|
|
73
|
+
const hubPath = basePath != null ? basePath : orgSlug && subdomain ? buildRoute(Route.HOST_PLUGIN, {
|
|
74
|
+
orgSlug,
|
|
75
|
+
host: subdomain,
|
|
76
|
+
pluginSlug: 'marketing'
|
|
77
|
+
}) : null;
|
|
78
|
+
const { data: user } = useUser();
|
|
79
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
80
|
+
const { confirm } = useConfirmationContext();
|
|
81
|
+
const manageApi = useCampaignManageApi(hostId);
|
|
82
|
+
const { scope: dataScope } = useOrgDataScope({
|
|
83
|
+
hostId
|
|
84
|
+
});
|
|
85
|
+
/*
|
|
86
|
+
* ORDERED AND CEILINGED, and not orderable on any DATE.
|
|
87
|
+
*
|
|
88
|
+
* No field here is on every send: a sent one is written `{status:'sent',
|
|
89
|
+
* sentAt}` and a scheduled one `{status:'scheduled', sendAtMs}`, by the two
|
|
90
|
+
* branches of `campaign-send.ts`, and there is no `createdAt` at all.
|
|
91
|
+
* `orderBy` on either would not mis-sort the list, it would DROP half of it.
|
|
92
|
+
*
|
|
93
|
+
* A bare `limit(30)` is answered in DOCUMENT-ID order, so the window is
|
|
94
|
+
* thirty sends chosen by id and then sorted by date, which reads as the most
|
|
95
|
+
* recent thirty and is not. `collectionCeiling` returns that same thirty —
|
|
96
|
+
* document-id order is what the bare cap already gave — but it says so,
|
|
97
|
+
* which is what stops the next edit reaching for `sentAt`, and it probes one
|
|
98
|
+
* past the ceiling so the reader is told the history is longer.
|
|
99
|
+
*/ const { data: sendDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'campaigns'), CAMPAIGN_CEILING), [
|
|
100
|
+
firestore,
|
|
101
|
+
hostId
|
|
102
|
+
], {
|
|
103
|
+
idField: '$id'
|
|
104
|
+
});
|
|
105
|
+
const { rows: readSends, truncated: sendsTruncated } = ceilingedWindow(sendDocs, CAMPAIGN_CEILING);
|
|
106
|
+
const { data: campaignDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'emailCampaigns'), CONTAINER_CEILING), [
|
|
107
|
+
firestore,
|
|
108
|
+
hostId
|
|
109
|
+
], {
|
|
110
|
+
idField: '$id'
|
|
111
|
+
});
|
|
112
|
+
const { rows: readCampaigns, truncated: campaignsTruncated } = ceilingedWindow(campaignDocs, CONTAINER_CEILING);
|
|
113
|
+
// Org email lists, so a row can name what it is aimed at rather than
|
|
114
|
+
// showing ids.
|
|
115
|
+
const { data: listDocs } = useFirestoreCollection(()=>dataScope ? query(collection(firestore, dataScope[0], dataScope[1], 'lists'), limit(50)) : null, [
|
|
116
|
+
firestore,
|
|
117
|
+
dataScope
|
|
118
|
+
], {
|
|
119
|
+
idField: '$id'
|
|
120
|
+
});
|
|
121
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
122
|
+
/*
|
|
123
|
+
* The org's topic catalog, read only while the CREATE DRAWER is open.
|
|
124
|
+
*
|
|
125
|
+
* It fills one picker in that drawer and is drawn nowhere in the table, so
|
|
126
|
+
* a reader who came to look at their campaigns must not pay for it — the
|
|
127
|
+
* same rule the emails list already applies to the campaigns it offers its
|
|
128
|
+
* own drawer, and the one the audiences section applies to a list's
|
|
129
|
+
* members. It is the largest read on this section at 200 documents, and it
|
|
130
|
+
* is the only one nothing on screen needs.
|
|
131
|
+
*/ const { topics, source: topicSource } = useCampaignTopicOptions(hostId, {
|
|
132
|
+
enabled: createOpen
|
|
133
|
+
});
|
|
134
|
+
const topicOptions = useMemo(()=>topics.map((topic)=>({
|
|
135
|
+
value: topic.id,
|
|
136
|
+
label: topic.name
|
|
137
|
+
})), [
|
|
138
|
+
topics
|
|
139
|
+
]);
|
|
140
|
+
const listNames = useMemo(()=>{
|
|
141
|
+
const names = new Map();
|
|
142
|
+
for (const list of listDocs != null ? listDocs : []){
|
|
143
|
+
var _list_name;
|
|
144
|
+
names.set(String(list.$id), String((_list_name = list.name) != null ? _list_name : list.$id));
|
|
145
|
+
}
|
|
146
|
+
return names;
|
|
147
|
+
}, [
|
|
148
|
+
listDocs
|
|
149
|
+
]);
|
|
150
|
+
const listOptions = useMemo(()=>(listDocs != null ? listDocs : []).map((list)=>{
|
|
151
|
+
var _list_name;
|
|
152
|
+
return {
|
|
153
|
+
value: String(list.$id),
|
|
154
|
+
label: String((_list_name = list.name) != null ? _list_name : list.$id)
|
|
155
|
+
};
|
|
156
|
+
}), [
|
|
157
|
+
listDocs
|
|
158
|
+
]);
|
|
159
|
+
const rows = useMemo(()=>{
|
|
160
|
+
const campaigns = readCampaigns.filter((campaign)=>!campaign.deletedAt);
|
|
161
|
+
return campaignListRows(campaigns, readSends, Date.now()).map((row)=>_extends({}, row, {
|
|
162
|
+
$id: row.id
|
|
163
|
+
}));
|
|
164
|
+
}, [
|
|
165
|
+
readCampaigns,
|
|
166
|
+
readSends
|
|
167
|
+
]);
|
|
168
|
+
const [createError, setCreateError] = useState(null);
|
|
169
|
+
const [creating, setCreating] = useState(false);
|
|
170
|
+
/** The campaign a delete is in flight for, so its row menu can say so. */ const [deletingId, setDeletingId] = useState('');
|
|
171
|
+
const openCampaign = useCallback((id)=>{
|
|
172
|
+
if (hubPath) void router.push(`${hubPath}/campaigns/${id}`);
|
|
173
|
+
}, [
|
|
174
|
+
hubPath,
|
|
175
|
+
router
|
|
176
|
+
]);
|
|
177
|
+
const handleCreate = useCallback(async (values)=>{
|
|
178
|
+
var _values_displayName;
|
|
179
|
+
if (creating) return;
|
|
180
|
+
/*
|
|
181
|
+
* `displayName` is the shared drawer's own field, and every artifact
|
|
182
|
+
* create in the console collects a name under that key. The campaign
|
|
183
|
+
* stores it as `name` — the container has one name and no second
|
|
184
|
+
* internal label — so the mapping happens here rather than by giving
|
|
185
|
+
* this one drawer a field the others do not have.
|
|
186
|
+
*/ const name = String((_values_displayName = values.displayName) != null ? _values_displayName : '').trim();
|
|
187
|
+
if (!name) return;
|
|
188
|
+
const startAtMs = values.startAt ? Date.parse(String(values.startAt)) : null;
|
|
189
|
+
const endAtMs = values.endAt ? Date.parse(String(values.endAt)) : null;
|
|
190
|
+
// Refused here rather than stored and rendered as a campaign that ends
|
|
191
|
+
// before it starts, which no window state describes.
|
|
192
|
+
if (startAtMs !== null && endAtMs !== null && endAtMs < startAtMs) {
|
|
193
|
+
return setCreateError('The end date is before the start date');
|
|
194
|
+
}
|
|
195
|
+
setCreating(true);
|
|
196
|
+
setCreateError(null);
|
|
197
|
+
const id = createResourceUid();
|
|
198
|
+
try {
|
|
199
|
+
var _ref;
|
|
200
|
+
await setDoc(doc(firestore, 'hosts', hostId, 'emailCampaigns', id), _extends({
|
|
201
|
+
name
|
|
202
|
+
}, Number.isFinite(startAtMs) && startAtMs !== null ? {
|
|
203
|
+
startAtMs
|
|
204
|
+
} : {}, Number.isFinite(endAtMs) && endAtMs !== null ? {
|
|
205
|
+
endAtMs
|
|
206
|
+
} : {}, {
|
|
207
|
+
listIds: Array.isArray(values.listIds) ? values.listIds.map(String) : []
|
|
208
|
+
}, values.topicId ? {
|
|
209
|
+
topicId: String(values.topicId)
|
|
210
|
+
} : {}, {
|
|
211
|
+
createdAtMs: Date.now(),
|
|
212
|
+
createdBy: String((_ref = user == null ? void 0 : user.uid) != null ? _ref : '')
|
|
213
|
+
}));
|
|
214
|
+
setCreateOpen(false);
|
|
215
|
+
openCampaign(id);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(error);
|
|
218
|
+
setCreateError('The campaign could not be created');
|
|
219
|
+
enqueueSnackbar('Could not create the campaign', {
|
|
220
|
+
variant: 'error',
|
|
221
|
+
allowDuplicate: true
|
|
222
|
+
});
|
|
223
|
+
} finally{
|
|
224
|
+
setCreating(false);
|
|
225
|
+
}
|
|
226
|
+
}, [
|
|
227
|
+
creating,
|
|
228
|
+
firestore,
|
|
229
|
+
hostId,
|
|
230
|
+
user,
|
|
231
|
+
enqueueSnackbar,
|
|
232
|
+
openCampaign
|
|
233
|
+
]);
|
|
234
|
+
const campaignHref = useCallback((id)=>hubPath ? `${hubPath}/campaigns/${id}` : null, [
|
|
235
|
+
hubPath
|
|
236
|
+
]);
|
|
237
|
+
/*==========================================
|
|
238
|
+
* DELETING A CAMPAIGN FROM ITS ROW.
|
|
239
|
+
*
|
|
240
|
+
* The container goes and its emails stay — the route detaches them first,
|
|
241
|
+
* so each one keeps its id, its report and its unsubscribe links, and
|
|
242
|
+
* reappears in this very table as a "Single send". That is not a
|
|
243
|
+
* convenience: a send id is inside the HMAC of every unsubscribe footer
|
|
244
|
+
* already delivered, so a delete that took the sends with it would break
|
|
245
|
+
* opt-outs that must go on working.
|
|
246
|
+
*
|
|
247
|
+
* The confirmation names the count and says what the delete does NOT do.
|
|
248
|
+
* Deleting a campaign cancels nothing — a scheduled email inside it still
|
|
249
|
+
* goes out — and a merchant who reads the button as "stop this campaign"
|
|
250
|
+
* has to be told before they press it.
|
|
251
|
+
*
|
|
252
|
+
* EDITING is deliberately not offered here. A record is edited on its own
|
|
253
|
+
* page in this console; the campaign's page carries the edit drawer, and
|
|
254
|
+
* this row's first entry is the way to it.
|
|
255
|
+
*=========================================*/ const handleDelete = useCallback(async (row)=>{
|
|
256
|
+
if (deletingId) return;
|
|
257
|
+
const emails = row.sends.length;
|
|
258
|
+
/*
|
|
259
|
+
* Emails with mail still to go: waiting for their time, AND part way
|
|
260
|
+
* through an audience larger than one batch. Counting only `scheduled`
|
|
261
|
+
* would miss the case that needs the warning most — an email delivering
|
|
262
|
+
* right now is stored as `scheduled` too, and `rollup.scheduled` is the
|
|
263
|
+
* narrower reading that leaves it out.
|
|
264
|
+
*/ const unfinished = row.rollup.scheduled + row.rollup.sending;
|
|
265
|
+
const agreed = await confirm({
|
|
266
|
+
title: `Delete ${row.name}?`,
|
|
267
|
+
description: (emails ? `The ${emails.toLocaleString()} ` + `${emails === 1 ? 'email' : 'emails'} in it are kept — each ` + 'keeps its own report and its unsubscribe links, and they stay ' + 'in this list as single sends. ' : 'It holds no emails. ') + (unfinished ? `${unfinished.toLocaleString()} of them ${unfinished === 1 ? 'is' : 'are'} still going out or still due, and deleting the campaign does ` + 'not stop that — the sender picks each one up from its own ' + 'record. Stop a send from its own page. ' : '') + 'Any screens, forms and contacts assigned to it come out of it and ' + 'are otherwise untouched. Only the campaign itself goes.',
|
|
268
|
+
confirmationText: 'Delete campaign'
|
|
269
|
+
}).then(()=>true).catch(()=>false);
|
|
270
|
+
if (!agreed) return;
|
|
271
|
+
setDeletingId(row.id);
|
|
272
|
+
try {
|
|
273
|
+
const { response, payload } = await manageApi({
|
|
274
|
+
action: 'deleteCampaign',
|
|
275
|
+
campaignId: row.id
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
var _ref;
|
|
279
|
+
return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The campaign could not be deleted', {
|
|
280
|
+
variant: 'warning',
|
|
281
|
+
allowDuplicate: true
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
enqueueSnackbar('Campaign deleted', {
|
|
285
|
+
variant: 'success',
|
|
286
|
+
persist: false
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(error);
|
|
290
|
+
enqueueSnackbar('The campaign could not be deleted', {
|
|
291
|
+
variant: 'error',
|
|
292
|
+
allowDuplicate: true
|
|
293
|
+
});
|
|
294
|
+
} finally{
|
|
295
|
+
setDeletingId('');
|
|
296
|
+
}
|
|
297
|
+
}, [
|
|
298
|
+
confirm,
|
|
299
|
+
deletingId,
|
|
300
|
+
enqueueSnackbar,
|
|
301
|
+
manageApi
|
|
302
|
+
]);
|
|
303
|
+
const columns = useMemo(()=>[
|
|
304
|
+
{
|
|
305
|
+
field: 'name',
|
|
306
|
+
headerName: 'Campaign',
|
|
307
|
+
flex: 1,
|
|
308
|
+
minWidth: 180,
|
|
309
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
|
|
310
|
+
direction: "row",
|
|
311
|
+
spacing: 1,
|
|
312
|
+
/*
|
|
313
|
+
`minWidth: 0` on the row AND on the name: a flex child will not
|
|
314
|
+
shrink below its content without it, so the chip beside it is
|
|
315
|
+
what gets clipped instead of the name being ellipsed.
|
|
316
|
+
*/ sx: {
|
|
317
|
+
alignItems: 'center',
|
|
318
|
+
minWidth: 0,
|
|
319
|
+
width: 1
|
|
320
|
+
},
|
|
321
|
+
children: [
|
|
322
|
+
campaignHref(row.id) ? /*#__PURE__*/ _jsx(AppLink, {
|
|
323
|
+
href: campaignHref(row.id),
|
|
324
|
+
onClick: (event)=>event.stopPropagation(),
|
|
325
|
+
style: {
|
|
326
|
+
minWidth: 0
|
|
327
|
+
},
|
|
328
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
329
|
+
variant: "body2",
|
|
330
|
+
noWrap: true,
|
|
331
|
+
children: row.name
|
|
332
|
+
})
|
|
333
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
334
|
+
variant: "body2",
|
|
335
|
+
noWrap: true,
|
|
336
|
+
sx: {
|
|
337
|
+
minWidth: 0
|
|
338
|
+
},
|
|
339
|
+
children: row.name
|
|
340
|
+
}),
|
|
341
|
+
row.legacy ? /*#__PURE__*/ _jsx(Chip, {
|
|
342
|
+
size: "small",
|
|
343
|
+
variant: "outlined",
|
|
344
|
+
label: "Single send",
|
|
345
|
+
// A label that reads "Single sen" is worse than a shorter name.
|
|
346
|
+
sx: {
|
|
347
|
+
flexShrink: 0
|
|
348
|
+
},
|
|
349
|
+
title: 'Sent before campaigns grouped their emails. Its report ' + 'and its unsubscribe links are unchanged.'
|
|
350
|
+
}) : null
|
|
351
|
+
]
|
|
352
|
+
})
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
field: 'windowState',
|
|
356
|
+
headerName: 'Window',
|
|
357
|
+
width: 170,
|
|
358
|
+
renderCell: ({ row })=>{
|
|
359
|
+
const start = formatDay(row.startAtMs);
|
|
360
|
+
const end = formatDay(row.endAtMs);
|
|
361
|
+
const range = start && end && start !== end ? `${start} – ${end}` : start || end || '';
|
|
362
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
363
|
+
sx: {
|
|
364
|
+
minWidth: 0
|
|
365
|
+
},
|
|
366
|
+
children: [
|
|
367
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
368
|
+
variant: "body2",
|
|
369
|
+
noWrap: true,
|
|
370
|
+
children: WINDOW_LABEL[row.windowState]
|
|
371
|
+
}),
|
|
372
|
+
range ? /*#__PURE__*/ _jsx(Typography, {
|
|
373
|
+
variant: "caption",
|
|
374
|
+
color: "text.secondary",
|
|
375
|
+
noWrap: true,
|
|
376
|
+
children: range
|
|
377
|
+
}) : null
|
|
378
|
+
]
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
field: 'listIds',
|
|
384
|
+
headerName: 'Lists',
|
|
385
|
+
width: 140,
|
|
386
|
+
sortable: false,
|
|
387
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
388
|
+
variant: "body2",
|
|
389
|
+
noWrap: true,
|
|
390
|
+
children: row.listIds.length ? row.listIds.map((id)=>{
|
|
391
|
+
var _listNames_get;
|
|
392
|
+
return (_listNames_get = listNames.get(id)) != null ? _listNames_get : id;
|
|
393
|
+
}).join(', ') : '—'
|
|
394
|
+
})
|
|
395
|
+
},
|
|
396
|
+
/*
|
|
397
|
+
THE FOUR COUNTS, RIGHT-ALIGNED HEAD AND BODY.
|
|
398
|
+
A figure is read by its last digit, so a column of them lines up on
|
|
399
|
+
the right or it does not line up at all. `headerAlign` has to be said
|
|
400
|
+
as well as `align`: a grid column defaults its header to the column
|
|
401
|
+
type's alignment rather than to the cell's, which is how these came to
|
|
402
|
+
sit with left headers over figures nobody could compare down the page.
|
|
403
|
+
|
|
404
|
+
EVERY ONE OF THEM MEASURES THE MAIL, and the headings say so. What a
|
|
405
|
+
campaign CAUSED — the submissions, leads, contacts and bookings
|
|
406
|
+
credited to it — is not a column here, because there is no figure to
|
|
407
|
+
read: a container has no conversion counter of its own, so a column
|
|
408
|
+
would be an aggregation per row for a campaign and a subcollection
|
|
409
|
+
document per row for a single send, on a list that costs two queries
|
|
410
|
+
however many campaigns it holds. Headings that named the campaign over
|
|
411
|
+
figures that all described its mail were the cheaper mistake, so the
|
|
412
|
+
headings name the mail and the note under the table says where the
|
|
413
|
+
outcome figures are.
|
|
414
|
+
*/ {
|
|
415
|
+
field: 'emails',
|
|
416
|
+
headerName: 'Emails',
|
|
417
|
+
width: 128,
|
|
418
|
+
align: 'right',
|
|
419
|
+
headerAlign: 'right',
|
|
420
|
+
/*
|
|
421
|
+
THE COUNT, AND WHAT IS STILL HAPPENING TO IT.
|
|
422
|
+
|
|
423
|
+
`sending` is called out apart from `scheduled` because the two are
|
|
424
|
+
opposite readings of the same stored status: an email delivering an
|
|
425
|
+
audience larger than one batch sits as `scheduled` between runs, and
|
|
426
|
+
rolling it in with the ones that have mailed nobody reports a
|
|
427
|
+
campaign that is half delivered as a campaign that has not started.
|
|
428
|
+
|
|
429
|
+
The note sits UNDER the figure rather than beside it — the column
|
|
430
|
+
would otherwise have to be wide enough for the longest sentence, and
|
|
431
|
+
the figure is what the column is read for.
|
|
432
|
+
*/ renderCell: ({ row })=>{
|
|
433
|
+
const { sends, sending, scheduled } = row.rollup;
|
|
434
|
+
const note = [
|
|
435
|
+
sending ? `${sending} sending` : '',
|
|
436
|
+
scheduled ? `${scheduled} scheduled` : ''
|
|
437
|
+
].filter(Boolean).join(', ');
|
|
438
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
439
|
+
sx: {
|
|
440
|
+
alignItems: 'flex-end',
|
|
441
|
+
minWidth: 0,
|
|
442
|
+
width: 1
|
|
443
|
+
},
|
|
444
|
+
children: [
|
|
445
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
446
|
+
variant: "body2",
|
|
447
|
+
children: String(sends)
|
|
448
|
+
}),
|
|
449
|
+
note ? /*#__PURE__*/ _jsx(Typography, {
|
|
450
|
+
variant: "caption",
|
|
451
|
+
color: "text.secondary",
|
|
452
|
+
noWrap: true,
|
|
453
|
+
children: note
|
|
454
|
+
}) : null
|
|
455
|
+
]
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
field: 'sent',
|
|
461
|
+
headerName: 'Mail sent',
|
|
462
|
+
width: 112,
|
|
463
|
+
align: 'right',
|
|
464
|
+
headerAlign: 'right',
|
|
465
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
466
|
+
variant: "body2",
|
|
467
|
+
children: figure(row.rollup.sent)
|
|
468
|
+
})
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
field: 'opens',
|
|
472
|
+
headerName: 'Mail opens',
|
|
473
|
+
width: 112,
|
|
474
|
+
align: 'right',
|
|
475
|
+
headerAlign: 'right',
|
|
476
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
477
|
+
variant: "body2",
|
|
478
|
+
children: figure(row.rollup.opens)
|
|
479
|
+
})
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
field: 'clicks',
|
|
483
|
+
headerName: 'Mail clicks',
|
|
484
|
+
width: 112,
|
|
485
|
+
align: 'right',
|
|
486
|
+
headerAlign: 'right',
|
|
487
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
488
|
+
variant: "body2",
|
|
489
|
+
children: figure(row.rollup.clicks)
|
|
490
|
+
})
|
|
491
|
+
},
|
|
492
|
+
/*
|
|
493
|
+
THE TRAILING CLUSTER, through the grid's own actions column.
|
|
494
|
+
`listActionsColumn` is what every other grid list in the console
|
|
495
|
+
renders, so the reader cannot tell which family of table they are
|
|
496
|
+
standing in — the hand-rolled tables on this surface reach the same
|
|
497
|
+
`RowActionsMenu` from the other direction.
|
|
498
|
+
|
|
499
|
+
Opening the campaign is restated here even though the row click does
|
|
500
|
+
the same thing, for the reason the screens table restates its own: the
|
|
501
|
+
menu is where a row's actions are NAMED, and an action absent from it
|
|
502
|
+
reads as an action the row does not have.
|
|
503
|
+
|
|
504
|
+
A "Single send" row is a SEND with no container — the table adopts it
|
|
505
|
+
so a merchant's history is not missing — and there is no container
|
|
506
|
+
under it to delete. Its entry is shown disabled with that reason
|
|
507
|
+
rather than hidden, because a control that vanishes and one that does
|
|
508
|
+
not apply look identical from the outside.
|
|
509
|
+
*/ listActionsColumn((row)=>{
|
|
510
|
+
var _row_name, _campaignHref;
|
|
511
|
+
return /*#__PURE__*/ _jsx(ListRowActions, {
|
|
512
|
+
label: String((_row_name = row.name) != null ? _row_name : row.id),
|
|
513
|
+
items: [
|
|
514
|
+
{
|
|
515
|
+
key: 'details',
|
|
516
|
+
label: 'Open campaign',
|
|
517
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
518
|
+
path: mdiEyeOutline.path,
|
|
519
|
+
size: 0.8
|
|
520
|
+
}),
|
|
521
|
+
href: (_campaignHref = campaignHref(row.id)) != null ? _campaignHref : undefined,
|
|
522
|
+
disabled: !campaignHref(row.id),
|
|
523
|
+
disabledReason: campaignHref(row.id) ? undefined : 'This site’s console URL has not resolved yet'
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
key: 'delete',
|
|
527
|
+
label: 'Delete campaign',
|
|
528
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
529
|
+
path: mdiDeleteOutline.path,
|
|
530
|
+
size: 0.8
|
|
531
|
+
}),
|
|
532
|
+
destructive: true,
|
|
533
|
+
disabled: row.legacy || Boolean(deletingId),
|
|
534
|
+
disabledReason: row.legacy ? 'This is a single send, not a campaign. Open it to ' + 'manage the email itself.' : 'A campaign is being deleted',
|
|
535
|
+
onClick: ()=>void handleDelete(row)
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
});
|
|
539
|
+
}, {
|
|
540
|
+
width: 72
|
|
541
|
+
})
|
|
542
|
+
], [
|
|
543
|
+
listNames,
|
|
544
|
+
campaignHref,
|
|
545
|
+
deletingId,
|
|
546
|
+
handleDelete
|
|
547
|
+
]);
|
|
548
|
+
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
549
|
+
header: 'Campaigns',
|
|
550
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
551
|
+
anchor: '#campaigns-group-emails',
|
|
552
|
+
excerpt: 'A campaign groups the emails you send to a set of lists over a ' + 'window of dates. Open one to write and send an email inside it.'
|
|
553
|
+
}),
|
|
554
|
+
HeaderProps: {
|
|
555
|
+
action: /*#__PURE__*/ _jsx(Button, {
|
|
556
|
+
size: "small",
|
|
557
|
+
variant: "contained",
|
|
558
|
+
disabled: creating,
|
|
559
|
+
onClick: ()=>{
|
|
560
|
+
setCreateError(null);
|
|
561
|
+
setCreateOpen(true);
|
|
562
|
+
},
|
|
563
|
+
children: creating ? 'Creating…' : 'Create campaign'
|
|
564
|
+
})
|
|
565
|
+
},
|
|
566
|
+
contentGutterX: true,
|
|
567
|
+
contentGutterY: true,
|
|
568
|
+
children: [
|
|
569
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
570
|
+
spacing: 1.5,
|
|
571
|
+
children: [
|
|
572
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
573
|
+
rows: rows,
|
|
574
|
+
columns: columns,
|
|
575
|
+
onOpen: (id)=>openCampaign(id),
|
|
576
|
+
noRowsLabel: "No campaigns yet",
|
|
577
|
+
noRowsDescription: "A campaign groups the emails you send to a set of lists.",
|
|
578
|
+
noRowsAction: /*#__PURE__*/ _jsx(Button, {
|
|
579
|
+
variant: "contained",
|
|
580
|
+
onClick: ()=>setCreateOpen(true),
|
|
581
|
+
children: 'Create campaign'
|
|
582
|
+
})
|
|
583
|
+
}),
|
|
584
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
585
|
+
variant: "caption",
|
|
586
|
+
color: "text.secondary",
|
|
587
|
+
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.'
|
|
588
|
+
}),
|
|
589
|
+
sendsTruncated || campaignsTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
590
|
+
severity: "info",
|
|
591
|
+
children: `Showing the most recent ${CONTAINER_CEILING} campaigns and ` + `${CAMPAIGN_CEILING} sends. This site has more — the ones ` + 'listed are not necessarily the most recent, because a send ' + 'carries no date field that every writer stamps.'
|
|
592
|
+
}) : null
|
|
593
|
+
]
|
|
594
|
+
}),
|
|
595
|
+
topicSource,
|
|
596
|
+
/*#__PURE__*/ _jsx(CreateArtifactDrawer, {
|
|
597
|
+
open: createOpen,
|
|
598
|
+
onClose: ()=>setCreateOpen(false),
|
|
599
|
+
title: "Create campaign",
|
|
600
|
+
submitLabel: "Create campaign",
|
|
601
|
+
includeDescription: false,
|
|
602
|
+
onSubmit: handleCreate,
|
|
603
|
+
extraFields: campaignFields(listOptions, topicOptions),
|
|
604
|
+
errorSlot: createError ? /*#__PURE__*/ _jsx(Alert, {
|
|
605
|
+
severity: "error",
|
|
606
|
+
sx: {
|
|
607
|
+
mt: 2,
|
|
608
|
+
mb: 1
|
|
609
|
+
},
|
|
610
|
+
children: createError
|
|
611
|
+
}) : null
|
|
612
|
+
})
|
|
613
|
+
]
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* What a campaign collects beyond its name.
|
|
618
|
+
*
|
|
619
|
+
* Data-driven-forms field descriptors, built from the org's own lists and
|
|
620
|
+
* topics so the drawer offers what this workspace actually has. Dates are
|
|
621
|
+
* plain `date` inputs: a campaign window is a day at each end, and a time of
|
|
622
|
+
* day would be a precision the model does not carry.
|
|
623
|
+
*/ function campaignFields(lists, topics) {
|
|
624
|
+
return [
|
|
625
|
+
{
|
|
626
|
+
component: 'text-field',
|
|
627
|
+
name: 'startAt',
|
|
628
|
+
label: 'Starts',
|
|
629
|
+
type: 'date',
|
|
630
|
+
helperText: 'When the campaign window opens'
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
component: 'text-field',
|
|
634
|
+
name: 'endAt',
|
|
635
|
+
label: 'Ends',
|
|
636
|
+
type: 'date',
|
|
637
|
+
helperText: 'Leave empty for an open-ended campaign',
|
|
638
|
+
InputLabelProps: {
|
|
639
|
+
shrink: true
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
component: 'select',
|
|
644
|
+
name: 'listIds',
|
|
645
|
+
label: 'Lists',
|
|
646
|
+
multiple: true,
|
|
647
|
+
initialValue: [],
|
|
648
|
+
// A campaign with no list is legitimate: its emails can go to leads, to
|
|
649
|
+
// site members, or to a segment. The lists are what it is AIMED at.
|
|
650
|
+
helperText: 'The lists this campaign is aimed at',
|
|
651
|
+
disableDefaultOption: true,
|
|
652
|
+
options: lists
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
component: 'select',
|
|
656
|
+
name: 'topicId',
|
|
657
|
+
label: 'Topic',
|
|
658
|
+
helperText: 'The stream its emails open on — each one can change it',
|
|
659
|
+
disableDefaultOption: true,
|
|
660
|
+
options: topics
|
|
661
|
+
}
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
HostCampaignsCard.displayName = 'HostCampaignsCard';
|
|
665
|
+
export default HostCampaignsCard;
|
|
666
|
+
|
|
667
|
+
//# sourceMappingURL=campaigns-card.js.map
|