@aglyn/plugins-outreach 1.0.0-beta.220 → 1.0.0-beta.222
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/do-not-contact-domains.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.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.222",
|
|
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.222",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.222",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.222",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.222",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.222",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.222",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.222",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.222",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.222",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.222",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.222",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.222",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.222",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OUTREACH_DO_NOT_CONTACT_REASON_LABELS } from '../model/outreach.types';
|
|
2
2
|
export interface OutreachDoNotContactDomainsCardProps {
|
|
3
3
|
/** The organization the hub is mounted under; `null` until the shell knows it. */
|
|
4
4
|
orgId: string | null;
|
|
5
5
|
}
|
|
6
6
|
/** Why a domain is on the list, as the card says it. */
|
|
7
|
-
export
|
|
7
|
+
export { OUTREACH_DO_NOT_CONTACT_REASON_LABELS };
|
|
8
8
|
/** Accessible names of the card's controls, spelled once for the specs. */
|
|
9
9
|
export declare const DO_NOT_CONTACT_DOMAIN_LABELS: {
|
|
10
10
|
readonly field: "Domain";
|
|
11
11
|
readonly add: "Add domain";
|
|
12
12
|
readonly remove: (domain: string) => string;
|
|
13
|
+
readonly import: "Import";
|
|
14
|
+
readonly export: "Export";
|
|
13
15
|
};
|
|
16
|
+
/** What the export dialog calls the list's search and filters. */
|
|
17
|
+
export declare const DO_NOT_CONTACT_EXPORT_FILTER_LABEL = "The domains matching this list\u2019s search and filters";
|
|
14
18
|
/** What the field says under a value that is not a domain. */
|
|
15
19
|
export declare const DO_NOT_CONTACT_DOMAIN_HINT = "A domain, such as example.com. Every address at it is refused.";
|
|
16
20
|
/**
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
18
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
import {
|
|
19
|
+
import { TransferResumeImport } from "@aglyn/aglyn/app-utils/transfer-resume-import";
|
|
20
|
+
import { pluginDocsHelp, useTransferLauncher } from "@aglyn/aglyn";
|
|
19
21
|
import { mdiTrashCanOutline } from "@aglyn/shared-data-mdi";
|
|
20
22
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
23
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
@@ -27,24 +29,22 @@ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filt
|
|
|
27
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
28
30
|
import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
|
|
29
31
|
import { useState } from "react";
|
|
32
|
+
import { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY } from "../constants/transfer-resources.js";
|
|
30
33
|
import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
|
|
31
34
|
import { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from "../model/do-not-contact-domain-list-query.js";
|
|
32
|
-
import { OUTREACH_DO_NOT_CONTACT_REASONS } from "../model/outreach.types.js";
|
|
35
|
+
import { OUTREACH_DO_NOT_CONTACT_REASON_LABELS, OUTREACH_DO_NOT_CONTACT_REASONS } from "../model/outreach.types.js";
|
|
33
36
|
import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
34
37
|
import { useOutreachApi } from "./use-outreach-api.js";
|
|
35
38
|
import { useOutreachDoNotContactDomainList } from "./use-outreach-data.js";
|
|
36
|
-
/** Why a domain is on the list, as the card says it. */ export
|
|
37
|
-
manual: 'Added by a member',
|
|
38
|
-
opt_out_reply: 'A reply asked not to be emailed',
|
|
39
|
-
unsubscribe: 'Unsubscribed',
|
|
40
|
-
hard_bounce: 'Mail bounced',
|
|
41
|
-
gateway_block: 'Its mail gateway blocked the sender'
|
|
42
|
-
};
|
|
39
|
+
/** Why a domain is on the list, as the card says it. */ export { OUTREACH_DO_NOT_CONTACT_REASON_LABELS };
|
|
43
40
|
/** Accessible names of the card's controls, spelled once for the specs. */ export const DO_NOT_CONTACT_DOMAIN_LABELS = {
|
|
44
41
|
field: 'Domain',
|
|
45
42
|
add: 'Add domain',
|
|
46
|
-
remove: (domain)=>`Remove ${domain}
|
|
43
|
+
remove: (domain)=>`Remove ${domain}`,
|
|
44
|
+
import: 'Import',
|
|
45
|
+
export: 'Export'
|
|
47
46
|
};
|
|
47
|
+
/** What the export dialog calls the list's search and filters. */ export const DO_NOT_CONTACT_EXPORT_FILTER_LABEL = 'The domains matching this list’s search and filters';
|
|
48
48
|
/** What the field says under a value that is not a domain. */ export const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.';
|
|
49
49
|
/*
|
|
50
50
|
* What the domains grid's Filters panel offers (AGL-3317), each field on the
|
|
@@ -63,6 +63,16 @@ const DOMAIN_FILTER_OPTIONS = {
|
|
|
63
63
|
label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason]
|
|
64
64
|
}))
|
|
65
65
|
};
|
|
66
|
+
/** The grid toolbar without its page-only CSV and print export. */ const GRID_EXPORT_OFF = {
|
|
67
|
+
toolbar: {
|
|
68
|
+
csvOptions: {
|
|
69
|
+
disableToolbarButton: true
|
|
70
|
+
},
|
|
71
|
+
printOptions: {
|
|
72
|
+
disableToolbarButton: true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
66
76
|
function addedOn(entry) {
|
|
67
77
|
if (!entry.addedAtMs) return '';
|
|
68
78
|
return new Intl.DateTimeFormat('en-US', {
|
|
@@ -168,6 +178,50 @@ function addedOn(entry) {
|
|
|
168
178
|
};
|
|
169
179
|
});
|
|
170
180
|
/** Nothing listed at all — not a filter that matched nothing. */ const none = listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0;
|
|
181
|
+
/*
|
|
182
|
+
* Import adds domains and addresses from a file; Export downloads the
|
|
183
|
+
* domains, narrowed to the list's search and filters when it has any (the
|
|
184
|
+
* export reads them as the same Firestore query). Outside the console
|
|
185
|
+
* shell there is no launcher, and no buttons.
|
|
186
|
+
*/ const transfer = useTransferLauncher();
|
|
187
|
+
const transferTarget = {
|
|
188
|
+
resource: OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY,
|
|
189
|
+
scope: 'org'
|
|
190
|
+
};
|
|
191
|
+
// Import for those who may write the records, Export for those who may read them.
|
|
192
|
+
const canImport = Boolean(transfer == null ? void 0 : transfer.can('import', transferTarget));
|
|
193
|
+
const canExport = Boolean(transfer == null ? void 0 : transfer.can('export', transferTarget));
|
|
194
|
+
const transferActions = transfer && (canImport || canExport) ? /*#__PURE__*/ _jsxs(Stack, {
|
|
195
|
+
direction: "row",
|
|
196
|
+
spacing: 1,
|
|
197
|
+
children: [
|
|
198
|
+
/*#__PURE__*/ _jsx(TransferResumeImport, {
|
|
199
|
+
target: transferTarget
|
|
200
|
+
}),
|
|
201
|
+
canImport && /*#__PURE__*/ _jsx(Button, {
|
|
202
|
+
size: "small",
|
|
203
|
+
onClick: ()=>transfer.openImport(transferTarget),
|
|
204
|
+
children: DO_NOT_CONTACT_DOMAIN_LABELS.import
|
|
205
|
+
}),
|
|
206
|
+
canExport && /*#__PURE__*/ _jsx(Button, {
|
|
207
|
+
size: "small",
|
|
208
|
+
onClick: ()=>transfer.openExport(_extends({}, transferTarget, filtering ? {
|
|
209
|
+
filter: {
|
|
210
|
+
label: DO_NOT_CONTACT_EXPORT_FILTER_LABEL,
|
|
211
|
+
value: {
|
|
212
|
+
clauses: gridFilter.clauses.map(({ field, op, value })=>({
|
|
213
|
+
field,
|
|
214
|
+
op,
|
|
215
|
+
value
|
|
216
|
+
})),
|
|
217
|
+
search: gridFilter.searchWords
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} : {})),
|
|
221
|
+
children: DO_NOT_CONTACT_DOMAIN_LABELS.export
|
|
222
|
+
})
|
|
223
|
+
]
|
|
224
|
+
}) : undefined;
|
|
171
225
|
const change = async (action, domain)=>{
|
|
172
226
|
setBusy(domain);
|
|
173
227
|
try {
|
|
@@ -190,6 +244,9 @@ function addedOn(entry) {
|
|
|
190
244
|
help: pluginDocsHelp('sequences', {
|
|
191
245
|
anchor: '#do-not-contact-domains'
|
|
192
246
|
}),
|
|
247
|
+
HeaderProps: {
|
|
248
|
+
action: transferActions
|
|
249
|
+
},
|
|
193
250
|
contentGutterX: true,
|
|
194
251
|
contentGutterY: true,
|
|
195
252
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -198,7 +255,7 @@ function addedOn(entry) {
|
|
|
198
255
|
/*#__PURE__*/ _jsx(Typography, {
|
|
199
256
|
variant: "body2",
|
|
200
257
|
color: "text.secondary",
|
|
201
|
-
children: 'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' + 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' + 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' + 'added here automatically, so the next person at that company is not tried.'
|
|
258
|
+
children: 'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' + 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' + 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' + 'added here automatically, so the next person at that company is not tried. Import adds ' + 'domains and email addresses from a file; Export downloads the domains.'
|
|
202
259
|
}),
|
|
203
260
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
204
261
|
direction: {
|
|
@@ -281,6 +338,11 @@ function addedOn(entry) {
|
|
|
281
338
|
quickFilter: true,
|
|
282
339
|
disableColumnSorting: true,
|
|
283
340
|
noRowsLabel: "No domains match these filters",
|
|
341
|
+
/*
|
|
342
|
+
* The grid's own export writes the page on screen; with the
|
|
343
|
+
* header's Export, which writes every matching domain, it
|
|
344
|
+
* would be a second button by the same name doing less.
|
|
345
|
+
*/ slotProps: transfer ? GRID_EXPORT_OFF : undefined,
|
|
284
346
|
hideFooter: true
|
|
285
347
|
}),
|
|
286
348
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.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 { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomainList } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n} as const\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317), each field on the\n * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):\n * the whole domain, why it is listed, and when it was added. The search box\n * reads the domain, each of its labels and the words of its detail.\n */\nconst DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n addedAtMs: 'Added',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the domains, alphabetical, with every clause and the search\n * word on the query (AGL-3321) — never matched over the domains that\n * happen to be loaded.\n */\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const rows = listed.rows.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n }))\n /** Nothing listed at all — not a filter that matched nothing. */\n const none =\n listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' && !filtering ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : none ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(listed.plan.refused, {\n fields: DOMAIN_FILTER_FIELDS,\n headers: DOMAIN_FILTER_HEADERS,\n options: DOMAIN_FILTER_OPTIONS,\n })}\n notices={listed.plan.notices}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n loading={listed.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and\n * the search word are on the list's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * order — alphabetical, or newest added while a date applies —\n * is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No domains match these filters\"\n hideFooter\n />\n <ListPagination\n page={listed.page}\n pageSize={listed.pageSize}\n rowCount={rows.length}\n hasMore={listed.hasMore}\n onPageChange={listed.setPage}\n onPageSizeChange={listed.setPageSize}\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useState","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomainList","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","fields","DOMAIN_FILTER_HEADERS","reason","addedAtMs","DOMAIN_FILTER_OPTIONS","map","value","label","addedOn","entry","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","listed","search","rows","$id","detail","none","status","page","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","xs","sm","alignItems","onChange","event","target","onKeyDown","key","preventDefault","helperText","flexGrow","what","headers","setClauses","options","refused","plan","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { pluginDocsHelp, useTransferLauncher } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY } from '../constants/transfer-resources'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomainList } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport { OUTREACH_DO_NOT_CONTACT_REASON_LABELS }\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n import: 'Import',\n export: 'Export',\n} as const\n\n/** What the export dialog calls the list's search and filters. */\nexport const DO_NOT_CONTACT_EXPORT_FILTER_LABEL = 'The domains matching this list’s search and filters'\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317), each field on the\n * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):\n * the whole domain, why it is listed, and when it was added. The search box\n * reads the domain, each of its labels and the words of its detail.\n */\nconst DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n addedAtMs: 'Added',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n\n/** The grid toolbar without its page-only CSV and print export. */\nconst GRID_EXPORT_OFF = {\n toolbar: { csvOptions: { disableToolbarButton: true }, printOptions: { disableToolbarButton: true } },\n}\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the domains, alphabetical, with every clause and the search\n * word on the query (AGL-3321) — never matched over the domains that\n * happen to be loaded.\n */\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const rows = listed.rows.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n }))\n /** Nothing listed at all — not a filter that matched nothing. */\n const none =\n listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0\n\n /*\n * Import adds domains and addresses from a file; Export downloads the\n * domains, narrowed to the list's search and filters when it has any (the\n * export reads them as the same Firestore query). Outside the console\n * shell there is no launcher, and no buttons.\n */\n const transfer = useTransferLauncher()\n const transferTarget = { resource: OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, scope: 'org' as const }\n // Import for those who may write the records, Export for those who may read them.\n const canImport = Boolean(transfer?.can('import', transferTarget))\n const canExport = Boolean(transfer?.can('export', transferTarget))\n const transferActions =\n transfer && (canImport || canExport) ? (\n <Stack direction=\"row\" spacing={1}>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={transferTarget} />\n {canImport && (\n <Button size=\"small\" onClick={() => transfer.openImport(transferTarget)}>\n {DO_NOT_CONTACT_DOMAIN_LABELS.import}\n </Button>\n )}\n {canExport && (\n <Button\n size=\"small\"\n onClick={() =>\n transfer.openExport({\n ...transferTarget,\n ...(filtering\n ? {\n filter: {\n label: DO_NOT_CONTACT_EXPORT_FILTER_LABEL,\n value: {\n clauses: gridFilter.clauses.map(({ field, op, value }) => ({ field, op, value })),\n search: gridFilter.searchWords,\n },\n },\n }\n : {}),\n })\n }\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.export}\n </Button>\n )}\n </Stack>\n ) : undefined\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n HeaderProps={{ action: transferActions }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried. Import adds ' +\n 'domains and email addresses from a file; Export downloads the domains.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' && !filtering ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : none ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(listed.plan.refused, {\n fields: DOMAIN_FILTER_FIELDS,\n headers: DOMAIN_FILTER_HEADERS,\n options: DOMAIN_FILTER_OPTIONS,\n })}\n notices={listed.plan.notices}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n loading={listed.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and\n * the search word are on the list's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * order — alphabetical, or newest added while a date applies —\n * is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No domains match these filters\"\n /*\n * The grid's own export writes the page on screen; with the\n * header's Export, which writes every matching domain, it\n * would be a second button by the same name doing less.\n */\n slotProps={transfer ? GRID_EXPORT_OFF : undefined}\n hideFooter\n />\n <ListPagination\n page={listed.page}\n pageSize={listed.pageSize}\n rowCount={rows.length}\n hasMore={listed.hasMore}\n onPageChange={listed.setPage}\n onPageSizeChange={listed.setPageSize}\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["TransferResumeImport","pluginDocsHelp","useTransferLauncher","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useState","OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomainList","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","import","export","DO_NOT_CONTACT_EXPORT_FILTER_LABEL","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","fields","DOMAIN_FILTER_HEADERS","reason","addedAtMs","DOMAIN_FILTER_OPTIONS","map","value","label","GRID_EXPORT_OFF","toolbar","csvOptions","disableToolbarButton","printOptions","addedOn","entry","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","listed","search","rows","$id","detail","none","status","page","transfer","transferTarget","resource","scope","canImport","Boolean","can","canExport","transferActions","direction","spacing","target","openImport","openExport","filter","op","undefined","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","HeaderProps","contentGutterX","contentGutterY","xs","sm","alignItems","onChange","event","onKeyDown","key","preventDefault","helperText","flexGrow","what","headers","setClauses","options","refused","plan","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","slotProps","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
|
|
@@ -15,6 +15,12 @@ export interface OutreachEnrollmentsTableProps {
|
|
|
15
15
|
* and a false one.
|
|
16
16
|
*/
|
|
17
17
|
trackClicks?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether this sequence counts opens, or once did (AGL-3488). The Opens
|
|
20
|
+
* column is shown only then, for the reason the Clicks column is: a
|
|
21
|
+
* column of dashes where nothing was measured reads as "nobody opened".
|
|
22
|
+
*/
|
|
23
|
+
countOpens?: boolean;
|
|
18
24
|
/** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */
|
|
19
25
|
timeZone: string | null;
|
|
20
26
|
/**
|
|
@@ -26,7 +26,7 @@ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filt
|
|
|
26
26
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
27
27
|
import { Card, CardContent, Chip, Link, Stack, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
28
28
|
import { useMemo } from "react";
|
|
29
|
-
import { outreachClickCountLabel, outreachClickSummary } from "../model/enrollment-engagement.js";
|
|
29
|
+
import { outreachClickCountLabel, outreachClickSummary, outreachMachineOpenLabel, outreachOpenCountLabel, outreachOpenSummary } from "../model/enrollment-engagement.js";
|
|
30
30
|
import { OUTREACH_ENROLLMENT_CLICK_FIELDS, OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
|
|
31
31
|
import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_STOP_REASONS, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
|
|
32
32
|
import { outreachNextSendState } from "../engine/next-send.js";
|
|
@@ -113,6 +113,9 @@ const CLICKED_OPTIONS = [
|
|
|
113
113
|
'lastClick',
|
|
114
114
|
'scannerClicks'
|
|
115
115
|
];
|
|
116
|
+
/** The opens column a reader can show from the column chooser, off until they do. */ const OPEN_DETAIL_COLUMNS = [
|
|
117
|
+
'lastOpen'
|
|
118
|
+
];
|
|
116
119
|
/** The table's grammar, with the click fields when the sequence counts clicks. */ function filterFieldsFor(trackClicks) {
|
|
117
120
|
return trackClicks ? OUTREACH_ENROLLMENT_LIST_QUERY.fields : BASE_FILTER_FIELDS;
|
|
118
121
|
}
|
|
@@ -158,7 +161,44 @@ const CLICKED_OPTIONS = [
|
|
|
158
161
|
children: text
|
|
159
162
|
}) : text;
|
|
160
163
|
}
|
|
161
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* The Opens cell (AGL-3488): the person's opens, and under them the fetches
|
|
166
|
+
* a machine made — counted apart and never in the person's number, as
|
|
167
|
+
* scanner clicks are never in their clicks.
|
|
168
|
+
*/ function OpensCell(props) {
|
|
169
|
+
const { opens } = props;
|
|
170
|
+
const machines = outreachMachineOpenLabel(opens);
|
|
171
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
172
|
+
spacing: 0,
|
|
173
|
+
sx: {
|
|
174
|
+
justifyContent: 'center',
|
|
175
|
+
height: '100%'
|
|
176
|
+
},
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
179
|
+
variant: "body2",
|
|
180
|
+
color: opens.opens ? undefined : 'text.secondary',
|
|
181
|
+
children: opens.opens ? String(opens.opens) : '—'
|
|
182
|
+
}),
|
|
183
|
+
machines ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
184
|
+
title: "Fetches of the tracking image by a mail provider’s proxy or a security scanner. They aren’t counted as the person’s opens.",
|
|
185
|
+
describeChild: true,
|
|
186
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
187
|
+
variant: "caption",
|
|
188
|
+
color: "text.secondary",
|
|
189
|
+
noWrap: true,
|
|
190
|
+
sx: {
|
|
191
|
+
textDecoration: 'underline dotted',
|
|
192
|
+
textUnderlineOffset: 3,
|
|
193
|
+
cursor: 'help'
|
|
194
|
+
},
|
|
195
|
+
children: machines
|
|
196
|
+
})
|
|
197
|
+
}) : null
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** The table's columns; the person and the actions are drawn from the row's enrollment. */ function columns(rowActions, trackClicks, countOpens, timeZone) {
|
|
162
202
|
return [
|
|
163
203
|
{
|
|
164
204
|
field: 'person',
|
|
@@ -263,6 +303,24 @@ const CLICKED_OPTIONS = [
|
|
|
263
303
|
width: 170,
|
|
264
304
|
sortable: false
|
|
265
305
|
},
|
|
306
|
+
...countOpens ? [
|
|
307
|
+
{
|
|
308
|
+
field: 'opens',
|
|
309
|
+
headerName: 'Opens',
|
|
310
|
+
description: 'A person’s opens of the emails. Fetches by mail proxies and security scanners are shown under them and not counted.',
|
|
311
|
+
width: 130,
|
|
312
|
+
sortable: false,
|
|
313
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(OpensCell, {
|
|
314
|
+
opens: row.openSummary
|
|
315
|
+
})
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
field: 'lastOpen',
|
|
319
|
+
headerName: 'Last open',
|
|
320
|
+
width: 170,
|
|
321
|
+
sortable: false
|
|
322
|
+
}
|
|
323
|
+
] : [],
|
|
266
324
|
...trackClicks ? [
|
|
267
325
|
{
|
|
268
326
|
field: 'clicks',
|
|
@@ -324,6 +382,7 @@ const CLICKED_OPTIONS = [
|
|
|
324
382
|
*/ export function OutreachEnrollmentsTable(props) {
|
|
325
383
|
const { steps, timeZone, api, onOpen, hrefFor } = props;
|
|
326
384
|
const trackClicks = props.trackClicks === true;
|
|
385
|
+
const countOpens = props.countOpens === true;
|
|
327
386
|
const nowMs = useOutreachNowMs();
|
|
328
387
|
const theme = useTheme();
|
|
329
388
|
const narrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
@@ -404,9 +463,13 @@ const CLICKED_OPTIONS = [
|
|
|
404
463
|
]), trackClicks ? Object.fromEntries(CLICK_DETAIL_COLUMNS.map((column)=>[
|
|
405
464
|
column,
|
|
406
465
|
false
|
|
466
|
+
])) : {}, countOpens ? Object.fromEntries(OPEN_DETAIL_COLUMNS.map((column)=>[
|
|
467
|
+
column,
|
|
468
|
+
false
|
|
407
469
|
])) : {}), [
|
|
408
470
|
fields,
|
|
409
|
-
trackClicks
|
|
471
|
+
trackClicks,
|
|
472
|
+
countOpens
|
|
410
473
|
]);
|
|
411
474
|
/*
|
|
412
475
|
* A person who followed this destination before each click was recorded
|
|
@@ -537,6 +600,7 @@ const CLICKED_OPTIONS = [
|
|
|
537
600
|
"aria-label": "Enrollments",
|
|
538
601
|
children: pageRows.map((enrollment)=>{
|
|
539
602
|
const clicks = clicksOf(enrollment);
|
|
603
|
+
const opens = outreachOpenSummary(enrollment.engagement);
|
|
540
604
|
return /*#__PURE__*/ _jsx(Card, {
|
|
541
605
|
variant: "outlined",
|
|
542
606
|
component: "li",
|
|
@@ -607,6 +671,11 @@ const CLICKED_OPTIONS = [
|
|
|
607
671
|
})
|
|
608
672
|
]
|
|
609
673
|
}),
|
|
674
|
+
countOpens && (opens.opens || opens.machineOpens) ? /*#__PURE__*/ _jsx(Typography, {
|
|
675
|
+
variant: "body2",
|
|
676
|
+
color: "text.secondary",
|
|
677
|
+
children: `Opens: ${outreachOpenCountLabel(opens)}`
|
|
678
|
+
}) : null,
|
|
610
679
|
trackClicks && clicks.clicks ? /*#__PURE__*/ _jsx(Typography, {
|
|
611
680
|
variant: "body2",
|
|
612
681
|
color: "text.secondary",
|
|
@@ -624,7 +693,7 @@ const CLICKED_OPTIONS = [
|
|
|
624
693
|
})
|
|
625
694
|
}) : /*#__PURE__*/ _jsx(ListTable, {
|
|
626
695
|
"aria-label": "Enrollments",
|
|
627
|
-
columns: listFilterGridColumns(columns(rowActions, trackClicks, timeZone), fields, filterOptions, ENROLLMENT_FILTER_HEADERS),
|
|
696
|
+
columns: listFilterGridColumns(columns(rowActions, trackClicks, countOpens, timeZone), fields, filterOptions, ENROLLMENT_FILTER_HEADERS),
|
|
628
697
|
initialState: {
|
|
629
698
|
columns: {
|
|
630
699
|
columnVisibilityModel: hiddenColumns
|
|
@@ -634,6 +703,7 @@ const CLICKED_OPTIONS = [
|
|
|
634
703
|
rows: pageRows.map((enrollment)=>{
|
|
635
704
|
var _enrollment_engagement;
|
|
636
705
|
const clicks = clicksOf(enrollment);
|
|
706
|
+
const opens = outreachOpenSummary(enrollment.engagement);
|
|
637
707
|
const next = nextSendState(enrollment);
|
|
638
708
|
return _extends({
|
|
639
709
|
$id: enrollment.id
|
|
@@ -646,6 +716,9 @@ const CLICKED_OPTIONS = [
|
|
|
646
716
|
].filter(Boolean).join(' — '),
|
|
647
717
|
lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
|
|
648
718
|
// What the export writes; the cells draw these their own way.
|
|
719
|
+
openSummary: opens,
|
|
720
|
+
opens: outreachOpenCountLabel(opens),
|
|
721
|
+
lastOpen: opens.opens ? formatOutreachTime(opens.lastOpenAtMs, timeZone) : '—',
|
|
649
722
|
clicks: outreachClickCountLabel(clicks),
|
|
650
723
|
linksFollowed: clicks.followed.join(' ') || '—',
|
|
651
724
|
lastClick: clicks.clicks ? formatOutreachTime((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.lastClickAtMs, timeZone) : '—',
|