@aglyn/plugins-email 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 +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ListImportDrawerProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
hostId: string;
|
|
5
|
+
listId: string;
|
|
6
|
+
listName: string;
|
|
7
|
+
/**
|
|
8
|
+
* Called whenever the run enrolls anybody.
|
|
9
|
+
*
|
|
10
|
+
* The audience page's subscriber total is a server aggregate taken once, so
|
|
11
|
+
* an import that ran to completion behind a stale count would read as
|
|
12
|
+
* having done nothing. Fired per run rather than once at the end: a large
|
|
13
|
+
* import is watched, and a total that only moves at the end is a total that
|
|
14
|
+
* looks stuck.
|
|
15
|
+
*/
|
|
16
|
+
onMembershipChanged?: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function ListImportDrawer(props: ListImportDrawerProps): import("react").JSX.Element;
|
|
19
|
+
export declare namespace ListImportDrawer {
|
|
20
|
+
var displayName: string;
|
|
21
|
+
}
|
|
22
|
+
export default ListImportDrawer;
|
|
@@ -0,0 +1,662 @@
|
|
|
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, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
/**
|
|
20
|
+
* BRINGING AN EXISTING LIST IN — the screen.
|
|
21
|
+
*
|
|
22
|
+
* `docs/specs/email-competitive-gaps.md` G5: a customer arriving from another
|
|
23
|
+
* product has a list and, until now, no way to bring it. The server half is
|
|
24
|
+
* `server-list-import.ts`; this is the surface that walks an operator through
|
|
25
|
+
* it.
|
|
26
|
+
*
|
|
27
|
+
* ## A drawer, because an import is three decisions and not a field
|
|
28
|
+
*
|
|
29
|
+
* Choose the file, read what is in it, then say whether you have these
|
|
30
|
+
* people's permission. Stacked above the membership table those three steps
|
|
31
|
+
* would have nowhere to grow — which is exactly how the audience card's
|
|
32
|
+
* create form came to offer four of a rule's nine fields — and the middle
|
|
33
|
+
* step is the one that must not be cramped: it is where the screening
|
|
34
|
+
* warnings and the consent readout are shown, and both exist to be READ
|
|
35
|
+
* before the operator attests.
|
|
36
|
+
*
|
|
37
|
+
* Creating is a drawer and picking is a dialog, so this composes
|
|
38
|
+
* `NavigationDrawerComponent` directly, the same way the suppressions card's
|
|
39
|
+
* Add does — the console's wrapper around it lives in an application a plugin
|
|
40
|
+
* library may not import.
|
|
41
|
+
*
|
|
42
|
+
* ## The numbers on screen are the server's, always
|
|
43
|
+
*
|
|
44
|
+
* Nothing here parses a file, screens it, or decides anything about consent.
|
|
45
|
+
* Every count and every sentence comes back from a route that read the
|
|
46
|
+
* merchant's actual contacts and both suppression lists. A screen that
|
|
47
|
+
* computed its own would be a second copy of the rule on the one surface
|
|
48
|
+
* whose job is to tell the operator the truth about what is about to happen.
|
|
49
|
+
*
|
|
50
|
+
* ## The run is a loop of bounded requests, and it says so
|
|
51
|
+
*
|
|
52
|
+
* A large file is not one request. The drawer calls `email/list-import-run`
|
|
53
|
+
* until it answers `complete`, showing the cursor against the total as it
|
|
54
|
+
* goes, so an import of forty thousand addresses is a progress bar rather
|
|
55
|
+
* than a spinner that either finishes or does not. Closing the drawer stops
|
|
56
|
+
* the loop and loses nothing: the job is durable, and reopening offers to
|
|
57
|
+
* resume it from where it stopped.
|
|
58
|
+
*/ import { LIST_IMPORT_MAX_CHARACTERS, pluginDocsHelp } from "@aglyn/aglyn";
|
|
59
|
+
import { HelpTip } from "@aglyn/shared-ui-jsx";
|
|
60
|
+
import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
|
|
61
|
+
import { Container, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
|
|
62
|
+
import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
|
|
63
|
+
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
64
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
65
|
+
import { Alert, AlertTitle, Button, Checkbox, Chip, FormControlLabel, IconButton, LinearProgress, Stack, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from "@mui/material";
|
|
66
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
67
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
68
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
69
|
+
/** What a refusal reason is called on screen. */ const REFUSAL_LABELS = {
|
|
70
|
+
declined: 'Declined marketing email',
|
|
71
|
+
'no-basis': 'No opt-in on record',
|
|
72
|
+
'suppressed-host': 'Unsubscribed from this site',
|
|
73
|
+
'suppressed-platform': 'Bounced or reported as spam',
|
|
74
|
+
'unroutable-address': 'Not a valid address',
|
|
75
|
+
'no-address': 'No address'
|
|
76
|
+
};
|
|
77
|
+
export function ListImportDrawer(props) {
|
|
78
|
+
const { open, onClose, hostId, listId, listName, onMembershipChanged } = props;
|
|
79
|
+
const { data: user } = useUser();
|
|
80
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
81
|
+
const [text, setText] = useState('');
|
|
82
|
+
const [fileName, setFileName] = useState('');
|
|
83
|
+
const [preview, setPreview] = useState(null);
|
|
84
|
+
const [attested, setAttested] = useState(false);
|
|
85
|
+
const [job, setJob] = useState(null);
|
|
86
|
+
const [busy, setBusy] = useState(false);
|
|
87
|
+
/**
|
|
88
|
+
* Set when the drawer closes mid-run, read by the loop.
|
|
89
|
+
*
|
|
90
|
+
* A ref and not state: the loop is an `async` function that has already
|
|
91
|
+
* captured its closure, so a state value it read would be the value from
|
|
92
|
+
* the render that started it — the loop would go on issuing requests after
|
|
93
|
+
* the operator closed the drawer, which is the one thing "closing stops it"
|
|
94
|
+
* has to mean.
|
|
95
|
+
*/ const abandoned = useRef(false);
|
|
96
|
+
const post = useCallback(async (route, body)=>{
|
|
97
|
+
const response = await authorizedFetch(user, `/api/email/${route}`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json'
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(_extends({
|
|
103
|
+
hostId,
|
|
104
|
+
listId
|
|
105
|
+
}, body))
|
|
106
|
+
});
|
|
107
|
+
const payload = await response.json().catch(()=>({}));
|
|
108
|
+
return {
|
|
109
|
+
ok: response.ok,
|
|
110
|
+
payload
|
|
111
|
+
};
|
|
112
|
+
}, [
|
|
113
|
+
user,
|
|
114
|
+
hostId,
|
|
115
|
+
listId
|
|
116
|
+
]);
|
|
117
|
+
/*
|
|
118
|
+
* An unfinished import is looked for when the DRAWER OPENS and at no other
|
|
119
|
+
* time — never on the page's mount. A merchant reading their audiences pays
|
|
120
|
+
* nothing for a feature they did not reach for, which is the read-cost rule
|
|
121
|
+
* `emails-console-read-cost.spec.tsx` meters.
|
|
122
|
+
*/ useEffect(()=>{
|
|
123
|
+
if (!open) return undefined;
|
|
124
|
+
abandoned.current = false;
|
|
125
|
+
let active = true;
|
|
126
|
+
void post('list-import-status', {}).then(({ ok, payload })=>{
|
|
127
|
+
if (!active || !ok) return;
|
|
128
|
+
const found = payload == null ? void 0 : payload.job;
|
|
129
|
+
if (found && found.status !== 'complete') setJob(found);
|
|
130
|
+
});
|
|
131
|
+
return ()=>{
|
|
132
|
+
active = false;
|
|
133
|
+
};
|
|
134
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
|
+
}, [
|
|
136
|
+
open
|
|
137
|
+
]);
|
|
138
|
+
const reset = useCallback(()=>{
|
|
139
|
+
setText('');
|
|
140
|
+
setFileName('');
|
|
141
|
+
setPreview(null);
|
|
142
|
+
setAttested(false);
|
|
143
|
+
setJob(null);
|
|
144
|
+
}, []);
|
|
145
|
+
const handleClose = useCallback(()=>{
|
|
146
|
+
// Stops the loop; the job survives and the next open offers to resume it.
|
|
147
|
+
abandoned.current = true;
|
|
148
|
+
onClose();
|
|
149
|
+
}, [
|
|
150
|
+
onClose
|
|
151
|
+
]);
|
|
152
|
+
const handleFile = useCallback(async (file)=>{
|
|
153
|
+
if (!file) return;
|
|
154
|
+
const contents = await file.text().catch(()=>'');
|
|
155
|
+
if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {
|
|
156
|
+
return void enqueueSnackbar('That file is too large to read in one go. Split it and import the ' + 'pieces.', {
|
|
157
|
+
variant: 'warning',
|
|
158
|
+
allowDuplicate: true
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
setFileName(file.name);
|
|
162
|
+
setText(contents);
|
|
163
|
+
setPreview(null);
|
|
164
|
+
setAttested(false);
|
|
165
|
+
}, [
|
|
166
|
+
enqueueSnackbar
|
|
167
|
+
]);
|
|
168
|
+
const handlePreview = useCallback(async ()=>{
|
|
169
|
+
if (busy || !text.trim()) return;
|
|
170
|
+
setBusy(true);
|
|
171
|
+
try {
|
|
172
|
+
const { ok, payload } = await post('list-import-preview', {
|
|
173
|
+
text
|
|
174
|
+
});
|
|
175
|
+
if (!ok) {
|
|
176
|
+
var _ref;
|
|
177
|
+
return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The file could not be read.', {
|
|
178
|
+
variant: 'warning',
|
|
179
|
+
allowDuplicate: true
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
setPreview(payload);
|
|
183
|
+
setAttested(false);
|
|
184
|
+
} catch (unused) {
|
|
185
|
+
enqueueSnackbar('The file could not be read.', {
|
|
186
|
+
variant: 'warning',
|
|
187
|
+
allowDuplicate: true
|
|
188
|
+
});
|
|
189
|
+
} finally{
|
|
190
|
+
setBusy(false);
|
|
191
|
+
}
|
|
192
|
+
}, [
|
|
193
|
+
busy,
|
|
194
|
+
text,
|
|
195
|
+
post,
|
|
196
|
+
enqueueSnackbar
|
|
197
|
+
]);
|
|
198
|
+
/**
|
|
199
|
+
* Drives one job to completion, one bounded request at a time.
|
|
200
|
+
*
|
|
201
|
+
* The loop's exit conditions are `complete`, an error, and the operator
|
|
202
|
+
* closing the drawer. It never guesses how many runs a file needs — the
|
|
203
|
+
* server answers `complete` when the cursor has reached the total, which is
|
|
204
|
+
* the only party that knows.
|
|
205
|
+
*/ const runToCompletion = useCallback(async (importId)=>{
|
|
206
|
+
setBusy(true);
|
|
207
|
+
try {
|
|
208
|
+
for(;;){
|
|
209
|
+
var _ref;
|
|
210
|
+
if (abandoned.current) return;
|
|
211
|
+
const { ok, payload } = await post('list-import-run', {
|
|
212
|
+
importId
|
|
213
|
+
});
|
|
214
|
+
if (!ok) {
|
|
215
|
+
var _ref1;
|
|
216
|
+
return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The import could not continue.', {
|
|
217
|
+
variant: 'warning',
|
|
218
|
+
allowDuplicate: true
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
setJob((previous)=>{
|
|
222
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
|
|
223
|
+
return {
|
|
224
|
+
importId,
|
|
225
|
+
status: (payload == null ? void 0 : payload.complete) ? 'complete' : 'running',
|
|
226
|
+
total: Number((_ref = (_ref1 = payload == null ? void 0 : payload.total) != null ? _ref1 : previous == null ? void 0 : previous.total) != null ? _ref : 0),
|
|
227
|
+
cursor: Number((_ref2 = payload == null ? void 0 : payload.cursor) != null ? _ref2 : 0),
|
|
228
|
+
enrolled: ((_ref3 = previous == null ? void 0 : previous.enrolled) != null ? _ref3 : 0) + Number((_ref4 = payload == null ? void 0 : payload.ranEnrolled) != null ? _ref4 : 0),
|
|
229
|
+
refused: ((_ref5 = previous == null ? void 0 : previous.refused) != null ? _ref5 : 0) + Number((_ref6 = payload == null ? void 0 : payload.ranRefused) != null ? _ref6 : 0),
|
|
230
|
+
refusals: mergeCounts((_ref7 = previous == null ? void 0 : previous.refusals) != null ? _ref7 : {}, (_ref8 = payload == null ? void 0 : payload.refusals) != null ? _ref8 : {}),
|
|
231
|
+
attested: (_ref9 = previous == null ? void 0 : previous.attested) != null ? _ref9 : false
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
if (Number((_ref = payload == null ? void 0 : payload.ranEnrolled) != null ? _ref : 0)) onMembershipChanged == null ? void 0 : onMembershipChanged();
|
|
235
|
+
if (payload == null ? void 0 : payload.complete) {
|
|
236
|
+
return void enqueueSnackbar('Import finished', {
|
|
237
|
+
variant: 'success',
|
|
238
|
+
persist: false
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} finally{
|
|
243
|
+
setBusy(false);
|
|
244
|
+
}
|
|
245
|
+
}, [
|
|
246
|
+
post,
|
|
247
|
+
enqueueSnackbar,
|
|
248
|
+
onMembershipChanged
|
|
249
|
+
]);
|
|
250
|
+
const handleStart = useCallback(async ()=>{
|
|
251
|
+
if (busy || !preview) return;
|
|
252
|
+
setBusy(true);
|
|
253
|
+
try {
|
|
254
|
+
var _ref, _ref1;
|
|
255
|
+
const { ok, payload } = await post('list-import-start', {
|
|
256
|
+
text,
|
|
257
|
+
// The operator's assertion, not a basis they named: the server derives
|
|
258
|
+
// a pass-through from each person's own record, and this flag can only
|
|
259
|
+
// ever add the attributable kind.
|
|
260
|
+
attestConsent: attested
|
|
261
|
+
});
|
|
262
|
+
if (!ok) {
|
|
263
|
+
var _ref2;
|
|
264
|
+
setBusy(false);
|
|
265
|
+
return void enqueueSnackbar((_ref2 = payload == null ? void 0 : payload.error) != null ? _ref2 : 'The import could not be started.', {
|
|
266
|
+
variant: 'warning',
|
|
267
|
+
allowDuplicate: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const started = {
|
|
271
|
+
importId: String((_ref = payload == null ? void 0 : payload.importId) != null ? _ref : ''),
|
|
272
|
+
status: 'running',
|
|
273
|
+
total: Number((_ref1 = payload == null ? void 0 : payload.total) != null ? _ref1 : 0),
|
|
274
|
+
cursor: 0,
|
|
275
|
+
enrolled: 0,
|
|
276
|
+
refused: 0,
|
|
277
|
+
refusals: {},
|
|
278
|
+
attested: (payload == null ? void 0 : payload.attested) === true
|
|
279
|
+
};
|
|
280
|
+
setJob(started);
|
|
281
|
+
setBusy(false);
|
|
282
|
+
await runToCompletion(started.importId);
|
|
283
|
+
} catch (unused) {
|
|
284
|
+
setBusy(false);
|
|
285
|
+
enqueueSnackbar('The import could not be started.', {
|
|
286
|
+
variant: 'warning',
|
|
287
|
+
allowDuplicate: true
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}, [
|
|
291
|
+
busy,
|
|
292
|
+
preview,
|
|
293
|
+
text,
|
|
294
|
+
attested,
|
|
295
|
+
post,
|
|
296
|
+
enqueueSnackbar,
|
|
297
|
+
runToCompletion
|
|
298
|
+
]);
|
|
299
|
+
const running = !!job && job.status !== 'complete';
|
|
300
|
+
const finished = !!job && job.status === 'complete';
|
|
301
|
+
return /*#__PURE__*/ _jsx(NavigationDrawerComponent, {
|
|
302
|
+
open: open,
|
|
303
|
+
anchor: "right",
|
|
304
|
+
variant: "temporary",
|
|
305
|
+
onClose: handleClose,
|
|
306
|
+
AppBarProps: {
|
|
307
|
+
color: 'surface'
|
|
308
|
+
},
|
|
309
|
+
appBarLeft: /*#__PURE__*/ _jsxs(_Fragment, {
|
|
310
|
+
children: [
|
|
311
|
+
/*#__PURE__*/ _jsxs(IconButton, {
|
|
312
|
+
color: "inherit",
|
|
313
|
+
edge: "start",
|
|
314
|
+
onClick: handleClose,
|
|
315
|
+
sx: {
|
|
316
|
+
mr: 2
|
|
317
|
+
},
|
|
318
|
+
children: [
|
|
319
|
+
/*#__PURE__*/ _jsx(MdiIcon, {
|
|
320
|
+
path: ICON_VARIANT_CLOSE.path
|
|
321
|
+
}),
|
|
322
|
+
/*#__PURE__*/ _jsx(SrOnly, {
|
|
323
|
+
children: 'close drawer'
|
|
324
|
+
})
|
|
325
|
+
]
|
|
326
|
+
}),
|
|
327
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
328
|
+
variant: "h6",
|
|
329
|
+
component: "div",
|
|
330
|
+
children: `Import into "${listName}"`
|
|
331
|
+
}),
|
|
332
|
+
/*#__PURE__*/ _jsx(HelpTip, _extends({}, pluginDocsHelp('emailCampaigns', {
|
|
333
|
+
anchor: '#import-a-list'
|
|
334
|
+
})))
|
|
335
|
+
]
|
|
336
|
+
}),
|
|
337
|
+
appBarRight: /*#__PURE__*/ _jsx(Button, {
|
|
338
|
+
variant: "outlined",
|
|
339
|
+
color: "inherit",
|
|
340
|
+
onClick: handleClose,
|
|
341
|
+
children: finished ? 'Done' : 'Cancel'
|
|
342
|
+
}),
|
|
343
|
+
children: /*#__PURE__*/ _jsx(Container, {
|
|
344
|
+
gutterY: true,
|
|
345
|
+
children: /*#__PURE__*/ _jsx(Stack, {
|
|
346
|
+
spacing: 2,
|
|
347
|
+
children: job ? /*#__PURE__*/ _jsx(ImportProgressPanel, {
|
|
348
|
+
job: job,
|
|
349
|
+
busy: busy,
|
|
350
|
+
onResume: ()=>void runToCompletion(job.importId),
|
|
351
|
+
onAnother: reset
|
|
352
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
353
|
+
children: [
|
|
354
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
355
|
+
variant: "body2",
|
|
356
|
+
color: "text.secondary",
|
|
357
|
+
children: 'A CSV with an email column, or one address per line. Every ' + 'address goes through the same checks as one you type in ' + 'by hand: anyone who unsubscribed, bounced, or told you ' + 'they do not want marketing email is refused, and nothing ' + 'you upload can override that.'
|
|
358
|
+
}),
|
|
359
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
360
|
+
direction: "row",
|
|
361
|
+
spacing: 1,
|
|
362
|
+
sx: {
|
|
363
|
+
alignItems: 'center'
|
|
364
|
+
},
|
|
365
|
+
children: [
|
|
366
|
+
/*#__PURE__*/ _jsxs(Button, {
|
|
367
|
+
variant: "outlined",
|
|
368
|
+
component: "label",
|
|
369
|
+
disabled: busy,
|
|
370
|
+
children: [
|
|
371
|
+
'Choose file',
|
|
372
|
+
/*#__PURE__*/ _jsx("input", {
|
|
373
|
+
type: "file",
|
|
374
|
+
accept: ".csv,.txt,text/csv,text/plain",
|
|
375
|
+
hidden: true,
|
|
376
|
+
onChange: (event)=>{
|
|
377
|
+
var _event_target_files;
|
|
378
|
+
return void handleFile((_event_target_files = event.target.files) == null ? void 0 : _event_target_files[0]);
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
}),
|
|
383
|
+
fileName ? /*#__PURE__*/ _jsx(Chip, {
|
|
384
|
+
size: "small",
|
|
385
|
+
variant: "outlined",
|
|
386
|
+
label: fileName
|
|
387
|
+
}) : null
|
|
388
|
+
]
|
|
389
|
+
}),
|
|
390
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
391
|
+
label: "Or paste the list",
|
|
392
|
+
value: text,
|
|
393
|
+
onChange: (event)=>{
|
|
394
|
+
setText(event.target.value);
|
|
395
|
+
setFileName('');
|
|
396
|
+
/*
|
|
397
|
+
* Any edit invalidates the answer. A preview belongs to the
|
|
398
|
+
* exact text it was run for, and leaving a stale one on
|
|
399
|
+
* screen would show counts, and an attestation checkbox,
|
|
400
|
+
* for a file the operator has since changed.
|
|
401
|
+
*/ setPreview(null);
|
|
402
|
+
setAttested(false);
|
|
403
|
+
},
|
|
404
|
+
multiline: true,
|
|
405
|
+
minRows: 4,
|
|
406
|
+
maxRows: 12,
|
|
407
|
+
fullWidth: true,
|
|
408
|
+
helperText: 'One address per line, or paste the whole CSV including ' + 'its header row.'
|
|
409
|
+
}),
|
|
410
|
+
preview ? /*#__PURE__*/ _jsx(ImportReviewPanel, {
|
|
411
|
+
preview: preview,
|
|
412
|
+
attested: attested,
|
|
413
|
+
onAttestedChange: setAttested
|
|
414
|
+
}) : null,
|
|
415
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
416
|
+
direction: "row",
|
|
417
|
+
spacing: 1,
|
|
418
|
+
sx: {
|
|
419
|
+
justifyContent: 'flex-end'
|
|
420
|
+
},
|
|
421
|
+
children: preview ? /*#__PURE__*/ _jsx(Button, {
|
|
422
|
+
variant: "contained",
|
|
423
|
+
disabled: busy || preview.usable === 0,
|
|
424
|
+
onClick: ()=>void handleStart(),
|
|
425
|
+
children: attested ? `Import ${preview.usable} addresses` : `Import the ${preview.optedIn} with an opt-in`
|
|
426
|
+
}) : /*#__PURE__*/ _jsx(Button, {
|
|
427
|
+
variant: "contained",
|
|
428
|
+
disabled: busy || !text.trim(),
|
|
429
|
+
onClick: ()=>void handlePreview(),
|
|
430
|
+
children: busy ? 'Checking…' : 'Check this file'
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
]
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
ListImportDrawer.displayName = 'ListImportDrawer';
|
|
440
|
+
/** Adds two refusal tallies together. */ function mergeCounts(base, extra) {
|
|
441
|
+
const merged = _extends({}, base);
|
|
442
|
+
for (const [key, value] of Object.entries(extra)){
|
|
443
|
+
var _merged_key;
|
|
444
|
+
merged[key] = ((_merged_key = merged[key]) != null ? _merged_key : 0) + Number(value != null ? value : 0);
|
|
445
|
+
}
|
|
446
|
+
return merged;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* WHAT IS IN THE FILE, and the one question the operator has to answer.
|
|
450
|
+
*
|
|
451
|
+
* Three blocks in the order they should be read: what the file contains, what
|
|
452
|
+
* the screening noticed, and what the consent gate said about a sample. The
|
|
453
|
+
* attestation control comes last on purpose — it is the act, and an act
|
|
454
|
+
* offered above its own evidence is an act taken without it.
|
|
455
|
+
*/ function ImportReviewPanel(props) {
|
|
456
|
+
const { preview, attested, onAttestedChange } = props;
|
|
457
|
+
const { screening } = preview;
|
|
458
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
459
|
+
spacing: 2,
|
|
460
|
+
children: [
|
|
461
|
+
/*#__PURE__*/ _jsxs(Alert, {
|
|
462
|
+
severity: "info",
|
|
463
|
+
children: [
|
|
464
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
465
|
+
children: `${preview.usable} addresses in this file`
|
|
466
|
+
}),
|
|
467
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
468
|
+
spacing: 0.5,
|
|
469
|
+
children: [
|
|
470
|
+
preview.duplicates ? /*#__PURE__*/ _jsx(Typography, {
|
|
471
|
+
variant: "body2",
|
|
472
|
+
children: `${preview.duplicates} repeated addresses were collapsed — the ` + 'same person twice is one member.'
|
|
473
|
+
}) : null,
|
|
474
|
+
preview.unusable ? /*#__PURE__*/ _jsx(Typography, {
|
|
475
|
+
variant: "body2",
|
|
476
|
+
children: `${preview.unusable} lines are not email addresses and will be ` + `skipped${preview.unusableSamples.length ? `, starting with: ${preview.unusableSamples.slice(0, 3).join(', ')}` : ''}.`
|
|
477
|
+
}) : null,
|
|
478
|
+
preview.overCeiling ? /*#__PURE__*/ _jsx(Typography, {
|
|
479
|
+
variant: "body2",
|
|
480
|
+
children: `This file names more than ${preview.ceiling} addresses. Only ` + 'the first are staged — split the file and import the rest ' + 'separately. Nothing already on the list is affected.'
|
|
481
|
+
}) : null
|
|
482
|
+
]
|
|
483
|
+
})
|
|
484
|
+
]
|
|
485
|
+
}),
|
|
486
|
+
screening.purchaseTellColumns.length ? /*#__PURE__*/ _jsxs(Alert, {
|
|
487
|
+
severity: "warning",
|
|
488
|
+
children: [
|
|
489
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
490
|
+
children: 'This file looks bought or appended'
|
|
491
|
+
}),
|
|
492
|
+
`Its columns include ${screening.purchaseTellColumns.join(', ')}. ` + 'Purchased, rented and appended lists are not allowed, and ' + 'importing one puts every site sending through this domain at ' + 'risk. If the column name is a coincidence, carry on.'
|
|
493
|
+
]
|
|
494
|
+
}) : null,
|
|
495
|
+
screening.roleAccounts ? /*#__PURE__*/ _jsxs(Alert, {
|
|
496
|
+
severity: "warning",
|
|
497
|
+
children: [
|
|
498
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
499
|
+
children: `${screening.roleAccounts} shared mailboxes`
|
|
500
|
+
}),
|
|
501
|
+
`Addresses like ${screening.roleAccountSamples.slice(0, 3).join(', ')} reach a mailbox several people read, or nobody. ` + 'They are a common sign of a list built by collecting addresses ' + 'rather than by people signing up. They will be imported if you ' + 'go ahead.'
|
|
502
|
+
]
|
|
503
|
+
}) : null,
|
|
504
|
+
/*#__PURE__*/ _jsxs(Alert, {
|
|
505
|
+
severity: preview.needAttestation ? 'warning' : 'success',
|
|
506
|
+
children: [
|
|
507
|
+
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
508
|
+
children: preview.sampleSize < preview.usable ? `Checked the first ${preview.sampleSize} of ${preview.usable}` : 'Checked every address'
|
|
509
|
+
}),
|
|
510
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
511
|
+
spacing: 0.5,
|
|
512
|
+
children: [
|
|
513
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
514
|
+
variant: "body2",
|
|
515
|
+
children: `${preview.optedIn} already have an opt-in on record, ` + `${preview.needAttestation} have no opt-in on record, and ` + `${preview.refused} cannot be added at all.`
|
|
516
|
+
}),
|
|
517
|
+
preview.sampleSize < preview.usable ? /*#__PURE__*/ _jsx(Typography, {
|
|
518
|
+
variant: "body2",
|
|
519
|
+
children: 'The rest are checked the same way as the import runs, and ' + 'the result is reported as it goes.'
|
|
520
|
+
}) : null
|
|
521
|
+
]
|
|
522
|
+
})
|
|
523
|
+
]
|
|
524
|
+
}),
|
|
525
|
+
preview.verdicts.length ? /*#__PURE__*/ _jsxs(ScrollTable, {
|
|
526
|
+
size: "small",
|
|
527
|
+
children: [
|
|
528
|
+
/*#__PURE__*/ _jsx(TableHead, {
|
|
529
|
+
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
530
|
+
children: [
|
|
531
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
532
|
+
children: 'Address'
|
|
533
|
+
}),
|
|
534
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
535
|
+
children: 'What we know'
|
|
536
|
+
})
|
|
537
|
+
]
|
|
538
|
+
})
|
|
539
|
+
}),
|
|
540
|
+
/*#__PURE__*/ _jsx(TableBody, {
|
|
541
|
+
children: preview.verdicts.slice(0, 10).map((verdict, index)=>{
|
|
542
|
+
var _verdict_email;
|
|
543
|
+
return /*#__PURE__*/ _jsxs(TableRow, {
|
|
544
|
+
children: [
|
|
545
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
546
|
+
children: (_verdict_email = verdict.email) != null ? _verdict_email : verdict.input
|
|
547
|
+
}),
|
|
548
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
549
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
550
|
+
variant: "body2",
|
|
551
|
+
color: "text.secondary",
|
|
552
|
+
children: verdict.summary
|
|
553
|
+
})
|
|
554
|
+
})
|
|
555
|
+
]
|
|
556
|
+
}, `${verdict.input}-${index}`);
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
]
|
|
560
|
+
}) : null,
|
|
561
|
+
preview.needAttestation ? /*#__PURE__*/ _jsx(FormControlLabel, {
|
|
562
|
+
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
563
|
+
checked: attested,
|
|
564
|
+
onChange: (event)=>onAttestedChange(event.target.checked)
|
|
565
|
+
}),
|
|
566
|
+
label: /*#__PURE__*/ _jsxs(Typography, {
|
|
567
|
+
variant: "body2",
|
|
568
|
+
children: [
|
|
569
|
+
'I have these people’s permission to send them marketing ' + 'email, and I can produce the record of it if asked. This ' + 'statement is stored against my account, with today’s date, ' + 'on every address it admits.',
|
|
570
|
+
screening.declaresBasis ? ' The opt-in source and date your file declares are kept with it.' : ''
|
|
571
|
+
]
|
|
572
|
+
})
|
|
573
|
+
}) : null,
|
|
574
|
+
preview.needAttestation && !attested ? /*#__PURE__*/ _jsx(Typography, {
|
|
575
|
+
variant: "body2",
|
|
576
|
+
color: "text.secondary",
|
|
577
|
+
children: 'Without that statement the import adds only the addresses that ' + 'already have an opt-in on record. Nobody else is added, and ' + 'nothing is deleted.'
|
|
578
|
+
}) : null
|
|
579
|
+
]
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/** How far the run has got, and what it did. */ function ImportProgressPanel(props) {
|
|
583
|
+
const { job, busy, onResume, onAnother } = props;
|
|
584
|
+
const done = job.status === 'complete';
|
|
585
|
+
const percent = job.total ? Math.min(job.cursor / job.total * 100, 100) : 0;
|
|
586
|
+
const refusals = Object.entries(job.refusals).filter(([, count])=>count > 0);
|
|
587
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
588
|
+
spacing: 2,
|
|
589
|
+
children: [
|
|
590
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
591
|
+
variant: "body2",
|
|
592
|
+
color: "text.secondary",
|
|
593
|
+
children: done ? `Finished. ${job.enrolled} of ${job.total} addresses were added.` : `${job.cursor} of ${job.total} addresses checked.`
|
|
594
|
+
}),
|
|
595
|
+
/*#__PURE__*/ _jsx(LinearProgress, {
|
|
596
|
+
variant: "determinate",
|
|
597
|
+
value: percent,
|
|
598
|
+
sx: {
|
|
599
|
+
borderRadius: 1
|
|
600
|
+
}
|
|
601
|
+
}),
|
|
602
|
+
refusals.length ? /*#__PURE__*/ _jsxs(ScrollTable, {
|
|
603
|
+
size: "small",
|
|
604
|
+
children: [
|
|
605
|
+
/*#__PURE__*/ _jsx(TableHead, {
|
|
606
|
+
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
607
|
+
children: [
|
|
608
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
609
|
+
children: 'Not added'
|
|
610
|
+
}),
|
|
611
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
612
|
+
align: "right",
|
|
613
|
+
children: 'Addresses'
|
|
614
|
+
})
|
|
615
|
+
]
|
|
616
|
+
})
|
|
617
|
+
}),
|
|
618
|
+
/*#__PURE__*/ _jsx(TableBody, {
|
|
619
|
+
children: refusals.map(([reason, count])=>{
|
|
620
|
+
var _REFUSAL_LABELS_reason;
|
|
621
|
+
return /*#__PURE__*/ _jsxs(TableRow, {
|
|
622
|
+
children: [
|
|
623
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
624
|
+
children: (_REFUSAL_LABELS_reason = REFUSAL_LABELS[reason]) != null ? _REFUSAL_LABELS_reason : reason
|
|
625
|
+
}),
|
|
626
|
+
/*#__PURE__*/ _jsx(TableCell, {
|
|
627
|
+
align: "right",
|
|
628
|
+
children: count
|
|
629
|
+
})
|
|
630
|
+
]
|
|
631
|
+
}, reason);
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
]
|
|
635
|
+
}) : null,
|
|
636
|
+
!done ? /*#__PURE__*/ _jsx(Alert, {
|
|
637
|
+
severity: "info",
|
|
638
|
+
children: 'Large files are added in batches, so this takes a few minutes. ' + 'You can close this — the import picks up where it stopped when ' + 'you come back.'
|
|
639
|
+
}) : null,
|
|
640
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
641
|
+
direction: "row",
|
|
642
|
+
spacing: 1,
|
|
643
|
+
sx: {
|
|
644
|
+
justifyContent: 'flex-end'
|
|
645
|
+
},
|
|
646
|
+
children: done ? /*#__PURE__*/ _jsx(Button, {
|
|
647
|
+
variant: "outlined",
|
|
648
|
+
onClick: onAnother,
|
|
649
|
+
children: 'Import another file'
|
|
650
|
+
}) : /*#__PURE__*/ _jsx(Button, {
|
|
651
|
+
variant: "contained",
|
|
652
|
+
disabled: busy,
|
|
653
|
+
onClick: onResume,
|
|
654
|
+
children: busy ? 'Importing…' : 'Resume import'
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
]
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
export default ListImportDrawer;
|
|
661
|
+
|
|
662
|
+
//# sourceMappingURL=list-import-drawer.js.map
|