@aglyn/plugins-marketing 1.0.0-beta.173 → 1.0.0-beta.175
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-detail-card.d.ts +12 -1
- package/src/lib/components/campaign-detail-card.js +146 -35
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.d.ts +13 -0
- package/src/lib/components/campaign-edit-drawer.js +56 -4
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-glance-card.component.js +10 -2
- package/src/lib/components/campaign-glance-card.component.js.map +1 -1
- package/src/lib/components/campaign-members-section.d.ts +6 -0
- package/src/lib/components/campaign-members-section.js +2 -2
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +58 -0
- package/src/lib/components/campaign-queries.js +72 -0
- package/src/lib/components/campaign-queries.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +21 -7
- package/src/lib/components/campaign-reach-sections.js +20 -17
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.d.ts +8 -1
- package/src/lib/components/campaign-report-card.js +33 -16
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.d.ts +10 -1
- package/src/lib/components/campaigns-card.js +102 -19
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.d.ts +3 -2
- package/src/lib/components/email-compose-card.js +18 -7
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.d.ts +11 -2
- package/src/lib/components/email-detail.js +50 -23
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/emails-list-card.d.ts +10 -1
- package/src/lib/components/emails-list-card.js +129 -23
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-marketing-summary-card.component.js +9 -1
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
- package/src/lib/components/marketing-console-page.js +81 -4
- package/src/lib/components/marketing-console-page.js.map +1 -1
- package/src/lib/components/marketing-console-sections.d.ts +16 -1
- package/src/lib/components/marketing-console-sections.js +25 -2
- package/src/lib/components/marketing-console-sections.js.map +1 -1
- package/src/lib/components/marketing-org-mount.d.ts +71 -0
- package/src/lib/components/marketing-org-mount.js +84 -0
- package/src/lib/components/marketing-org-mount.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +2 -2
- package/src/lib/components/use-campaign-send-api.js +22 -6
- package/src/lib/components/use-campaign-send-api.js.map +1 -1
- package/src/lib/components/use-emails-hub-path.d.ts +13 -3
- package/src/lib/components/use-emails-hub-path.js +25 -4
- package/src/lib/components/use-emails-hub-path.js.map +1 -1
- package/src/lib/components/use-marketing-hub-path.d.ts +19 -16
- package/src/lib/components/use-marketing-hub-path.js +6 -32
- package/src/lib/components/use-marketing-hub-path.js.map +1 -1
- package/src/lib/model/marketing-record-routes.js +11 -4
- package/src/lib/model/marketing-record-routes.js.map +1 -1
- package/src/lib/plugin.js +31 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/campaign-manage.d.ts +31 -2
- package/src/lib/server/campaign-manage.js +365 -137
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.d.ts +45 -0
- package/src/lib/server/campaign-org-refs.js +83 -0
- package/src/lib/server/campaign-org-refs.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +32 -2
- package/src/lib/server/campaign-process-scheduled.js +79 -9
- package/src/lib/server/campaign-process-scheduled.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +18 -4
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +139 -55
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-campaign-reach.d.ts +6 -5
- package/src/lib/server/email-campaign-reach.js +14 -14
- package/src/lib/server/email-campaign-reach.js.map +1 -1
- package/src/lib/server/email-events.js +48 -17
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/marketing-figures.js +12 -1
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server.js +7 -3
- package/src/lib/server.js.map +1 -1
package/README.md
CHANGED
|
@@ -19,10 +19,11 @@ The plugin declares four registrars in `plugins.config.json`, plus a `site` modu
|
|
|
19
19
|
**Console** (`registerMarketingConsole`, the `console` registrar):
|
|
20
20
|
|
|
21
21
|
- A `Marketing` nav item at `/marketing` with the sections Overview, Campaigns, Conversions, Overlays and A/B testing. Each section is a route; the page is code-split.
|
|
22
|
+
- The same page at the organization level, `/[orgSlug]/marketing`, declared in `orgNavItems` with the sections Campaigns and Emails. Campaigns and their sends belong to the organization (`orgs/{orgId}/emailCampaigns`, `orgs/{orgId}/campaigns`): the org hub lists every one, places a campaign on every site or on chosen sites (`visibleTo`), and asks which site an email is sent as before writing it. A site's hub lists the campaigns placed on that site and the emails sent as it. Both levels share the `release_marketing` flag.
|
|
22
23
|
- Campaigns end to end. The plugin owns the campaign composer, test sends, scheduling, reports and recipients. The individual messages are listed on the Emails page, but that page belongs to the email plugin, which hosts an `emailMessages` zone; this plugin fills it with the message list, one message's page and its composer. It also fills `emailTemplateRecipients` (who received the sends built from a template).
|
|
23
24
|
- Widgets in other hosts' slots: the "Last campaign" card on the site dashboard (`hostDashboard`), the campaigns card in the Inbox (`inboxCampaigns`), and campaign attribution on a record's page (`crmRecordAttribution`, `inboxRecordAttribution`).
|
|
24
25
|
- Zones this plugin hosts for other plugins to fill, registered with `registerPluginZone`: two on the A/B testing card (beside the variants, and below a test's results), and five bare zones in the campaign composer and on a message's page for whichever plugin keeps the mail itself: the topic picker, the topic options, the add-a-sender editor, the design creator and the sent-design preview.
|
|
25
|
-
- A record route for `campaign`, so another plugin can link to a campaign without knowing this plugin's URLs.
|
|
26
|
+
- A record route for `campaign`, at both the site and the organization level, so another plugin can link to a campaign without knowing this plugin's URLs.
|
|
26
27
|
|
|
27
28
|
**Server**, exported from `@aglyn/plugins-marketing/server`:
|
|
28
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketing",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.175",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.175",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.175",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.175",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.175",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.175",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.175",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.175",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.175",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.175",
|
|
37
|
+
"@aglyn/shared-util-http": "1.0.0-beta.175",
|
|
38
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.175",
|
|
39
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.175",
|
|
40
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.175",
|
|
41
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.175",
|
|
42
42
|
"@swc/helpers": "0.5.23"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface CampaignDetailCardProps {
|
|
2
|
-
|
|
2
|
+
/** The site, or `null` on the org Marketing hub. */
|
|
3
|
+
hostId: string | null;
|
|
3
4
|
/** A campaign container id, or a send id from before containers existed. */
|
|
4
5
|
campaignId: string;
|
|
5
6
|
/** The marketing hub URL, for the way back to the campaigns list. */
|
|
@@ -47,6 +48,16 @@ export interface CampaignDetailCardProps {
|
|
|
47
48
|
*
|
|
48
49
|
* The extra read is one document, and it buys the guarantee that a pasted
|
|
49
50
|
* report link resolves whichever kind of id it carries.
|
|
51
|
+
*
|
|
52
|
+
* ## Under a site, and over the organization
|
|
53
|
+
*
|
|
54
|
+
* The container and its sends are the org's, so the same page renders on the
|
|
55
|
+
* org hub with `hostId` null. What changes there is everything that is a SITE
|
|
56
|
+
* fact: the emails listed are every site's rather than the ones sent as this
|
|
57
|
+
* site, the forms and screens filed under the campaign are gathered from each
|
|
58
|
+
* site it is placed on, the conversions are one chosen site's (they are
|
|
59
|
+
* recorded per site), and writing an email first asks which site it is sent
|
|
60
|
+
* as — its sender, its designs and its consent all belong to that site.
|
|
50
61
|
*/
|
|
51
62
|
export declare function CampaignDetailCard(props: CampaignDetailCardProps): import("react").JSX.Element;
|
|
52
63
|
export declare namespace CampaignDetailCard {
|
|
@@ -24,18 +24,20 @@ import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.com
|
|
|
24
24
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
25
25
|
import { ceilingedWindow } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
26
26
|
import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
27
|
-
import { Alert, Box, Button, Chip, Divider, Stack, Typography } from "@mui/material";
|
|
28
|
-
import { collection, deleteField,
|
|
27
|
+
import { Alert, Box, Button, Chip, Divider, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
28
|
+
import { collection, deleteField, documentId, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
|
|
29
29
|
import { useCallback, useMemo, useState } from "react";
|
|
30
30
|
import { useRouter } from "next/navigation";
|
|
31
31
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
32
|
-
import { useFirestore, useFirestoreCollection, useFirestoreDoc
|
|
33
|
-
import { campaignRollup, campaignSendDisplay, campaignWindowState, emailListTimeMs } from "@aglyn/shared-ui-email-campaigns/model";
|
|
32
|
+
import { useFirestore, useFirestoreCollection, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
33
|
+
import { campaignRollup, campaignSendDisplay, campaignSiteIds, campaignVisibleTo, campaignWindowState, emailListTimeMs } from "@aglyn/shared-ui-email-campaigns/model";
|
|
34
34
|
import CampaignComposer from "./campaign-composer.js";
|
|
35
35
|
import CampaignEditDrawer from "./campaign-edit-drawer.js";
|
|
36
36
|
import { CampaignConversionsSection, CampaignDestinationsSection, CampaignRevenueSection, CampaignSequencesSection } from "./campaign-reach-sections.js";
|
|
37
37
|
import CampaignMembersSection from "./campaign-members-section.js";
|
|
38
38
|
import CampaignReportCard from "./campaign-report-card.js";
|
|
39
|
+
import { campaignContainerDoc, campaignSendsQuery } from "./campaign-queries.js";
|
|
40
|
+
import { orgSiteHubPath, orgSiteName, orgSiteOptions, topicCatalogHostId, useMarketingOrgId, useMarketingOrgMount } from "./marketing-org-mount.js";
|
|
39
41
|
import { useCampaignManageApi } from "./use-campaign-send-api.js";
|
|
40
42
|
import { useEmailsHubPath } from "./use-emails-hub-path.js";
|
|
41
43
|
import { useCampaignTopicOptions } from "./use-campaign-topic-options.js";
|
|
@@ -98,28 +100,44 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
98
100
|
*
|
|
99
101
|
* The extra read is one document, and it buys the guarantee that a pasted
|
|
100
102
|
* report link resolves whichever kind of id it carries.
|
|
103
|
+
*
|
|
104
|
+
* ## Under a site, and over the organization
|
|
105
|
+
*
|
|
106
|
+
* The container and its sends are the org's, so the same page renders on the
|
|
107
|
+
* org hub with `hostId` null. What changes there is everything that is a SITE
|
|
108
|
+
* fact: the emails listed are every site's rather than the ones sent as this
|
|
109
|
+
* site, the forms and screens filed under the campaign are gathered from each
|
|
110
|
+
* site it is placed on, the conversions are one chosen site's (they are
|
|
111
|
+
* recorded per site), and writing an email first asks which site it is sent
|
|
112
|
+
* as — its sender, its designs and its consent all belong to that site.
|
|
101
113
|
*/ export function CampaignDetailCard(props) {
|
|
102
114
|
var _campaign_listIds;
|
|
115
|
+
var _placedSites_;
|
|
103
116
|
const { hostId, campaignId, basePath } = props;
|
|
104
117
|
const firestore = useFirestore();
|
|
105
118
|
// The sibling hub, for the two records this page links to but does not own:
|
|
106
119
|
// each message's report and the template it was built from.
|
|
107
120
|
const emailsHub = useEmailsHubPath();
|
|
108
121
|
const router = useRouter();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
});
|
|
122
|
+
const orgMount = useMarketingOrgMount();
|
|
123
|
+
const { orgId } = useMarketingOrgId(hostId);
|
|
112
124
|
const { confirm } = useConfirmationContext();
|
|
113
125
|
const { enqueueSnackbar } = useSnackbar();
|
|
114
|
-
const manageApi = useCampaignManageApi(hostId);
|
|
126
|
+
const manageApi = useCampaignManageApi(hostId, orgId);
|
|
127
|
+
/*
|
|
128
|
+
* The site an email written here is sent AS. Under a site it is that site
|
|
129
|
+
* and never asked; on the org hub it is chosen before the composer opens.
|
|
130
|
+
* Empty means nobody has chosen yet.
|
|
131
|
+
*/ const [composeHostId, setComposeHostId] = useState('');
|
|
132
|
+
/** The site whose conversions the org hub is showing; empty until chosen. */ const [conversionsHostId, setConversionsHostId] = useState('');
|
|
115
133
|
const [composing, setComposing] = useState(false);
|
|
116
134
|
const [editing, setEditing] = useState(false);
|
|
117
135
|
const [saving, setSaving] = useState(false);
|
|
118
136
|
const [saveError, setSaveError] = useState(null);
|
|
119
137
|
const [deleting, setDeleting] = useState(false);
|
|
120
|
-
const { data: campaign, status } = useFirestoreDoc(()=>
|
|
138
|
+
const { data: campaign, status } = useFirestoreDoc(()=>orgId ? campaignContainerDoc(firestore, orgId, campaignId) : null, [
|
|
121
139
|
firestore,
|
|
122
|
-
|
|
140
|
+
orgId,
|
|
123
141
|
campaignId
|
|
124
142
|
]);
|
|
125
143
|
/*
|
|
@@ -133,8 +151,11 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
133
151
|
* to avoid. Not listening is what a null builder means.
|
|
134
152
|
*
|
|
135
153
|
* The campaign's emails come back by the field each send carries: equality
|
|
136
|
-
* on one field ordered by DOCUMENT NAME
|
|
137
|
-
* single-field index serves
|
|
154
|
+
* on one field ordered by DOCUMENT NAME. On the org hub that is all the
|
|
155
|
+
* query says, and the automatic single-field index serves it. Under a site
|
|
156
|
+
* the sends are also narrowed to the ones sent as that site — the scope
|
|
157
|
+
* filter that makes the read provable for a site collaborator — and that
|
|
158
|
+
* pair is the one composite index this page needs. Ordering on a date
|
|
138
159
|
* would need that index AND would drop every send missing that particular
|
|
139
160
|
* date — a sent send carries `sentAt`, a scheduled one `sendAtMs`, and
|
|
140
161
|
* neither is on both. Sorted by date below, over a window this page holds
|
|
@@ -142,17 +163,21 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
142
163
|
*
|
|
143
164
|
* One document past the ceiling, so "this campaign has more emails than are
|
|
144
165
|
* listed" is a fact rather than a guess.
|
|
145
|
-
*/ const { data: sendDocs } = useFirestoreCollection(()=>
|
|
166
|
+
*/ const { data: sendDocs } = useFirestoreCollection(()=>{
|
|
167
|
+
const sends = campaign ? campaignSendsQuery(firestore, orgId, hostId) : null;
|
|
168
|
+
return sends ? query(sends, where('emailCampaignId', '==', campaignId), orderBy(documentId()), limit(CAMPAIGN_EMAIL_CEILING + 1)) : null;
|
|
169
|
+
}, [
|
|
146
170
|
firestore,
|
|
171
|
+
orgId,
|
|
147
172
|
hostId,
|
|
148
173
|
campaignId,
|
|
149
174
|
Boolean(campaign)
|
|
150
175
|
], {
|
|
151
176
|
idField: '$id'
|
|
152
177
|
});
|
|
153
|
-
const { data: listDocs } = useFirestoreCollection(()=>campaign &&
|
|
178
|
+
const { data: listDocs } = useFirestoreCollection(()=>campaign && orgId ? query(collection(firestore, 'orgs', orgId, 'lists'), limit(50)) : null, [
|
|
154
179
|
firestore,
|
|
155
|
-
|
|
180
|
+
orgId,
|
|
156
181
|
Boolean(campaign)
|
|
157
182
|
], {
|
|
158
183
|
idField: '$id'
|
|
@@ -165,7 +190,7 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
165
190
|
* campaign's numbers for a field they are not looking at. The composer
|
|
166
191
|
* carries its own read of the same catalog, behind its own button, for the
|
|
167
192
|
* same reason.
|
|
168
|
-
*/ const { topics, source: topicSource } = useCampaignTopicOptions(hostId, {
|
|
193
|
+
*/ const { topics, source: topicSource } = useCampaignTopicOptions(topicCatalogHostId(hostId, orgMount), {
|
|
169
194
|
enabled: editing
|
|
170
195
|
});
|
|
171
196
|
const topicOptions = useMemo(()=>topics.map((topic)=>({
|
|
@@ -213,18 +238,25 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
213
238
|
* A cleared date is written as `null` — the absence the model already
|
|
214
239
|
* spells — and a cleared topic REMOVES the field, because the model has no
|
|
215
240
|
* null topic and `campaignTopicId` is handed to the composer as one.
|
|
241
|
+
*
|
|
242
|
+
* `visibleTo` is written only from the org hub, and only when its drawer
|
|
243
|
+
* says the placement changed. A site hub never touches it: the rules let a
|
|
244
|
+
* site collaborator edit a campaign only while its scope is unchanged, and
|
|
245
|
+
* a site hub has no field for it anyway.
|
|
216
246
|
*=========================================*/ const handleSave = useCallback(async (values)=>{
|
|
217
|
-
if (saving) return;
|
|
247
|
+
if (saving || !orgId) return;
|
|
218
248
|
setSaving(true);
|
|
219
249
|
setSaveError(null);
|
|
220
250
|
try {
|
|
221
|
-
await updateDoc(
|
|
251
|
+
await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), _extends({
|
|
222
252
|
name: values.name,
|
|
223
253
|
startAtMs: values.startAtMs,
|
|
224
254
|
endAtMs: values.endAtMs,
|
|
225
255
|
listIds: values.listIds,
|
|
226
256
|
topicId: values.topicId ? values.topicId : deleteField()
|
|
227
|
-
}
|
|
257
|
+
}, !hostId && values.siteIds !== undefined ? {
|
|
258
|
+
visibleTo: campaignVisibleTo(values.siteIds)
|
|
259
|
+
} : {}));
|
|
228
260
|
setEditing(false);
|
|
229
261
|
enqueueSnackbar('Campaign updated', {
|
|
230
262
|
variant: 'success',
|
|
@@ -241,6 +273,7 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
241
273
|
enqueueSnackbar,
|
|
242
274
|
firestore,
|
|
243
275
|
hostId,
|
|
276
|
+
orgId,
|
|
244
277
|
saving
|
|
245
278
|
]);
|
|
246
279
|
/*==========================================
|
|
@@ -313,8 +346,9 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
313
346
|
sends.length
|
|
314
347
|
]);
|
|
315
348
|
// Still settling. Falling through to the send report here would flash "this
|
|
316
|
-
// campaign could not be loaded" on every open of a campaign that exists
|
|
317
|
-
|
|
349
|
+
// campaign could not be loaded" on every open of a campaign that exists —
|
|
350
|
+
// and under a site nothing is read at all until the site's org is known.
|
|
351
|
+
if (!campaign && (status === 'loading' || !orgId)) return null;
|
|
318
352
|
/*
|
|
319
353
|
* NOT A CONTAINER — so it is a send id, and the send's own report is what
|
|
320
354
|
* this URL has always meant.
|
|
@@ -352,7 +386,16 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
352
386
|
* Nor is a redirect the answer to the second. A redirect would be another
|
|
353
387
|
* thing to be wrong about an id that is inside an HMAC; a page that keeps
|
|
354
388
|
* working has nothing to get wrong, and it costs nothing to leave standing.
|
|
355
|
-
*=========================================*/
|
|
389
|
+
*=========================================*/ /*
|
|
390
|
+
* On the org hub there is no Emails console to send a reader to, so a
|
|
391
|
+
* message opens on the org hub's own Emails section — the same page, over
|
|
392
|
+
* every site.
|
|
393
|
+
*/ const messagesPath = orgMount ? `${orgMount.basePath}/emails` : emailsHub ? `${emailsHub}/messages` : null;
|
|
394
|
+
const sendHref = (send)=>messagesPath ? `${messagesPath}/${send.$id}` : undefined;
|
|
395
|
+
/** A template is a site's design, so it opens on the site the send used. */ const templatesPath = (send)=>{
|
|
396
|
+
const hub = orgMount ? orgSiteHubPath(orgMount, send.hostId, 'emails') : emailsHub;
|
|
397
|
+
return hub ? `${hub}/templates` : null;
|
|
398
|
+
};
|
|
356
399
|
/**
|
|
357
400
|
* What one of this campaign's emails can be opened into.
|
|
358
401
|
*
|
|
@@ -367,6 +410,7 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
367
410
|
*/ const sendActions = (send)=>{
|
|
368
411
|
var _send_templateScreenId;
|
|
369
412
|
const templateScreenId = String((_send_templateScreenId = send.templateScreenId) != null ? _send_templateScreenId : '');
|
|
413
|
+
const templates = templatesPath(send);
|
|
370
414
|
return [
|
|
371
415
|
{
|
|
372
416
|
key: 'details',
|
|
@@ -386,8 +430,8 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
386
430
|
path: mdiPaletteOutline.path,
|
|
387
431
|
size: 0.8
|
|
388
432
|
}),
|
|
389
|
-
href: templateScreenId &&
|
|
390
|
-
disabled: !templateScreenId || !
|
|
433
|
+
href: templateScreenId && templates ? `${templates}/${templateScreenId}` : undefined,
|
|
434
|
+
disabled: !templateScreenId || !templates,
|
|
391
435
|
disabledReason: templateScreenId ? 'This site’s console URL has not resolved yet' : 'This message was not built from a template'
|
|
392
436
|
}
|
|
393
437
|
];
|
|
@@ -504,6 +548,17 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
504
548
|
const windowState = campaignWindowState(campaign, Date.now());
|
|
505
549
|
const start = campaign.startAtMs ? new Date(campaign.startAtMs).toLocaleDateString() : '';
|
|
506
550
|
const end = campaign.endAtMs ? new Date(campaign.endAtMs).toLocaleDateString() : '';
|
|
551
|
+
/*
|
|
552
|
+
* THE SITES IT IS PLACED ON, which the org hub needs three times over: the
|
|
553
|
+
* forms and screens filed under it are gathered from each of them, its
|
|
554
|
+
* conversions are read for one of them, and an email written here is sent
|
|
555
|
+
* as one of them. Every site when it is placed everywhere. Under a site the
|
|
556
|
+
* answer is that site, and none of this is read.
|
|
557
|
+
*/ const placedIds = campaignSiteIds(campaign);
|
|
558
|
+
const placedSites = orgMount ? orgMount.hosts.filter((site)=>placedIds === null || placedIds.includes(site.id)) : [];
|
|
559
|
+
const conversionsHost = hostId != null ? hostId : conversionsHostId || ((_placedSites_ = placedSites[0]) == null ? void 0 : _placedSites_.id) || '';
|
|
560
|
+
const conversionsBasePath = hostId ? basePath : orgSiteHubPath(orgMount, conversionsHost, 'marketing');
|
|
561
|
+
/** Asked only when there is a choice to make; one site answers itself. */ const writeAsHostId = hostId != null ? hostId : placedSites.length === 1 ? placedSites[0].id : composeHostId;
|
|
507
562
|
/* The card, named so the page chrome above it is a plain list of
|
|
508
563
|
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
509
564
|
header: 'Campaign',
|
|
@@ -600,37 +655,71 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
600
655
|
})
|
|
601
656
|
]
|
|
602
657
|
}),
|
|
658
|
+
orgMount ? /*#__PURE__*/ _jsx(Typography, {
|
|
659
|
+
variant: "body2",
|
|
660
|
+
color: "text.secondary",
|
|
661
|
+
children: placedIds === null ? 'Offered on every site' : placedIds.length ? `Offered on ${placedIds.map((id)=>orgSiteName(orgMount, id)).join(', ')}` : 'Not offered on any site yet — edit the campaign to place it'
|
|
662
|
+
}) : null,
|
|
603
663
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
604
|
-
/*#__PURE__*/ _jsx(
|
|
605
|
-
|
|
664
|
+
orgMount && placedSites.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
|
|
665
|
+
select: true,
|
|
666
|
+
size: "small",
|
|
667
|
+
label: "Conversions on",
|
|
668
|
+
value: conversionsHost,
|
|
669
|
+
onChange: (event)=>setConversionsHostId(event.target.value),
|
|
670
|
+
sx: {
|
|
671
|
+
maxWidth: 320
|
|
672
|
+
},
|
|
673
|
+
children: placedSites.map((site)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
674
|
+
value: site.id,
|
|
675
|
+
children: site.name || site.subdomain || site.id
|
|
676
|
+
}, site.id))
|
|
677
|
+
}) : null,
|
|
678
|
+
conversionsHost ? /*#__PURE__*/ _jsx(CampaignConversionsSection, {
|
|
679
|
+
hostId: conversionsHost,
|
|
606
680
|
sendIds: sendIds,
|
|
607
681
|
truncated: sendsTruncated,
|
|
608
|
-
basePath:
|
|
682
|
+
basePath: conversionsBasePath
|
|
683
|
+
}, conversionsHost) : /*#__PURE__*/ _jsx(Typography, {
|
|
684
|
+
variant: "body2",
|
|
685
|
+
color: "text.secondary",
|
|
686
|
+
children: 'Conversions are counted per site, and this campaign is not ' + 'placed on one yet.'
|
|
609
687
|
}),
|
|
610
688
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
611
689
|
/*#__PURE__*/ _jsx(CampaignSequencesSection, {
|
|
612
|
-
|
|
690
|
+
orgId: orgId,
|
|
613
691
|
campaignId: campaignId
|
|
614
692
|
}),
|
|
615
693
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
616
694
|
/*#__PURE__*/ _jsx(CampaignRevenueSection, {
|
|
617
|
-
|
|
695
|
+
orgId: orgId,
|
|
618
696
|
sendIds: sendIds,
|
|
619
697
|
truncated: sendsTruncated
|
|
620
698
|
}),
|
|
621
699
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
622
700
|
/*#__PURE__*/ _jsx(CampaignDestinationsSection, {
|
|
623
|
-
|
|
701
|
+
orgId: orgId,
|
|
624
702
|
sendIds: sendIds,
|
|
625
703
|
truncated: sendsTruncated
|
|
626
704
|
}),
|
|
627
705
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
628
|
-
/*#__PURE__*/ _jsx(CampaignMembersSection, {
|
|
706
|
+
hostId ? /*#__PURE__*/ _jsx(CampaignMembersSection, {
|
|
629
707
|
hostId: hostId,
|
|
630
708
|
campaignId: campaignId,
|
|
631
709
|
startAtMs: campaign.startAtMs,
|
|
632
710
|
endAtMs: campaign.endAtMs
|
|
633
|
-
})
|
|
711
|
+
}) : /*
|
|
712
|
+
* Forms and screens are a site's records, so on the org hub each
|
|
713
|
+
* site the campaign is placed on answers for its own. One section
|
|
714
|
+
* per site keeps each one's figures and ceilings its own rather
|
|
715
|
+
* than merging lists that were bounded separately.
|
|
716
|
+
*/ placedSites.map((site)=>/*#__PURE__*/ _jsx(CampaignMembersSection, {
|
|
717
|
+
hostId: site.id,
|
|
718
|
+
siteName: site.name || site.subdomain || site.id,
|
|
719
|
+
campaignId: campaignId,
|
|
720
|
+
startAtMs: campaign.startAtMs,
|
|
721
|
+
endAtMs: campaign.endAtMs
|
|
722
|
+
}, site.id)),
|
|
634
723
|
/*#__PURE__*/ _jsx(Divider, {}),
|
|
635
724
|
/*#__PURE__*/ _jsx(Section, {
|
|
636
725
|
title: "The mail, across this campaign",
|
|
@@ -731,13 +820,34 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
731
820
|
mb: 2
|
|
732
821
|
}
|
|
733
822
|
}),
|
|
734
|
-
/*#__PURE__*/ _jsx(
|
|
735
|
-
|
|
823
|
+
!hostId && placedSites.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
|
|
824
|
+
select: true,
|
|
825
|
+
size: "small",
|
|
826
|
+
label: "Send as",
|
|
827
|
+
value: composeHostId,
|
|
828
|
+
onChange: (event)=>setComposeHostId(event.target.value),
|
|
829
|
+
helperText: "The site this email is sent from — its sender, designs and unsubscribe page",
|
|
830
|
+
sx: {
|
|
831
|
+
mb: 2,
|
|
832
|
+
maxWidth: 420
|
|
833
|
+
},
|
|
834
|
+
fullWidth: true,
|
|
835
|
+
children: placedSites.map((site)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
836
|
+
value: site.id,
|
|
837
|
+
children: site.name || site.subdomain || site.id
|
|
838
|
+
}, site.id))
|
|
839
|
+
}) : null,
|
|
840
|
+
writeAsHostId ? /*#__PURE__*/ _jsx(CampaignComposer, {
|
|
841
|
+
hostId: writeAsHostId,
|
|
736
842
|
emailCampaignId: campaignId,
|
|
737
843
|
campaignListIds: listIds,
|
|
738
844
|
campaignTopicId: campaign.topicId,
|
|
739
845
|
onSent: ()=>setComposing(false)
|
|
740
|
-
})
|
|
846
|
+
}, writeAsHostId) : !placedSites.length && !hostId ? /*#__PURE__*/ _jsx(Typography, {
|
|
847
|
+
variant: "body2",
|
|
848
|
+
color: "text.secondary",
|
|
849
|
+
children: 'Place this campaign on a site before writing an email in ' + 'it — every email is sent as one site.'
|
|
850
|
+
}) : null
|
|
741
851
|
]
|
|
742
852
|
}) : null
|
|
743
853
|
]
|
|
@@ -755,6 +865,7 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
755
865
|
};
|
|
756
866
|
}),
|
|
757
867
|
topics: topicOptions,
|
|
868
|
+
sites: orgMount ? orgSiteOptions(orgMount) : undefined,
|
|
758
869
|
busy: saving,
|
|
759
870
|
error: saveError,
|
|
760
871
|
onSubmit: (values)=>void handleSave(values)
|