@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,338 @@
|
|
|
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 { pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
22
|
+
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
|
+
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
24
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
25
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
26
|
+
import { Alert, Chip, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
27
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
28
|
+
/** The server's page size. Fixed, so the reader is not offered a choice. */ const PAGE_SIZE = 25;
|
|
29
|
+
const FILTER_LABELS = {
|
|
30
|
+
all: 'Everyone this was sent to',
|
|
31
|
+
opened: 'Opened it',
|
|
32
|
+
clicked: 'Clicked something'
|
|
33
|
+
};
|
|
34
|
+
const recipientsDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
35
|
+
anchor: '#opens--clicks',
|
|
36
|
+
excerpt: 'The people an email reached, and which of them opened it or clicked ' + 'a link — read from the per-recipient delivery log.'
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* WHO, not how many.
|
|
40
|
+
*
|
|
41
|
+
* ## Why this is a fetch and not a listen
|
|
42
|
+
*
|
|
43
|
+
* Every other card on this page reads Firestore directly. This one cannot:
|
|
44
|
+
* the per-recipient delivery log lives at `emailDeliveries/{sha256(address)}`
|
|
45
|
+
* — one platform-level collection holding every site's mail and every
|
|
46
|
+
* transactional message besides — and the query that narrows it to this
|
|
47
|
+
* site's campaigns is a collection-group query whose scoping predicate is
|
|
48
|
+
* part of the query. A security rule cannot require a `where` clause, so
|
|
49
|
+
* there is no rule that would admit this read and refuse the same read
|
|
50
|
+
* without the filter. The narrowing happens on the server, behind the same
|
|
51
|
+
* site role the send path requires.
|
|
52
|
+
*
|
|
53
|
+
* ## Why it is its own card
|
|
54
|
+
*
|
|
55
|
+
* A separate card is a separate mount, and this is the only read on either
|
|
56
|
+
* detail page whose cost grows with how much mail there has been. It asks
|
|
57
|
+
* once on mount and once per page turn — never on a poll — so a reader who
|
|
58
|
+
* came for the preview and the totals above pays for one page of this and
|
|
59
|
+
* nothing more.
|
|
60
|
+
*
|
|
61
|
+
* ## The cursor, and why "Previous" keeps a stack
|
|
62
|
+
*
|
|
63
|
+
* The route pages forward with an opaque cursor, which is the only shape a
|
|
64
|
+
* Firestore query offers without counting the whole result first. Going back
|
|
65
|
+
* therefore means remembering where each page started, so the cursors are
|
|
66
|
+
* kept in a list indexed by page — `null` for the first — rather than
|
|
67
|
+
* re-walking forward from the beginning.
|
|
68
|
+
*/ export function EmailRecipientsCard(props) {
|
|
69
|
+
var _cursors_page;
|
|
70
|
+
const { hostId, screenId, emailId } = props;
|
|
71
|
+
const { data: user } = useUser();
|
|
72
|
+
const [filter, setFilter] = useState('all');
|
|
73
|
+
const [page, setPage] = useState(0);
|
|
74
|
+
/** Cursor for each page. Index 0 is always `null` — the first page. */ const [cursors, setCursors] = useState([
|
|
75
|
+
null
|
|
76
|
+
]);
|
|
77
|
+
const [rows, setRows] = useState([]);
|
|
78
|
+
const [hasMore, setHasMore] = useState(false);
|
|
79
|
+
const [loading, setLoading] = useState(true);
|
|
80
|
+
const [failure, setFailure] = useState(null);
|
|
81
|
+
const [campaignsOmitted, setCampaignsOmitted] = useState(0);
|
|
82
|
+
const [campaignsRead, setCampaignsRead] = useState(0);
|
|
83
|
+
const cursor = (_cursors_page = cursors[page]) != null ? _cursors_page : null;
|
|
84
|
+
useEffect(()=>{
|
|
85
|
+
if (!user) return undefined;
|
|
86
|
+
let active = true;
|
|
87
|
+
setLoading(true);
|
|
88
|
+
setFailure(null);
|
|
89
|
+
void (async ()=>{
|
|
90
|
+
try {
|
|
91
|
+
var _ref, _ref1, _ref2;
|
|
92
|
+
const response = await authorizedFetch(user, '/api/campaigns/recipients', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json'
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(_extends({
|
|
98
|
+
hostId
|
|
99
|
+
}, screenId ? {
|
|
100
|
+
screenId
|
|
101
|
+
} : {}, emailId ? {
|
|
102
|
+
emailId
|
|
103
|
+
} : {}, {
|
|
104
|
+
filter,
|
|
105
|
+
cursor
|
|
106
|
+
}))
|
|
107
|
+
});
|
|
108
|
+
const payload = await response.json().catch(()=>({}));
|
|
109
|
+
if (!active) return;
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
var _ref3;
|
|
112
|
+
setRows([]);
|
|
113
|
+
setHasMore(false);
|
|
114
|
+
setFailure(String((_ref3 = payload == null ? void 0 : payload.error) != null ? _ref3 : 'Could not read recipients'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
/*
|
|
118
|
+
* A read that FAILED is not a campaign nobody opened.
|
|
119
|
+
*
|
|
120
|
+
* The route answers 200 with `lookupFailed` for a delivery-log read
|
|
121
|
+
* that could not run — a missing collection-group index is the likely
|
|
122
|
+
* one — because the rest of the payload is still true. Rendering that
|
|
123
|
+
* as an empty table is how a merchant concludes their campaign
|
|
124
|
+
* reached nobody, which is the one wrong answer this table can give.
|
|
125
|
+
*/ if (payload == null ? void 0 : payload.lookupFailed) {
|
|
126
|
+
setRows([]);
|
|
127
|
+
setHasMore(false);
|
|
128
|
+
setFailure('The delivery log could not be read, so this table is empty for ' + 'a reason that has nothing to do with your campaign. The ' + 'numbers above come from the campaigns themselves and are ' + 'unaffected.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const nextRows = Array.isArray(payload == null ? void 0 : payload.rows) ? payload.rows : [];
|
|
132
|
+
setRows(nextRows);
|
|
133
|
+
setCampaignsOmitted(Number((_ref = payload == null ? void 0 : payload.campaignsOmitted) != null ? _ref : 0));
|
|
134
|
+
setCampaignsRead(Number((_ref1 = payload == null ? void 0 : payload.campaignsRead) != null ? _ref1 : 0));
|
|
135
|
+
const nextCursor = (_ref2 = payload == null ? void 0 : payload.cursor) != null ? _ref2 : null;
|
|
136
|
+
setHasMore(Boolean(nextCursor));
|
|
137
|
+
if (nextCursor) {
|
|
138
|
+
setCursors((existing)=>{
|
|
139
|
+
if (existing[page + 1] === nextCursor) return existing;
|
|
140
|
+
const next = existing.slice(0, page + 1);
|
|
141
|
+
next[page + 1] = nextCursor;
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(error);
|
|
147
|
+
if (active) {
|
|
148
|
+
setRows([]);
|
|
149
|
+
setHasMore(false);
|
|
150
|
+
setFailure('Could not read recipients');
|
|
151
|
+
}
|
|
152
|
+
} finally{
|
|
153
|
+
if (active) setLoading(false);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
return ()=>{
|
|
157
|
+
active = false;
|
|
158
|
+
};
|
|
159
|
+
}, [
|
|
160
|
+
user,
|
|
161
|
+
hostId,
|
|
162
|
+
screenId,
|
|
163
|
+
emailId,
|
|
164
|
+
filter,
|
|
165
|
+
cursor,
|
|
166
|
+
page
|
|
167
|
+
]);
|
|
168
|
+
// A new filter is a new result set, so the cursors collected under the old
|
|
169
|
+
// one describe positions in a query that no longer exists.
|
|
170
|
+
const handleFilter = useCallback((next)=>{
|
|
171
|
+
setFilter(next);
|
|
172
|
+
setPage(0);
|
|
173
|
+
setCursors([
|
|
174
|
+
null
|
|
175
|
+
]);
|
|
176
|
+
}, []);
|
|
177
|
+
const columns = useMemo(()=>[
|
|
178
|
+
{
|
|
179
|
+
field: 'to',
|
|
180
|
+
headerName: 'Recipient',
|
|
181
|
+
flex: 1,
|
|
182
|
+
minWidth: 240,
|
|
183
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
|
|
184
|
+
spacing: 0.5,
|
|
185
|
+
sx: {
|
|
186
|
+
py: 1,
|
|
187
|
+
minWidth: 0,
|
|
188
|
+
wordBreak: 'break-all'
|
|
189
|
+
},
|
|
190
|
+
children: [
|
|
191
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
192
|
+
variant: "body2",
|
|
193
|
+
children: row.to
|
|
194
|
+
}),
|
|
195
|
+
row.clickedLinks.map((link)=>/*#__PURE__*/ _jsx(Typography, {
|
|
196
|
+
variant: "caption",
|
|
197
|
+
color: "text.secondary",
|
|
198
|
+
children: link
|
|
199
|
+
}, link))
|
|
200
|
+
]
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
...emailId ? [] : [
|
|
204
|
+
{
|
|
205
|
+
field: 'subject',
|
|
206
|
+
headerName: 'Email',
|
|
207
|
+
flex: 1,
|
|
208
|
+
minWidth: 180,
|
|
209
|
+
valueGetter: (_value, row)=>{
|
|
210
|
+
var _row_subject;
|
|
211
|
+
return (_row_subject = row.subject) != null ? _row_subject : '—';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
{
|
|
216
|
+
field: 'status',
|
|
217
|
+
headerName: 'State',
|
|
218
|
+
width: 130,
|
|
219
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Chip, {
|
|
220
|
+
size: "small",
|
|
221
|
+
label: row.status
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
field: 'openCount',
|
|
226
|
+
headerName: 'Opens',
|
|
227
|
+
type: 'number',
|
|
228
|
+
align: 'right',
|
|
229
|
+
headerAlign: 'right',
|
|
230
|
+
width: 100,
|
|
231
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
232
|
+
variant: "body2",
|
|
233
|
+
sx: {
|
|
234
|
+
fontWeight: 'bold'
|
|
235
|
+
},
|
|
236
|
+
children: row.openCount.toLocaleString()
|
|
237
|
+
})
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
field: 'clickCount',
|
|
241
|
+
headerName: 'Clicks',
|
|
242
|
+
type: 'number',
|
|
243
|
+
align: 'right',
|
|
244
|
+
headerAlign: 'right',
|
|
245
|
+
width: 100,
|
|
246
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
247
|
+
variant: "body2",
|
|
248
|
+
sx: {
|
|
249
|
+
fontWeight: 'bold'
|
|
250
|
+
},
|
|
251
|
+
children: row.clickCount.toLocaleString()
|
|
252
|
+
})
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
field: 'lastEventAtMs',
|
|
256
|
+
headerName: 'Last event',
|
|
257
|
+
width: 190,
|
|
258
|
+
renderCell: ({ row })=>row.lastEventAtMs ? new Date(row.lastEventAtMs).toLocaleString() : '—'
|
|
259
|
+
}
|
|
260
|
+
], [
|
|
261
|
+
emailId
|
|
262
|
+
]);
|
|
263
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
264
|
+
header: 'Recipients',
|
|
265
|
+
help: recipientsDocsHelp,
|
|
266
|
+
contentGutterX: true,
|
|
267
|
+
contentGutterY: true,
|
|
268
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
269
|
+
spacing: 2,
|
|
270
|
+
children: [
|
|
271
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
272
|
+
variant: "body2",
|
|
273
|
+
color: "text.secondary",
|
|
274
|
+
children: (emailId ? 'One row per address this email was sent to, newest first. ' : 'One row per message sent from this template, newest first. ') + 'Opens are counted by a tracking pixel a mail client may block ' + 'or pre-fetch, so an absent open is weaker evidence than a click.'
|
|
275
|
+
}),
|
|
276
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
277
|
+
select: true,
|
|
278
|
+
size: "small",
|
|
279
|
+
label: "Show",
|
|
280
|
+
value: filter,
|
|
281
|
+
onChange: (event)=>handleFilter(event.target.value),
|
|
282
|
+
sx: {
|
|
283
|
+
alignSelf: 'flex-start',
|
|
284
|
+
minWidth: 260
|
|
285
|
+
},
|
|
286
|
+
children: Object.keys(FILTER_LABELS).map((key)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
287
|
+
value: key,
|
|
288
|
+
children: FILTER_LABELS[key]
|
|
289
|
+
}, key))
|
|
290
|
+
}),
|
|
291
|
+
failure ? /*#__PURE__*/ _jsx(Alert, {
|
|
292
|
+
severity: "warning",
|
|
293
|
+
children: failure
|
|
294
|
+
}) : null,
|
|
295
|
+
campaignsOmitted ? /*#__PURE__*/ _jsx(Alert, {
|
|
296
|
+
severity: "info",
|
|
297
|
+
children: 'This template has been used by more emails than one read can ' + `span. These rows cover its ${campaignsRead} most recent ` + `emails; ${campaignsOmitted} older ` + `${campaignsOmitted === 1 ? 'email is' : 'emails are'} ` + 'not included.'
|
|
298
|
+
}) : null,
|
|
299
|
+
rows.length === 0 && !loading && !failure ? /*#__PURE__*/ _jsx(Typography, {
|
|
300
|
+
variant: "body2",
|
|
301
|
+
color: "text.secondary",
|
|
302
|
+
children: filter === 'all' ? 'No delivery records have been kept for this yet.' : 'Nobody in the delivery log matches that yet.'
|
|
303
|
+
}) : /*#__PURE__*/ _jsx(ListTable, {
|
|
304
|
+
"aria-label": "Recipients",
|
|
305
|
+
rows: rows,
|
|
306
|
+
columns: columns,
|
|
307
|
+
getRowId: (row)=>row.messageId,
|
|
308
|
+
rowHeight: TABLE_ROW_HEIGHT,
|
|
309
|
+
// A recipient carries the links they followed beneath the
|
|
310
|
+
// address, so a row is as tall as its list of links.
|
|
311
|
+
getRowHeight: ()=>'auto',
|
|
312
|
+
// One page of a cursor feed, turned by the footer below: the grid
|
|
313
|
+
// neither slices it nor filters the page and calls that the log.
|
|
314
|
+
hideFooter: true,
|
|
315
|
+
disableColumnFilter: true,
|
|
316
|
+
quickFilter: false
|
|
317
|
+
}),
|
|
318
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
319
|
+
page: page,
|
|
320
|
+
pageSize: PAGE_SIZE,
|
|
321
|
+
rowCount: rows.length,
|
|
322
|
+
hasMore: hasMore,
|
|
323
|
+
disabled: loading,
|
|
324
|
+
onPageChange: setPage
|
|
325
|
+
}),
|
|
326
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
327
|
+
variant: "caption",
|
|
328
|
+
color: "text.secondary",
|
|
329
|
+
children: 'Up to ten distinct destinations are kept per recipient, so a ' + 'reader who clicked more links than that has the rest counted ' + 'but not listed.'
|
|
330
|
+
})
|
|
331
|
+
]
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
EmailRecipientsCard.displayName = 'EmailRecipientsCard';
|
|
336
|
+
export default EmailRecipientsCard;
|
|
337
|
+
|
|
338
|
+
//# sourceMappingURL=email-recipients-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/email-recipients-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Chip,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\n\n/** The server's page size. Fixed, so the reader is not offered a choice. */\nconst PAGE_SIZE = 25\n\n/** Which recipients the table asks for. Matches the route's own vocabulary. */\ntype EngagementFilter = 'all' | 'opened' | 'clicked'\n\nconst FILTER_LABELS: Record<EngagementFilter, string> = {\n all: 'Everyone this was sent to',\n opened: 'Opened it',\n clicked: 'Clicked something',\n}\n\ninterface RecipientRow {\n messageId: string\n to: string\n subject: string | null\n campaignId: string | null\n status: string\n openCount: number\n clickCount: number\n clickedLinks: string[]\n firstSeenAtMs: number\n lastEventAtMs: number\n}\n\nconst recipientsDocsHelp = pluginDocsHelp('emailCampaigns', {\n anchor: '#opens--clicks',\n excerpt:\n 'The people an email reached, and which of them opened it or clicked ' +\n 'a link — read from the per-recipient delivery log.',\n})\n\nexport interface EmailRecipientsCardProps {\n hostId: string\n /**\n * Read every message built from this template.\n *\n * Exactly one of `screenId` and `emailId` is passed. The route takes the\n * SCOPE rather than a list of message ids because a caller who could name\n * ids could name another site's, and the narrower input is the one that\n * cannot be got wrong later.\n */\n screenId?: string\n /** Read one message. */\n emailId?: string\n}\n\n/**\n * WHO, not how many.\n *\n * ## Why this is a fetch and not a listen\n *\n * Every other card on this page reads Firestore directly. This one cannot:\n * the per-recipient delivery log lives at `emailDeliveries/{sha256(address)}`\n * — one platform-level collection holding every site's mail and every\n * transactional message besides — and the query that narrows it to this\n * site's campaigns is a collection-group query whose scoping predicate is\n * part of the query. A security rule cannot require a `where` clause, so\n * there is no rule that would admit this read and refuse the same read\n * without the filter. The narrowing happens on the server, behind the same\n * site role the send path requires.\n *\n * ## Why it is its own card\n *\n * A separate card is a separate mount, and this is the only read on either\n * detail page whose cost grows with how much mail there has been. It asks\n * once on mount and once per page turn — never on a poll — so a reader who\n * came for the preview and the totals above pays for one page of this and\n * nothing more.\n *\n * ## The cursor, and why \"Previous\" keeps a stack\n *\n * The route pages forward with an opaque cursor, which is the only shape a\n * Firestore query offers without counting the whole result first. Going back\n * therefore means remembering where each page started, so the cursors are\n * kept in a list indexed by page — `null` for the first — rather than\n * re-walking forward from the beginning.\n */\nexport function EmailRecipientsCard(props: EmailRecipientsCardProps) {\n const { hostId, screenId, emailId } = props\n const { data: user } = useUser()\n\n const [filter, setFilter] = useState<EngagementFilter>('all')\n const [page, setPage] = useState(0)\n /** Cursor for each page. Index 0 is always `null` — the first page. */\n const [cursors, setCursors] = useState<(string | null)[]>([null])\n const [rows, setRows] = useState<RecipientRow[]>([])\n const [hasMore, setHasMore] = useState(false)\n const [loading, setLoading] = useState(true)\n const [failure, setFailure] = useState<string | null>(null)\n const [campaignsOmitted, setCampaignsOmitted] = useState(0)\n const [campaignsRead, setCampaignsRead] = useState(0)\n\n const cursor = cursors[page] ?? null\n\n useEffect(() => {\n if (!user) return undefined\n let active = true\n setLoading(true)\n setFailure(null)\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n '/api/campaigns/recipients',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n ...(screenId ? { screenId } : {}),\n ...(emailId ? { emailId } : {}),\n filter,\n cursor,\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!active) return\n if (!response.ok) {\n setRows([])\n setHasMore(false)\n setFailure(String(payload?.error ?? 'Could not read recipients'))\n return\n }\n /*\n * A read that FAILED is not a campaign nobody opened.\n *\n * The route answers 200 with `lookupFailed` for a delivery-log read\n * that could not run — a missing collection-group index is the likely\n * one — because the rest of the payload is still true. Rendering that\n * as an empty table is how a merchant concludes their campaign\n * reached nobody, which is the one wrong answer this table can give.\n */\n if (payload?.lookupFailed) {\n setRows([])\n setHasMore(false)\n setFailure(\n 'The delivery log could not be read, so this table is empty for ' +\n 'a reason that has nothing to do with your campaign. The ' +\n 'numbers above come from the campaigns themselves and are ' +\n 'unaffected.',\n )\n return\n }\n const nextRows: RecipientRow[] = Array.isArray(payload?.rows)\n ? payload.rows\n : []\n setRows(nextRows)\n setCampaignsOmitted(Number(payload?.campaignsOmitted ?? 0))\n setCampaignsRead(Number(payload?.campaignsRead ?? 0))\n const nextCursor: string | null = payload?.cursor ?? null\n setHasMore(Boolean(nextCursor))\n if (nextCursor) {\n setCursors((existing) => {\n if (existing[page + 1] === nextCursor) return existing\n const next = existing.slice(0, page + 1)\n next[page + 1] = nextCursor\n return next\n })\n }\n } catch (error) {\n console.error(error)\n if (active) {\n setRows([])\n setHasMore(false)\n setFailure('Could not read recipients')\n }\n } finally {\n if (active) setLoading(false)\n }\n })()\n return () => {\n active = false\n }\n }, [user, hostId, screenId, emailId, filter, cursor, page])\n\n // A new filter is a new result set, so the cursors collected under the old\n // one describe positions in a query that no longer exists.\n const handleFilter = useCallback((next: EngagementFilter) => {\n setFilter(next)\n setPage(0)\n setCursors([null])\n }, [])\n\n const columns = useMemo<GridColDef<RecipientRow>[]>(\n () => [\n {\n field: 'to',\n headerName: 'Recipient',\n flex: 1,\n minWidth: 240,\n renderCell: ({ row }) => (\n <Stack spacing={0.5} sx={{ py: 1, minWidth: 0, wordBreak: 'break-all' }}>\n <Typography variant=\"body2\">{row.to}</Typography>\n {/*\n * WHICH links, under the person who followed them. The\n * per-campaign link rollup counts destinations and names nobody;\n * this is the other half of the same question, and it is the half\n * a merchant asks when they want to know who to call.\n */}\n {row.clickedLinks.map((link) => (\n <Typography key={link} variant=\"caption\" color=\"text.secondary\">\n {link}\n </Typography>\n ))}\n </Stack>\n ),\n },\n ...(emailId\n ? []\n : [\n {\n field: 'subject',\n headerName: 'Email',\n flex: 1,\n minWidth: 180,\n valueGetter: (_value: unknown, row: RecipientRow) => row.subject ?? '—',\n } satisfies GridColDef<RecipientRow>,\n ]),\n {\n field: 'status',\n headerName: 'State',\n width: 130,\n renderCell: ({ row }) => <Chip size=\"small\" label={row.status} />,\n },\n {\n field: 'openCount',\n headerName: 'Opens',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {row.openCount.toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'clickCount',\n headerName: 'Clicks',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {row.clickCount.toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'lastEventAtMs',\n headerName: 'Last event',\n width: 190,\n renderCell: ({ row }) =>\n row.lastEventAtMs ? new Date(row.lastEventAtMs).toLocaleString() : '—',\n },\n ],\n [emailId],\n )\n\n return (\n <CardDisplay\n header={'Recipients'}\n help={recipientsDocsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(emailId\n ? 'One row per address this email was sent to, newest first. '\n : 'One row per message sent from this template, newest first. ') +\n 'Opens are counted by a tracking pixel a mail client may block ' +\n 'or pre-fetch, so an absent open is weaker evidence than a click.'}\n </Typography>\n\n <TextField\n select\n size=\"small\"\n label=\"Show\"\n value={filter}\n onChange={(event) =>\n handleFilter(event.target.value as EngagementFilter)\n }\n sx={{ alignSelf: 'flex-start', minWidth: 260 }}\n >\n {(Object.keys(FILTER_LABELS) as EngagementFilter[]).map((key) => (\n <MenuItem key={key} value={key}>\n {FILTER_LABELS[key]}\n </MenuItem>\n ))}\n </TextField>\n\n {failure ? <Alert severity=\"warning\">{failure}</Alert> : null}\n\n {campaignsOmitted ? (\n <Alert severity=\"info\">\n {'This template has been used by more emails than one read can ' +\n `span. These rows cover its ${campaignsRead} most recent ` +\n `emails; ${campaignsOmitted} older ` +\n `${campaignsOmitted === 1 ? 'email is' : 'emails are'} ` +\n 'not included.'}\n </Alert>\n ) : null}\n\n {rows.length === 0 && !loading && !failure ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {filter === 'all'\n ? 'No delivery records have been kept for this yet.'\n : 'Nobody in the delivery log matches that yet.'}\n </Typography>\n ) : (\n <ListTable\n aria-label=\"Recipients\"\n rows={rows}\n columns={columns}\n getRowId={(row: RecipientRow) => row.messageId}\n rowHeight={TABLE_ROW_HEIGHT}\n // A recipient carries the links they followed beneath the\n // address, so a row is as tall as its list of links.\n getRowHeight={() => 'auto'}\n // One page of a cursor feed, turned by the footer below: the grid\n // neither slices it nor filters the page and calls that the log.\n hideFooter\n disableColumnFilter\n quickFilter={false}\n />\n )}\n\n {/*\n * A cursor feed knows whether ANOTHER page exists and never how many\n * rows there are in total, so `hasMore` is passed and `count` is not\n * — the control renders \"1–25 of more than 25\" rather than inventing\n * a total nobody paid to count.\n */}\n <ListPagination\n page={page}\n pageSize={PAGE_SIZE}\n rowCount={rows.length}\n hasMore={hasMore}\n disabled={loading}\n onPageChange={setPage}\n />\n\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Up to ten distinct destinations are kept per recipient, so a ' +\n 'reader who clicked more links than that has the rest counted ' +\n 'but not listed.'}\n </Typography>\n </Stack>\n </CardDisplay>\n )\n}\nEmailRecipientsCard.displayName = 'EmailRecipientsCard'\n\nexport default EmailRecipientsCard\n"],"names":["pluginDocsHelp","CardDisplay","ListPagination","ListTable","TABLE_ROW_HEIGHT","useUser","authorizedFetch","Alert","Chip","MenuItem","Stack","TextField","Typography","useCallback","useEffect","useMemo","useState","PAGE_SIZE","FILTER_LABELS","all","opened","clicked","recipientsDocsHelp","anchor","excerpt","EmailRecipientsCard","props","cursors","hostId","screenId","emailId","data","user","filter","setFilter","page","setPage","setCursors","rows","setRows","hasMore","setHasMore","loading","setLoading","failure","setFailure","campaignsOmitted","setCampaignsOmitted","campaignsRead","setCampaignsRead","cursor","undefined","active","response","method","headers","body","JSON","stringify","payload","json","catch","ok","String","error","lookupFailed","nextRows","Array","isArray","Number","nextCursor","Boolean","existing","next","slice","console","handleFilter","columns","field","headerName","flex","minWidth","renderCell","row","spacing","sx","py","wordBreak","variant","to","clickedLinks","map","link","color","valueGetter","_value","subject","width","size","label","status","type","align","headerAlign","fontWeight","openCount","toLocaleString","clickCount","lastEventAtMs","Date","header","help","contentGutterX","contentGutterY","select","value","onChange","event","target","alignSelf","Object","keys","key","severity","length","aria-label","getRowId","messageId","rowHeight","getRowHeight","hideFooter","disableColumnFilter","quickFilter","pageSize","rowCount","disabled","onPageChange","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface EmailsListCardProps {
|
|
2
|
+
hostId: string;
|
|
3
|
+
/** The emails hub URL, so a row can link to the message's own page. */
|
|
4
|
+
basePath: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* EVERY MESSAGE, AS AGAINST EVERY CAMPAIGN.
|
|
8
|
+
*
|
|
9
|
+
* A campaign groups messages; this is the messages. The two are different
|
|
10
|
+
* questions — "how did the spring promotion do" and "what went out on the
|
|
11
|
+
* 14th, and to whom" — and they were previously the same list because a
|
|
12
|
+
* campaign document WAS a single send.
|
|
13
|
+
*
|
|
14
|
+
* ## Ordered in the browser, deliberately
|
|
15
|
+
*
|
|
16
|
+
* No SEND date is on every message: a sent one carries `sentAt` and a
|
|
17
|
+
* scheduled one carries `sendAtMs`, written by two different branches of the
|
|
18
|
+
* send path, and a draft carries neither — so `orderBy` on either would not
|
|
19
|
+
* mis-sort this list, it would DROP half of it. `collectionCeiling` reads a
|
|
20
|
+
* bounded window in document-id order and probes one past the ceiling, so the
|
|
21
|
+
* rows are sorted here and the reader is told when there are more.
|
|
22
|
+
*
|
|
23
|
+
* Every writer stamps a `createdAtMs`, which is the field this list could be
|
|
24
|
+
* ordered on in Firestore. Moving it there is a query change with the same
|
|
25
|
+
* hazard as the two above — `orderBy` drops a document missing the field —
|
|
26
|
+
* so it needs the corpus proven to carry one, not just the writers.
|
|
27
|
+
*
|
|
28
|
+
* The page is therefore a SLICE of a window this card already holds, not a
|
|
29
|
+
* query: paging an id-ordered walk and re-sorting each page by date would run
|
|
30
|
+
* in one order within a page and another across them.
|
|
31
|
+
*/
|
|
32
|
+
export declare function EmailsListCard(props: EmailsListCardProps): import("react").JSX.Element;
|
|
33
|
+
export declare namespace EmailsListCard {
|
|
34
|
+
var displayName: string;
|
|
35
|
+
}
|
|
36
|
+
export default EmailsListCard;
|