@aglyn/plugins-outreach 1.0.0-beta.196 → 1.0.0-beta.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/compliance-section.d.ts +3 -1
- package/src/lib/components/compliance-section.js +110 -45
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.d.ts +7 -1
- package/src/lib/components/enroll-dialog.js +106 -3
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/mailbox-card.js +11 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/outreach-ui.js +1 -0
- package/src/lib/components/outreach-ui.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +2 -0
- package/src/lib/components/sequence-editor.js +51 -26
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +7 -0
- package/src/lib/components/sequence-step-card.js +37 -14
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +12 -3
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/step-test-dialog.d.ts +31 -0
- package/src/lib/components/step-test-dialog.js +247 -0
- package/src/lib/components/step-test-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +8 -1
- package/src/lib/components/use-outreach-api.js +4 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +6 -1
- package/src/lib/components/use-outreach-data.js +6 -3
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +7 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/delivery-status.d.ts +8 -0
- package/src/lib/engine/delivery-status.js +10 -5
- package/src/lib/engine/delivery-status.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +2 -0
- package/src/lib/engine/enrollment-state.js +2 -2
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +28 -1
- package/src/lib/engine/gates.js +28 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +97 -0
- package/src/lib/engine/mail-gateway.js +226 -0
- package/src/lib/engine/mail-gateway.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +3 -3
- package/src/lib/engine/mailbox-health.js +17 -11
- package/src/lib/engine/mailbox-health.js.map +1 -1
- package/src/lib/engine/thread-classification.d.ts +2 -0
- package/src/lib/engine/thread-classification.js +2 -1
- package/src/lib/engine/thread-classification.js.map +1 -1
- package/src/lib/enrollment/enroll-people.d.ts +6 -1
- package/src/lib/enrollment/enroll-people.js +18 -4
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.d.ts +7 -0
- package/src/lib/enrollment/gate-lookups.js +11 -2
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +35 -1
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +85 -3
- package/src/lib/model/outreach.types.js +21 -2
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +7 -1
- package/src/lib/model/step-preview.js +3 -2
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +23 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +15 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -2
- package/src/lib/routes/platform-deps.js +2 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.d.ts +24 -0
- package/src/lib/routes/preview-routes.js +99 -95
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +27 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +6 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/step-test-routes.d.ts +88 -0
- package/src/lib/routes/step-test-routes.js +275 -0
- package/src/lib/routes/step-test-routes.js.map +1 -0
- package/src/lib/runtime/click-events.js +10 -0
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +11 -0
- package/src/lib/runtime/click-link.js +9 -4
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.js +3 -0
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +46 -0
- package/src/lib/runtime/gateway-ledger.js +113 -0
- package/src/lib/runtime/gateway-ledger.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.js +3 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +6 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +48 -0
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +40 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +107 -0
- package/src/lib/storage/domain-intel-store.js +320 -0
- package/src/lib/storage/domain-intel-store.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.198",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.198",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.198",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.198",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.198",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.198",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.198",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.198",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.198",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.198",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.198",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.198",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.198",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -7,7 +7,9 @@ export declare const OUTREACH_POSTAL_ADDRESS_HELP: string;
|
|
|
7
7
|
/**
|
|
8
8
|
* Sequences → Compliance (AGL-2980): who every email says sent it, the
|
|
9
9
|
* countries a sequence may send to at all, and — below the settings, with
|
|
10
|
-
* no Save of its own — the domains no sequence emails (AGL-3244).
|
|
10
|
+
* no Save of its own — the domains no sequence emails (AGL-3244). Each
|
|
11
|
+
* settings card saves its own fields from its header, the console's place
|
|
12
|
+
* for a card's actions (AGL-3333).
|
|
11
13
|
*
|
|
12
14
|
* The footer is previewed from what is typed, by the engine's own
|
|
13
15
|
* `composeOutreachFooter`, so the page shows the lines every email will end
|
|
@@ -21,7 +21,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
|
21
21
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
23
23
|
import { Alert, Autocomplete, Button, Chip, Paper, Stack, TextField, Typography } from "@mui/material";
|
|
24
|
-
import { useEffect, useMemo, useState } from "react";
|
|
24
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
25
25
|
import { composeOutreachFooter } from "../engine/compose.js";
|
|
26
26
|
import { outreachComplianceSettingsEqual, outreachCountryLabel, outreachCountryOptions, OUTREACH_BRAND_NAME_MAX, OUTREACH_LEGAL_NAME_MAX, OUTREACH_POSTAL_ADDRESS_MAX, validateOutreachComplianceSettings } from "../model/compliance-settings.js";
|
|
27
27
|
import { OutreachDoNotContactDomainsCard } from "./do-not-contact-domains.js";
|
|
@@ -37,10 +37,41 @@ const EMPTY = {
|
|
|
37
37
|
'US'
|
|
38
38
|
]
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* The page's cards and the settings each one owns. Every card saves only its
|
|
42
|
+
* own fields onto what is stored, so saving the countries never commits a
|
|
43
|
+
* half-edited sender identity, and the other way round.
|
|
44
|
+
*/ const CARDS = {
|
|
45
|
+
identity: {
|
|
46
|
+
fields: [
|
|
47
|
+
'legalName',
|
|
48
|
+
'brandName',
|
|
49
|
+
'postalAddress'
|
|
50
|
+
],
|
|
51
|
+
saved: 'Sender identity saved.'
|
|
52
|
+
},
|
|
53
|
+
countries: {
|
|
54
|
+
fields: [
|
|
55
|
+
'allowedCountries'
|
|
56
|
+
],
|
|
57
|
+
saved: 'Allowed countries saved.'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function pickCard(settings, card) {
|
|
61
|
+
return Object.fromEntries(CARDS[card].fields.map((field)=>[
|
|
62
|
+
field,
|
|
63
|
+
settings[field]
|
|
64
|
+
]));
|
|
65
|
+
}
|
|
66
|
+
function cardEqual(card, a, b) {
|
|
67
|
+
return outreachComplianceSettingsEqual(_extends({}, b, pickCard(a, card)), b);
|
|
68
|
+
}
|
|
40
69
|
/**
|
|
41
70
|
* Sequences → Compliance (AGL-2980): who every email says sent it, the
|
|
42
71
|
* countries a sequence may send to at all, and — below the settings, with
|
|
43
|
-
* no Save of its own — the domains no sequence emails (AGL-3244).
|
|
72
|
+
* no Save of its own — the domains no sequence emails (AGL-3244). Each
|
|
73
|
+
* settings card saves its own fields from its header, the console's place
|
|
74
|
+
* for a card's actions (AGL-3333).
|
|
44
75
|
*
|
|
45
76
|
* The footer is previewed from what is typed, by the engine's own
|
|
46
77
|
* `composeOutreachFooter`, so the page shows the lines every email will end
|
|
@@ -53,12 +84,27 @@ const EMPTY = {
|
|
|
53
84
|
const loaded = useOutreachComplianceSettings(api, orgId);
|
|
54
85
|
const { enqueueSnackbar } = useSnackbar();
|
|
55
86
|
const [form, setForm] = useState(EMPTY);
|
|
56
|
-
const [saving, setSaving] = useState(
|
|
87
|
+
const [saving, setSaving] = useState(null);
|
|
57
88
|
const [serverIssues, setServerIssues] = useState({});
|
|
58
89
|
const countries = useMemo(()=>outreachCountryOptions(), []);
|
|
59
90
|
const stored = loaded.settings;
|
|
91
|
+
const previousStored = useRef(null);
|
|
60
92
|
useEffect(()=>{
|
|
61
|
-
if (stored)
|
|
93
|
+
if (!stored) return;
|
|
94
|
+
const before = previousStored.current;
|
|
95
|
+
previousStored.current = stored;
|
|
96
|
+
// A fresh read replaces every card the member has not touched; a card
|
|
97
|
+
// with unsaved edits keeps them, so saving one card never wipes another.
|
|
98
|
+
setForm((current)=>{
|
|
99
|
+
if (!before) return _extends({}, stored);
|
|
100
|
+
let next = _extends({}, stored);
|
|
101
|
+
for (const card of Object.keys(CARDS)){
|
|
102
|
+
if (!cardEqual(card, current, before)) {
|
|
103
|
+
next = _extends({}, next, pickCard(current, card));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
62
108
|
}, [
|
|
63
109
|
stored
|
|
64
110
|
]);
|
|
@@ -68,37 +114,54 @@ const EMPTY = {
|
|
|
68
114
|
var _issues_find;
|
|
69
115
|
return (_serverIssues_field = serverIssues[field]) != null ? _serverIssues_field : (_issues_find = issues.find((issue)=>issue.field === field)) == null ? void 0 : _issues_find.message;
|
|
70
116
|
};
|
|
71
|
-
const
|
|
117
|
+
/** What saving one card would store: its own fields over the stored rest. */ const candidate = (card)=>validateOutreachComplianceSettings(stored ? _extends({}, stored, pickCard(form, card)) : form);
|
|
118
|
+
const cardDirty = (card)=>stored ? !outreachComplianceSettingsEqual(stored, candidate(card).settings) : false;
|
|
119
|
+
const cardBlocked = (card)=>candidate(card).issues.some((issue)=>CARDS[card].fields.includes(issue.field));
|
|
72
120
|
const footer = composeOutreachFooter(normalized);
|
|
73
121
|
const set = (field)=>(value)=>{
|
|
74
|
-
setServerIssues({
|
|
122
|
+
setServerIssues((previous)=>{
|
|
123
|
+
const next = _extends({}, previous);
|
|
124
|
+
delete next[field];
|
|
125
|
+
return next;
|
|
126
|
+
});
|
|
75
127
|
setForm((previous)=>_extends({}, previous, {
|
|
76
128
|
[field]: value
|
|
77
129
|
}));
|
|
78
130
|
};
|
|
79
|
-
const
|
|
80
|
-
|
|
131
|
+
const clearIssues = (card)=>setServerIssues((previous)=>{
|
|
132
|
+
const next = _extends({}, previous);
|
|
133
|
+
for (const field of CARDS[card].fields){
|
|
134
|
+
delete next[field];
|
|
135
|
+
}
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
const discard = (card)=>{
|
|
139
|
+
clearIssues(card);
|
|
140
|
+
if (stored) setForm((previous)=>_extends({}, previous, pickCard(stored, card)));
|
|
141
|
+
};
|
|
142
|
+
const save = async (card)=>{
|
|
143
|
+
setSaving(card);
|
|
81
144
|
try {
|
|
82
|
-
const answer = await api.saveSettings(
|
|
83
|
-
|
|
84
|
-
setForm(_extends({}, answer.settings));
|
|
145
|
+
const answer = await api.saveSettings(candidate(card).settings);
|
|
146
|
+
clearIssues(card);
|
|
147
|
+
setForm((previous)=>_extends({}, previous, pickCard(answer.settings, card)));
|
|
85
148
|
loaded.reload();
|
|
86
|
-
enqueueSnackbar(answer.changed ?
|
|
149
|
+
enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {
|
|
87
150
|
variant: answer.changed ? 'success' : 'info'
|
|
88
151
|
});
|
|
89
152
|
} catch (error) {
|
|
90
153
|
if (error instanceof OutreachRouteError && error.issues.length) {
|
|
91
|
-
setServerIssues(Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
setServerIssues((previous)=>_extends({}, previous, Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
|
|
155
|
+
issue.field,
|
|
156
|
+
issue.message
|
|
157
|
+
]))));
|
|
95
158
|
}
|
|
96
159
|
enqueueSnackbar(error.message, {
|
|
97
160
|
variant: 'error',
|
|
98
161
|
allowDuplicate: true
|
|
99
162
|
});
|
|
100
163
|
} finally{
|
|
101
|
-
setSaving(
|
|
164
|
+
setSaving(null);
|
|
102
165
|
}
|
|
103
166
|
};
|
|
104
167
|
if (loaded.status === 'loading' || !orgId && !stored) {
|
|
@@ -121,6 +184,29 @@ const EMPTY = {
|
|
|
121
184
|
name: outreachCountryLabel(code)
|
|
122
185
|
};
|
|
123
186
|
});
|
|
187
|
+
const cardActions = (card)=>{
|
|
188
|
+
const dirty = cardDirty(card);
|
|
189
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
190
|
+
direction: "row",
|
|
191
|
+
spacing: 1,
|
|
192
|
+
children: [
|
|
193
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
194
|
+
size: "small",
|
|
195
|
+
variant: "text",
|
|
196
|
+
disabled: !dirty || saving !== null,
|
|
197
|
+
onClick: ()=>discard(card),
|
|
198
|
+
children: "Discard changes"
|
|
199
|
+
}),
|
|
200
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
201
|
+
size: "small",
|
|
202
|
+
variant: "contained",
|
|
203
|
+
disabled: !dirty || saving !== null || cardBlocked(card),
|
|
204
|
+
onClick: ()=>void save(card),
|
|
205
|
+
children: saving === card ? 'Saving…' : 'Save'
|
|
206
|
+
})
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
};
|
|
124
210
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
125
211
|
spacing: 2,
|
|
126
212
|
children: [
|
|
@@ -129,6 +215,9 @@ const EMPTY = {
|
|
|
129
215
|
help: pluginDocsHelp('sequences', {
|
|
130
216
|
anchor: '#compliance-settings'
|
|
131
217
|
}),
|
|
218
|
+
HeaderProps: {
|
|
219
|
+
action: cardActions('identity')
|
|
220
|
+
},
|
|
132
221
|
contentGutterX: true,
|
|
133
222
|
contentGutterY: true,
|
|
134
223
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -215,6 +304,9 @@ const EMPTY = {
|
|
|
215
304
|
help: pluginDocsHelp('sequences', {
|
|
216
305
|
anchor: '#allowed-countries'
|
|
217
306
|
}),
|
|
307
|
+
HeaderProps: {
|
|
308
|
+
action: cardActions('countries')
|
|
309
|
+
},
|
|
218
310
|
contentGutterX: true,
|
|
219
311
|
contentGutterY: true,
|
|
220
312
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -232,7 +324,7 @@ const EMPTY = {
|
|
|
232
324
|
getOptionLabel: (option)=>option.name,
|
|
233
325
|
isOptionEqualToValue: (option, value)=>option.code === value.code,
|
|
234
326
|
onChange: (_event, next)=>{
|
|
235
|
-
|
|
327
|
+
clearIssues('countries');
|
|
236
328
|
setForm((previous)=>_extends({}, previous, {
|
|
237
329
|
allowedCountries: next.map((option)=>option.code)
|
|
238
330
|
}));
|
|
@@ -257,33 +349,6 @@ const EMPTY = {
|
|
|
257
349
|
]
|
|
258
350
|
})
|
|
259
351
|
}),
|
|
260
|
-
/*#__PURE__*/ _jsxs(Stack, {
|
|
261
|
-
direction: {
|
|
262
|
-
xs: 'column',
|
|
263
|
-
sm: 'row'
|
|
264
|
-
},
|
|
265
|
-
spacing: 1,
|
|
266
|
-
sx: {
|
|
267
|
-
justifyContent: 'flex-end'
|
|
268
|
-
},
|
|
269
|
-
children: [
|
|
270
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
271
|
-
variant: "text",
|
|
272
|
-
disabled: !dirty || saving,
|
|
273
|
-
onClick: ()=>{
|
|
274
|
-
setServerIssues({});
|
|
275
|
-
if (stored) setForm(_extends({}, stored));
|
|
276
|
-
},
|
|
277
|
-
children: "Discard changes"
|
|
278
|
-
}),
|
|
279
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
280
|
-
variant: "contained",
|
|
281
|
-
disabled: !dirty || saving || issues.length > 0,
|
|
282
|
-
onClick: ()=>void save(),
|
|
283
|
-
children: saving ? 'Saving…' : 'Save'
|
|
284
|
-
})
|
|
285
|
-
]
|
|
286
|
-
}),
|
|
287
352
|
/*#__PURE__*/ _jsx(OutreachDoNotContactDomainsCard, {
|
|
288
353
|
orgId: orgId
|
|
289
354
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState(false)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n useEffect(() => {\n if (stored) setForm({ ...stored })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n const dirty = stored\n ? !outreachComplianceSettingsEqual(stored, normalized)\n : false\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues({})\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const save = async () => {\n setSaving(true)\n try {\n const answer = await api.saveSettings(normalized)\n setServerIssues({})\n setForm({ ...answer.settings })\n loaded.reload()\n enqueueSnackbar(\n answer.changed ? 'Compliance settings saved.' : 'Nothing changed.',\n {\n variant: answer.changed ? 'success' : 'info',\n },\n )\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n setServerIssues({})\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n variant=\"text\"\n disabled={!dirty || saving}\n onClick={() => {\n setServerIssues({})\n if (stored) setForm({ ...stored })\n }}\n >\n Discard changes\n </Button>\n <Button\n variant=\"contained\"\n disabled={!dirty || saving || issues.length > 0}\n onClick={() => void save()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n\n {/* Its own list, saved as it is edited (AGL-3244): no Save above it. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","countries","stored","settings","normalized","issues","field","find","issue","message","dirty","footer","set","value","previous","save","answer","saveSettings","reload","changed","variant","error","length","Object","fromEntries","filter","map","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","spacing","header","help","anchor","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","next","renderValue","getItemProps","index","key","item","size","renderInput","params","direction","xs","sm","justifyContent","disabled","onClick","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,KAAK,EACLC,YAAY,EACZC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,qBAAqB,QAAQ,uBAAmB;AACzD,SACEC,+BAA+B,EAC/BC,oBAAoB,EACpBC,sBAAsB,EACtBC,uBAAuB,EACvBC,uBAAuB,EACvBC,2BAA2B,EAC3BC,kCAAkC,QAG7B,kCAA8B;AAErC,SAASC,+BAA+B,QAAQ,8BAA0B;AAC1E,SAASC,eAAe,EAAEC,mBAAmB,QAAQ,mBAAe;AACpE,SAASC,kBAAkB,EAAEC,cAAc,QAAQ,wBAAoB;AACvE,SAASC,6BAA6B,QAAQ,6BAAyB;AAOvE,uEAAuE,GACvE,OAAO,MAAMC,+BACX,qGACA,8EAA6E;AAE/E,MAAMC,QAAoC;IACxCC,WAAW;IACXC,WAAW;IACXC,eAAe;IACfC,kBAAkB;QAAC;KAAK;AAC1B;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,0BACdC,KAAqC;QAiHzBC,aAcAA,cAcAA;IA3IZ,MAAM,EAAEC,KAAK,EAAE,GAAGF;IAClB,MAAMG,MAAMZ,eAAeW;IAC3B,MAAME,SAASZ,8BAA8BW,KAAKD;IAClD,MAAM,EAAEG,eAAe,EAAE,GAAGtC;IAC5B,MAAM,CAACuC,MAAMC,QAAQ,GAAG7B,SAAqCgB;IAC7D,MAAM,CAACc,QAAQC,UAAU,GAAG/B,SAAS;IACrC,MAAM,CAACgC,cAAcC,gBAAgB,GAAGjC,SAEtC,CAAC;IACH,MAAMkC,YAAYnC,QAAQ,IAAMK,0BAA0B,EAAE;IAE5D,MAAM+B,SAAST,OAAOU,QAAQ;IAC9BtC,UAAU;QACR,IAAIqC,QAAQN,QAAQ,aAAKM;IAC3B,GAAG;QAACA;KAAO;IAEX,MAAM,EAAEC,UAAUC,UAAU,EAAEC,MAAM,EAAE,GACpC9B,mCAAmCoB;IACrC,MAAML,aAAa,CAACgB;YAClBP;YACAM;gBADAN,sBAAAA,YAAY,CAACO,MAAM,YAAnBP,uBACAM,eAAAA,OAAOE,IAAI,CAAC,CAACC,QAAUA,MAAMF,KAAK,KAAKA,2BAAvCD,aAA+CI,OAAO;;IACxD,MAAMC,QAAQR,SACV,CAACjC,gCAAgCiC,QAAQE,cACzC;IACJ,MAAMO,SAAS3C,sBAAsBoC;IACrC,MAAMQ,MAAM,CAACN,QAA4C,CAACO;YACxDb,gBAAgB,CAAC;YACjBJ,QAAQ,CAACkB,WAAc,aAAKA;oBAAU,CAACR,MAAM,EAAEO;;QACjD;IAEA,MAAME,OAAO;QACXjB,UAAU;QACV,IAAI;YACF,MAAMkB,SAAS,MAAMxB,IAAIyB,YAAY,CAACb;YACtCJ,gBAAgB,CAAC;YACjBJ,QAAQ,aAAKoB,OAAOb,QAAQ;YAC5BV,OAAOyB,MAAM;YACbxB,gBACEsB,OAAOG,OAAO,GAAG,+BAA+B,oBAChD;gBACEC,SAASJ,OAAOG,OAAO,GAAG,YAAY;YACxC;QAEJ,EAAE,OAAOE,OAAO;YACd,IAAIA,iBAAiB1C,sBAAsB0C,MAAMhB,MAAM,CAACiB,MAAM,EAAE;gBAC9DtB,gBACEuB,OAAOC,WAAW,CAChBH,MAAMhB,MAAM,CACToB,MAAM,CACL,CACEjB,QAIG,WAAWA,OAEjBkB,GAAG,CAAC,CAAClB,QAAU;wBAACA,MAAMF,KAAK;wBAAEE,MAAMC,OAAO;qBAAC;YAGpD;YACAf,gBAAgB,AAAC2B,MAAgBZ,OAAO,EAAE;gBACxCW,SAAS;gBACTO,gBAAgB;YAClB;QACF,SAAU;YACR7B,UAAU;QACZ;IACF;IAEA,IAAIL,OAAOmC,MAAM,KAAK,aAAc,CAACrC,SAAS,CAACW,QAAS;QACtD,qBAAO,KAACzB;YAAgBoD,OAAM;;IAChC;IACA,IAAIpC,OAAOmC,MAAM,KAAK,WAAWnC,OAAOmC,MAAM,KAAK,WAAW;QAC5D,qBACE,KAAClD;YACCkD,QAAQnC,OAAOmC,MAAM;YACrBE,MAAK;YACLrB,SAAShB,OAAOgB,OAAO;YACvBsB,SAAStC,OAAOyB,MAAM;;IAG5B;IAEA,MAAMc,oBAAoBrC,KAAKR,gBAAgB,CAACuC,GAAG,CACjD,CAACO;YACChC;gBAAAA,kBAAAA,UAAUM,IAAI,CAAC,CAAC2B,SAAWA,OAAOD,IAAI,KAAKA,iBAA3ChC,kBAAoD;YAClDgC;YACAE,MAAMjE,qBAAqB+D;QAC7B;;IAGJ,qBACE,MAACvE;QAAM0E,SAAS;;0BACd,KAACjF;gBACCkF,QAAO;gBACPC,MAAMpF,eAAe,aAAa;oBAAEqF,QAAQ;gBAAuB;gBACnEC,cAAc;gBACdC,cAAc;0BAEd,cAAA,MAAC/E;oBAAM0E,SAAS;;sCACd,KAACxE;4BAAWwD,SAAQ;4BAAQsB,OAAM;sCAC/B,0FACC,qFACA;;sCAEJ,KAAC/E;4BACCkE,OAAM;4BACNhB,OAAOlB,KAAKX,SAAS;4BACrB2D,UAAU,CAACC,QAAUhC,IAAI,aAAagC,MAAMC,MAAM,CAAChC,KAAK;4BACxDQ,OAAOyB,QAAQxD,WAAW;4BAC1ByD,UAAU,GACRzD,cAAAA,WAAW,wBAAXA,cACA;4BAEF0D,WAAW;gCACTC,WAAW;oCAAEC,WAAW7E,0BAA0B;gCAAG;4BACvD;4BACA8E,SAAS;;sCAEX,KAACxF;4BACCkE,OAAM;4BACNhB,OAAOlB,KAAKV,SAAS;4BACrB0D,UAAU,CAACC,QAAUhC,IAAI,aAAagC,MAAMC,MAAM,CAAChC,KAAK;4BACxDQ,OAAOyB,QAAQxD,WAAW;4BAC1ByD,UAAU,GACRzD,eAAAA,WAAW,wBAAXA,eACA;4BAEF0D,WAAW;gCACTC,WAAW;oCAAEC,WAAW9E,0BAA0B;gCAAG;4BACvD;4BACA+E,SAAS;;sCAEX,KAACxF;4BACCkE,OAAM;4BACNhB,OAAOlB,KAAKT,aAAa;4BACzByD,UAAU,CAACC,QAAUhC,IAAI,iBAAiBgC,MAAMC,MAAM,CAAChC,KAAK;4BAC5DQ,OAAOyB,QAAQxD,WAAW;4BAC1ByD,UAAU,GACRzD,eAAAA,WAAW,4BAAXA,eAA+BR;4BAEjCkE,WAAW;gCACTC,WAAW;oCAAEC,WAAW5E,8BAA8B;gCAAG;4BAC3D;4BACA8E,SAAS;4BACTC,SAAS;4BACTF,SAAS;;wBAEVxC,OAAOA,MAAM,iBACZ,MAACjD;4BAAM0E,SAAS;;8CACd,KAACxE;oCAAWwD,SAAQ;8CAAY;;8CAChC,KAAC3D;oCAAM2D,SAAQ;oCAAWkC,IAAI;wCAAEC,GAAG;oCAAI;8CACrC,cAAA,KAAC3F;wCACCwD,SAAQ;wCACRkC,IAAI;4CAAEE,YAAY;4CAAYC,WAAW;wCAAa;wCACtDC,eAAY;kDAEX/C,OAAOA,MAAM;;;;2CAKpB,KAACtD;4BAAMsG,UAAS;sCACb,8FACC;;;;;0BAMV,KAACxG;gBACCkF,QAAO;gBACPC,MAAMpF,eAAe,aAAa;oBAAEqF,QAAQ;gBAAqB;gBACjEC,cAAc;gBACdC,cAAc;0BAEd,cAAA,MAAC/E;oBAAM0E,SAAS;;sCACd,KAACxE;4BAAWwD,SAAQ;4BAAQsB,OAAM;sCAC/B,+FACC,wGACA,gGACA,gGACA;;sCAEJ,KAACpF;4BACCsG,QAAQ;4BACRC,SAAS5D;4BACTY,OAAOmB;4BACP8B,gBAAgB,CAAC5B,SAAWA,OAAOC,IAAI;4BACvC4B,sBAAsB,CAAC7B,QAAQrB,QAAUqB,OAAOD,IAAI,KAAKpB,MAAMoB,IAAI;4BACnEU,UAAU,CAACqB,QAAQC;gCACjBjE,gBAAgB,CAAC;gCACjBJ,QAAQ,CAACkB,WAAc,aAClBA;wCACH3B,kBAAkB8E,KAAKvC,GAAG,CAAC,CAACQ,SAAWA,OAAOD,IAAI;;4BAEtD;4BACAiC,aAAa,CAACrD,OAAOsD,eACnBtD,MAAMa,GAAG,CAAC,CAACQ,QAAQkC;oCACjB,MAAyBD,gBAAAA,aAAa;wCAAEC;oCAAM,IAAxC,EAAEC,GAAG,EAAW,GAAGF,eAATG,wCAASH;;;oCACzB,qBACE,KAAC3G;wCAAe+G,MAAK;wCAAQ1C,OAAOK,OAAOC,IAAI;uCAAMmC,OAA1CD;gCAEf;4BAEFG,aAAa,CAACC,uBACZ,KAAC9G,wBACK8G;oCACJ5C,OAAM;oCACNR,OAAOyB,QAAQxD,WAAW;oCAC1ByD,YAAYzD,WAAW;;;;;;0BAOjC,MAAC5B;gBACCgH,WAAW;oBAAEC,IAAI;oBAAUC,IAAI;gBAAM;gBACrCxC,SAAS;gBACTkB,IAAI;oBAAEuB,gBAAgB;gBAAW;;kCAEjC,KAACtH;wBACC6D,SAAQ;wBACR0D,UAAU,CAACpE,SAASb;wBACpBkF,SAAS;4BACP/E,gBAAgB,CAAC;4BACjB,IAAIE,QAAQN,QAAQ,aAAKM;wBAC3B;kCACD;;kCAGD,KAAC3C;wBACC6D,SAAQ;wBACR0D,UAAU,CAACpE,SAASb,UAAUQ,OAAOiB,MAAM,GAAG;wBAC9CyD,SAAS,IAAM,KAAKhE;kCAEnBlB,SAAS,YAAY;;;;0BAK1B,KAACrB;gBAAgCe,OAAOA;;;;AAG9C;AACAH,0BAA0B4F,WAAW,GAAG;AAExC,eAAe5F,0BAAyB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\ntype ComplianceCardKey = 'identity' | 'countries'\n\n/**\n * The page's cards and the settings each one owns. Every card saves only its\n * own fields onto what is stored, so saving the countries never commits a\n * half-edited sender identity, and the other way round.\n */\nconst CARDS: Record<\n ComplianceCardKey,\n { fields: readonly (keyof OutreachComplianceSettings)[]; saved: string }\n> = {\n identity: {\n fields: ['legalName', 'brandName', 'postalAddress'],\n saved: 'Sender identity saved.',\n },\n countries: {\n fields: ['allowedCountries'],\n saved: 'Allowed countries saved.',\n },\n}\n\nfunction pickCard(\n settings: OutreachComplianceSettings,\n card: ComplianceCardKey,\n): Partial<OutreachComplianceSettings> {\n return Object.fromEntries(\n CARDS[card].fields.map((field) => [field, settings[field]]),\n )\n}\n\nfunction cardEqual(\n card: ComplianceCardKey,\n a: OutreachComplianceSettings,\n b: OutreachComplianceSettings,\n): boolean {\n return outreachComplianceSettingsEqual({ ...b, ...pickCard(a, card) }, b)\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244). Each\n * settings card saves its own fields from its header, the console's place\n * for a card's actions (AGL-3333).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState<ComplianceCardKey | null>(null)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n const previousStored = useRef<OutreachComplianceSettings | null>(null)\n useEffect(() => {\n if (!stored) return\n const before = previousStored.current\n previousStored.current = stored\n // A fresh read replaces every card the member has not touched; a card\n // with unsaved edits keeps them, so saving one card never wipes another.\n setForm((current) => {\n if (!before) return { ...stored }\n let next: OutreachComplianceSettings = { ...stored }\n for (const card of Object.keys(CARDS) as ComplianceCardKey[]) {\n if (!cardEqual(card, current, before)) {\n next = { ...next, ...pickCard(current, card) }\n }\n }\n return next\n })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n /** What saving one card would store: its own fields over the stored rest. */\n const candidate = (card: ComplianceCardKey) =>\n validateOutreachComplianceSettings(\n stored ? { ...stored, ...pickCard(form, card) } : form,\n )\n const cardDirty = (card: ComplianceCardKey) =>\n stored\n ? !outreachComplianceSettingsEqual(stored, candidate(card).settings)\n : false\n const cardBlocked = (card: ComplianceCardKey) =>\n candidate(card).issues.some((issue) =>\n (CARDS[card].fields as readonly string[]).includes(issue.field),\n )\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues((previous) => {\n const next = { ...previous }\n delete next[field as OutreachComplianceField]\n return next\n })\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const clearIssues = (card: ComplianceCardKey) =>\n setServerIssues((previous) => {\n const next = { ...previous }\n for (const field of CARDS[card].fields) {\n delete next[field as OutreachComplianceField]\n }\n return next\n })\n\n const discard = (card: ComplianceCardKey) => {\n clearIssues(card)\n if (stored)\n setForm((previous) => ({ ...previous, ...pickCard(stored, card) }))\n }\n\n const save = async (card: ComplianceCardKey) => {\n setSaving(card)\n try {\n const answer = await api.saveSettings(candidate(card).settings)\n clearIssues(card)\n setForm((previous) => ({\n ...previous,\n ...pickCard(answer.settings, card),\n }))\n loaded.reload()\n enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {\n variant: answer.changed ? 'success' : 'info',\n })\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues((previous) => ({\n ...previous,\n ...Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n }))\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(null)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n const cardActions = (card: ComplianceCardKey) => {\n const dirty = cardDirty(card)\n return (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!dirty || saving !== null}\n onClick={() => discard(card)}\n >\n Discard changes\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!dirty || saving !== null || cardBlocked(card)}\n onClick={() => void save(card)}\n >\n {saving === card ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n )\n }\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n HeaderProps={{ action: cardActions('identity') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n HeaderProps={{ action: cardActions('countries') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n clearIssues('countries')\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n {/* Its own list, saved as it is edited (AGL-3244): it has no Save. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useRef","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","CARDS","identity","fields","saved","countries","pickCard","settings","card","Object","fromEntries","map","field","cardEqual","a","b","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","stored","previousStored","before","current","next","keys","normalized","issues","find","issue","message","candidate","cardDirty","cardBlocked","some","includes","footer","set","value","previous","clearIssues","discard","save","answer","saveSettings","reload","changed","variant","error","length","filter","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","cardActions","dirty","direction","spacing","size","disabled","onClick","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","renderValue","getItemProps","index","key","item","renderInput","params","displayName"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type OutreachEnrollPreviewPerson } from '../model/outreach-api';
|
|
1
|
+
import { type OutreachEnrollGateway, type OutreachEnrollPreviewPerson } from '../model/outreach-api';
|
|
2
2
|
import { type OutreachAttestationKind, type OutreachSequence } from '../model/outreach.types';
|
|
3
3
|
import { type OutreachCuratedDraftState } from './curated-step-editor';
|
|
4
4
|
import type { OutreachApi } from './use-outreach-api';
|
|
@@ -12,6 +12,12 @@ export interface OutreachEnrollDialogProps {
|
|
|
12
12
|
uid: string | null;
|
|
13
13
|
api: OutreachApi;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether the gateway in front of this person refused the sender this week
|
|
17
|
+
* (AGL-3326): the person is shown, and left un-ticked, rather than blocked
|
|
18
|
+
* — the founder decides, and ticking them is the decision.
|
|
19
|
+
*/
|
|
20
|
+
export declare function outreachGatewayRefusedThisWeek(gateway: OutreachEnrollGateway | null | undefined): boolean;
|
|
15
21
|
/** Where one person's curated drafts stand (AGL-3324). */
|
|
16
22
|
type Curation = {
|
|
17
23
|
status: 'idle';
|
|
@@ -20,6 +20,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
|
20
20
|
import { HelpTip } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import { Alert, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Paper, Stack, Tab, Tabs, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
22
22
|
import { useMemo, useState } from "react";
|
|
23
|
+
import { isOutreachGatewayFronted, outreachGatewayChip } from "../engine/mail-gateway.js";
|
|
23
24
|
import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
|
|
24
25
|
import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
|
|
25
26
|
import { outreachEmailStepLabel } from "./curate-step-dialog.js";
|
|
@@ -36,6 +37,20 @@ const STATUS_COLORS = {
|
|
|
36
37
|
needs_confirmation: 'warning',
|
|
37
38
|
blocked: 'default'
|
|
38
39
|
};
|
|
40
|
+
/** The gateway chip's color by its tone (AGL-3326). */ const GATEWAY_TONE_COLORS = {
|
|
41
|
+
refused: 'error',
|
|
42
|
+
delivered: 'success',
|
|
43
|
+
neutral: 'default',
|
|
44
|
+
blocked: 'default'
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Whether the gateway in front of this person refused the sender this week
|
|
48
|
+
* (AGL-3326): the person is shown, and left un-ticked, rather than blocked
|
|
49
|
+
* — the founder decides, and ticking them is the decision.
|
|
50
|
+
*/ export function outreachGatewayRefusedThisWeek(gateway) {
|
|
51
|
+
var _ref;
|
|
52
|
+
return ((_ref = gateway == null ? void 0 : gateway.blocked7) != null ? _ref : 0) > 0;
|
|
53
|
+
}
|
|
39
54
|
/** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied) {
|
|
40
55
|
var _ref;
|
|
41
56
|
return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).map(outreachDraftToOverride).filter((override)=>override !== null);
|
|
@@ -82,6 +97,7 @@ const STATUS_COLORS = {
|
|
|
82
97
|
const [problem, setProblem] = useState(null);
|
|
83
98
|
const [supplied, setSupplied] = useState({});
|
|
84
99
|
const [emailPreview, setEmailPreview] = useState({});
|
|
100
|
+
const [stepTests, setStepTests] = useState({});
|
|
85
101
|
const views = useOutreachSavedViews(props.open ? orgId : null, props.uid);
|
|
86
102
|
const search = useOutreachContactSearch({
|
|
87
103
|
orgId: props.open && tab === 'search' ? orgId : null,
|
|
@@ -103,6 +119,7 @@ const STATUS_COLORS = {
|
|
|
103
119
|
setProblem(null);
|
|
104
120
|
setSupplied({});
|
|
105
121
|
setEmailPreview({});
|
|
122
|
+
setStepTests({});
|
|
106
123
|
};
|
|
107
124
|
const close = ()=>{
|
|
108
125
|
reset();
|
|
@@ -123,7 +140,9 @@ const STATUS_COLORS = {
|
|
|
123
140
|
setSupplied(Object.fromEntries(answer.people.map((person)=>[
|
|
124
141
|
person.personId,
|
|
125
142
|
{
|
|
126
|
-
|
|
143
|
+
// A person whose gateway refused the sender this week starts
|
|
144
|
+
// un-ticked (AGL-3326); the chip beside them says why.
|
|
145
|
+
include: person.status !== 'blocked' && !outreachGatewayRefusedThisWeek(person.gateway),
|
|
127
146
|
personalLine: '',
|
|
128
147
|
attestations: []
|
|
129
148
|
}
|
|
@@ -279,6 +298,47 @@ const STATUS_COLORS = {
|
|
|
279
298
|
]))
|
|
280
299
|
});
|
|
281
300
|
};
|
|
301
|
+
/**
|
|
302
|
+
* This person's first email, sent to the member as a test (AGL-3325):
|
|
303
|
+
* their record, the personal line as written so far and the curated copy
|
|
304
|
+
* they used (AGL-3324) fill it, and nothing is enrolled or stored for
|
|
305
|
+
* them.
|
|
306
|
+
*/ const sendTest = async (person)=>{
|
|
307
|
+
setStepTests((previous)=>_extends({}, previous, {
|
|
308
|
+
[person.personId]: {
|
|
309
|
+
status: 'sending'
|
|
310
|
+
}
|
|
311
|
+
}));
|
|
312
|
+
try {
|
|
313
|
+
var _ref;
|
|
314
|
+
var _supplied_person_personId;
|
|
315
|
+
const overrides = usedOverrides(supplied[person.personId]);
|
|
316
|
+
const answer = await api.sendStepTest(_extends({
|
|
317
|
+
sequenceId: sequence.id
|
|
318
|
+
}, person.target === 'lead' && person.leadId ? {
|
|
319
|
+
leadId: person.leadId
|
|
320
|
+
} : {
|
|
321
|
+
contactId: person.contactId
|
|
322
|
+
}, {
|
|
323
|
+
personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
|
|
324
|
+
}, overrides.length ? {
|
|
325
|
+
stepOverrides: overrides
|
|
326
|
+
} : {}));
|
|
327
|
+
setStepTests((previous)=>_extends({}, previous, {
|
|
328
|
+
[person.personId]: {
|
|
329
|
+
status: 'sent',
|
|
330
|
+
answer
|
|
331
|
+
}
|
|
332
|
+
}));
|
|
333
|
+
} catch (error) {
|
|
334
|
+
setStepTests((previous)=>_extends({}, previous, {
|
|
335
|
+
[person.personId]: {
|
|
336
|
+
status: 'failed',
|
|
337
|
+
message: error.message
|
|
338
|
+
}
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
};
|
|
282
342
|
const title = `Enroll people in ${sequence.name}`;
|
|
283
343
|
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
284
344
|
open: props.open,
|
|
@@ -544,6 +604,8 @@ const STATUS_COLORS = {
|
|
|
544
604
|
onSupply: (next)=>supply(person.personId, next),
|
|
545
605
|
emailPreview: emailPreview[person.personId],
|
|
546
606
|
onPreviewEmail: ()=>void previewEmail(person),
|
|
607
|
+
stepTest: stepTests[person.personId],
|
|
608
|
+
onSendTest: ()=>void sendTest(person),
|
|
547
609
|
onCurate: ()=>void curate(person),
|
|
548
610
|
onWriteThemselves: ()=>writeThemselves(person),
|
|
549
611
|
disabled: stage.kind === 'enrolling'
|
|
@@ -616,11 +678,29 @@ const STATUS_COLORS = {
|
|
|
616
678
|
OutreachEnrollDialog.displayName = 'OutreachEnrollDialog';
|
|
617
679
|
function summary(answer) {
|
|
618
680
|
const count = (status)=>answer.people.filter((person)=>person.status === status).length;
|
|
619
|
-
|
|
681
|
+
// The people behind a security gateway (AGL-3326): a Barracuda, a
|
|
682
|
+
// Proofpoint or a Mimecast refuses a sender for a whole company, and a
|
|
683
|
+
// LinkedIn note first is the cheaper first touch there.
|
|
684
|
+
const fronted = answer.people.filter((person)=>{
|
|
685
|
+
var _person_gateway;
|
|
686
|
+
return person.status !== 'blocked' && isOutreachGatewayFronted((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.gateway);
|
|
687
|
+
}).length;
|
|
688
|
+
return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked` + (fronted ? ` · ${fronted} gateway-fronted — LinkedIn-first?` : '');
|
|
689
|
+
}
|
|
690
|
+
/** The Check-people chip for one person's mail gateway (AGL-3326), or nothing. */ function GatewayChip(props) {
|
|
691
|
+
const chip = outreachGatewayChip(props.gateway);
|
|
692
|
+
if (!chip) return null;
|
|
693
|
+
return /*#__PURE__*/ _jsx(Chip, {
|
|
694
|
+
size: "small",
|
|
695
|
+
variant: chip.tone === 'neutral' ? 'outlined' : 'filled',
|
|
696
|
+
color: GATEWAY_TONE_COLORS[chip.tone],
|
|
697
|
+
label: chip.label,
|
|
698
|
+
"aria-label": `Mail gateway for ${props.name}: ${chip.label}`
|
|
699
|
+
});
|
|
620
700
|
}
|
|
621
701
|
function PersonRow(props) {
|
|
622
702
|
var _ref, _ref1, _ref2, _ref3;
|
|
623
|
-
var _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
|
|
703
|
+
var _person_gateway, _person_gateway1, _person_gateway2, _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
|
|
624
704
|
const { person, supplied, disabled } = props;
|
|
625
705
|
const name = person.name || person.email || person.personId;
|
|
626
706
|
const line = (_ref = supplied == null ? void 0 : supplied.personalLine) != null ? _ref : '';
|
|
@@ -686,6 +766,10 @@ function PersonRow(props) {
|
|
|
686
766
|
variant: "outlined",
|
|
687
767
|
label: "Cold"
|
|
688
768
|
}) : null,
|
|
769
|
+
/*#__PURE__*/ _jsx(GatewayChip, {
|
|
770
|
+
gateway: person.gateway,
|
|
771
|
+
name: name
|
|
772
|
+
}),
|
|
689
773
|
/*#__PURE__*/ _jsx(Chip, {
|
|
690
774
|
size: "small",
|
|
691
775
|
label: STATUS_LABELS[person.status],
|
|
@@ -721,6 +805,11 @@ function PersonRow(props) {
|
|
|
721
805
|
}),
|
|
722
806
|
label: "Enroll this person"
|
|
723
807
|
}),
|
|
808
|
+
(supplied == null ? void 0 : supplied.include) === false && outreachGatewayRefusedThisWeek(person.gateway) ? /*#__PURE__*/ _jsx(Typography, {
|
|
809
|
+
variant: "caption",
|
|
810
|
+
color: "text.secondary",
|
|
811
|
+
children: `Left un-ticked: their mail gateway refused ${((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.blocked7) === 1 ? 'an email' : `${(_person_gateway1 = person.gateway) == null ? void 0 : _person_gateway1.blocked7} emails`} from this organization this week. Tick them to enroll anyway${((_person_gateway2 = person.gateway) == null ? void 0 : _person_gateway2.hold) ? ' — the email will go, and it is your call' : ''}.`
|
|
812
|
+
}) : null,
|
|
724
813
|
(supplied == null ? void 0 : supplied.include) === false ? null : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
725
814
|
children: [
|
|
726
815
|
/*#__PURE__*/ _jsx(TextField, {
|
|
@@ -773,6 +862,12 @@ function PersonRow(props) {
|
|
|
773
862
|
disabled: disabled,
|
|
774
863
|
children: "Preview the first email"
|
|
775
864
|
}),
|
|
865
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
866
|
+
size: "small",
|
|
867
|
+
onClick: props.onSendTest,
|
|
868
|
+
disabled: disabled || ((_props_stepTest = props.stepTest) == null ? void 0 : _props_stepTest.status) === 'sending',
|
|
869
|
+
children: ((_props_stepTest1 = props.stepTest) == null ? void 0 : _props_stepTest1.status) === 'sending' ? 'Sending…' : 'Send me this as a test'
|
|
870
|
+
}),
|
|
776
871
|
/*#__PURE__*/ _jsx(Button, {
|
|
777
872
|
size: "small",
|
|
778
873
|
onClick: props.onCurate,
|
|
@@ -781,6 +876,14 @@ function PersonRow(props) {
|
|
|
781
876
|
})
|
|
782
877
|
]
|
|
783
878
|
}),
|
|
879
|
+
((_props_stepTest2 = props.stepTest) == null ? void 0 : _props_stepTest2.status) === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
|
|
880
|
+
severity: "error",
|
|
881
|
+
children: props.stepTest.message
|
|
882
|
+
}) : null,
|
|
883
|
+
((_props_stepTest3 = props.stepTest) == null ? void 0 : _props_stepTest3.status) === 'sent' ? /*#__PURE__*/ _jsx(Alert, {
|
|
884
|
+
severity: "success",
|
|
885
|
+
children: `Sent to ${props.stepTest.answer.sentTo} as “${props.stepTest.answer.subject}”. ${name} was not enrolled.`
|
|
886
|
+
}) : null,
|
|
784
887
|
curation.status === 'drafting' ? /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
785
888
|
label: "Drafting their emails…"
|
|
786
889
|
}) : null,
|