@aglyn/plugins-outreach 1.0.0-beta.199 → 1.0.0-beta.201
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.js +60 -42
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +25 -2
- package/src/lib/components/enrollments-table.js +303 -283
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +73 -11
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +13 -1
- package/src/lib/components/sequences-section.js +108 -83
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +30 -25
- package/src/lib/components/use-outreach-data.js +93 -104
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/enrollment/enrollment-search.d.ts +32 -0
- package/src/lib/enrollment/enrollment-search.js +40 -0
- package/src/lib/enrollment/enrollment-search.js.map +1 -0
- package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
- package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +28 -0
- package/src/lib/model/enrollment-list-query.js +151 -0
- package/src/lib/model/enrollment-list-query.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +90 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-list-query.d.ts +22 -0
- package/src/lib/model/sequence-list-query.js +99 -0
- package/src/lib/model/sequence-list-query.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +34 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +6 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.js +6 -1
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
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.201",
|
|
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.201",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.201",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.201",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.201",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.201",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.201",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.201",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.201",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.201",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.201",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.201",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.201",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -19,17 +19,20 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
|
19
19
|
import { mdiTrashCanOutline } from "@aglyn/shared-data-mdi";
|
|
20
20
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
22
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
23
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
22
24
|
import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
|
-
import {
|
|
25
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
24
26
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
25
27
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
26
28
|
import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
|
|
27
|
-
import {
|
|
29
|
+
import { useState } from "react";
|
|
28
30
|
import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
|
|
31
|
+
import { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from "../model/do-not-contact-domain-list-query.js";
|
|
29
32
|
import { OUTREACH_DO_NOT_CONTACT_REASONS } from "../model/outreach.types.js";
|
|
30
33
|
import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
31
34
|
import { useOutreachApi } from "./use-outreach-api.js";
|
|
32
|
-
import {
|
|
35
|
+
import { useOutreachDoNotContactDomainList } from "./use-outreach-data.js";
|
|
33
36
|
/** Why a domain is on the list, as the card says it. */ export const OUTREACH_DO_NOT_CONTACT_REASON_LABELS = {
|
|
34
37
|
manual: 'Added by a member',
|
|
35
38
|
opt_out_reply: 'A reply asked not to be emailed',
|
|
@@ -44,17 +47,15 @@ import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
|
|
|
44
47
|
};
|
|
45
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.';
|
|
46
49
|
/*
|
|
47
|
-
* What the domains grid's Filters panel offers (AGL-3317)
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
inMemoryListField('detail', 'text')
|
|
53
|
-
];
|
|
50
|
+
* What the domains grid's Filters panel offers (AGL-3317), each field on the
|
|
51
|
+
* list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):
|
|
52
|
+
* the whole domain, why it is listed, and when it was added. The search box
|
|
53
|
+
* reads the domain, each of its labels and the words of its detail.
|
|
54
|
+
*/ const DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields;
|
|
54
55
|
const DOMAIN_FILTER_HEADERS = {
|
|
55
56
|
domain: 'Domain',
|
|
56
57
|
reason: 'Why',
|
|
57
|
-
|
|
58
|
+
addedAtMs: 'Added'
|
|
58
59
|
};
|
|
59
60
|
const DOMAIN_FILTER_OPTIONS = {
|
|
60
61
|
reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason)=>({
|
|
@@ -62,10 +63,6 @@ const DOMAIN_FILTER_OPTIONS = {
|
|
|
62
63
|
label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason]
|
|
63
64
|
}))
|
|
64
65
|
};
|
|
65
|
-
/** What the quick search reads on a domain row. */ const DOMAIN_SEARCH_FIELDS = [
|
|
66
|
-
'domain',
|
|
67
|
-
'detail'
|
|
68
|
-
];
|
|
69
66
|
function addedOn(entry) {
|
|
70
67
|
if (!entry.addedAtMs) return '';
|
|
71
68
|
return new Intl.DateTimeFormat('en-US', {
|
|
@@ -140,7 +137,6 @@ function addedOn(entry) {
|
|
|
140
137
|
*/ export function OutreachDoNotContactDomainsCard(props) {
|
|
141
138
|
const { orgId } = props;
|
|
142
139
|
const api = useOutreachApi(orgId);
|
|
143
|
-
const listed = useOutreachDoNotContactDomains(orgId);
|
|
144
140
|
const { enqueueSnackbar } = useSnackbar();
|
|
145
141
|
const [draft, setDraft] = useState('');
|
|
146
142
|
const [busy, setBusy] = useState(null);
|
|
@@ -151,27 +147,27 @@ function addedOn(entry) {
|
|
|
151
147
|
'reason'
|
|
152
148
|
]
|
|
153
149
|
});
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
150
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
151
|
+
/*
|
|
152
|
+
* One page of the domains, alphabetical, with every clause and the search
|
|
153
|
+
* word on the query (AGL-3321) — never matched over the domains that
|
|
154
|
+
* happen to be loaded.
|
|
155
|
+
*/ const listed = useOutreachDoNotContactDomainList(orgId, {
|
|
156
|
+
clauses: gridFilter.clauses,
|
|
157
|
+
search: gridFilter.searchWords
|
|
158
|
+
});
|
|
159
|
+
const rows = listed.rows.map((entry)=>{
|
|
160
|
+
var _entry_addedAtMs, _entry_detail;
|
|
161
|
+
return {
|
|
162
|
+
$id: entry.domain,
|
|
163
|
+
domain: entry.domain,
|
|
164
|
+
reason: entry.reason,
|
|
165
|
+
added: addedOn(entry),
|
|
166
|
+
addedAtMs: (_entry_addedAtMs = entry.addedAtMs) != null ? _entry_addedAtMs : 0,
|
|
167
|
+
detail: (_entry_detail = entry.detail) != null ? _entry_detail : ''
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
/** Nothing listed at all — not a filter that matched nothing. */ const none = listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0;
|
|
175
171
|
const change = async (action, domain)=>{
|
|
176
172
|
setBusy(domain);
|
|
177
173
|
try {
|
|
@@ -241,12 +237,12 @@ function addedOn(entry) {
|
|
|
241
237
|
})
|
|
242
238
|
]
|
|
243
239
|
}),
|
|
244
|
-
listed.status === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
240
|
+
listed.status === 'loading' && !filtering ? /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
245
241
|
label: "Loading domains…"
|
|
246
242
|
}) : listed.status === 'error' || listed.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
|
|
247
243
|
status: listed.status,
|
|
248
244
|
what: "the do-not-contact domains"
|
|
249
|
-
}) :
|
|
245
|
+
}) : none ? /*#__PURE__*/ _jsx(Typography, {
|
|
250
246
|
variant: "body2",
|
|
251
247
|
color: "text.secondary",
|
|
252
248
|
children: "No domains yet."
|
|
@@ -260,18 +256,40 @@ function addedOn(entry) {
|
|
|
260
256
|
onChange: gridFilter.setClauses,
|
|
261
257
|
options: DOMAIN_FILTER_OPTIONS
|
|
262
258
|
}),
|
|
259
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
260
|
+
refused: listQueryRefusals(listed.plan.refused, {
|
|
261
|
+
fields: DOMAIN_FILTER_FIELDS,
|
|
262
|
+
headers: DOMAIN_FILTER_HEADERS,
|
|
263
|
+
options: DOMAIN_FILTER_OPTIONS
|
|
264
|
+
}),
|
|
265
|
+
notices: listed.plan.notices
|
|
266
|
+
}),
|
|
263
267
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
264
268
|
"aria-label": "Do not contact domains",
|
|
265
269
|
columns: domainColumns(busy !== null, (domain)=>void change('remove', domain)),
|
|
266
270
|
rows: rows,
|
|
271
|
+
loading: listed.status === 'loading',
|
|
267
272
|
/*
|
|
268
|
-
* The panel and the search are the grid's;
|
|
269
|
-
*
|
|
273
|
+
* The panel and the search are the grid's; every clause and
|
|
274
|
+
* the search word are on the list's query (AGL-3321). The grid
|
|
275
|
+
* neither filters nor sorts the page it is handed: the query's
|
|
276
|
+
* order — alphabetical, or newest added while a date applies —
|
|
277
|
+
* is the list's.
|
|
270
278
|
*/ filterMode: "server",
|
|
271
279
|
filterModel: gridFilter.filterModel,
|
|
272
280
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
273
281
|
quickFilter: true,
|
|
274
|
-
|
|
282
|
+
disableColumnSorting: true,
|
|
283
|
+
noRowsLabel: "No domains match these filters",
|
|
284
|
+
hideFooter: true
|
|
285
|
+
}),
|
|
286
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
287
|
+
page: listed.page,
|
|
288
|
+
pageSize: listed.pageSize,
|
|
289
|
+
rowCount: rows.length,
|
|
290
|
+
hasMore: listed.hasMore,
|
|
291
|
+
onPageChange: listed.setPage,
|
|
292
|
+
onPageSizeChange: listed.setPageSize
|
|
275
293
|
})
|
|
276
294
|
]
|
|
277
295
|
})
|
|
@@ -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 {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} 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 { useMemo, useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\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 { useOutreachDoNotContactDomains } 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). The card reads\n * every listed domain, so the panel and the search answer over all of them.\n */\nconst DOMAIN_FILTER_FIELDS = [\n inMemoryListField('domain', 'text'),\n inMemoryListField('reason', 'select'),\n inMemoryListField('detail', 'text'),\n]\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n detail: 'Detail',\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/** What the quick search reads on a domain row. */\nconst DOMAIN_SEARCH_FIELDS = ['domain', 'detail'] as const\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 listed = useOutreachDoNotContactDomains(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 searchKey = gridFilter.searchWords.join(' ')\n const rows = useMemo(\n () =>\n filterListRows(\n listed.data.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 DOMAIN_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: DOMAIN_SEARCH_FIELDS, words: gridFilter.searchWords },\n ),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [listed.data, gridFilter.clauses, searchKey],\n )\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' ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : listed.data.length === 0 ? (\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 <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n /*\n * The panel and the search are the grid's; the card answers\n * them over every listed domain (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No domains match these filters\"\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListTable","listActionsColumn","filterListRows","inMemoryListField","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useMemo","useState","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomains","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","DOMAIN_FILTER_HEADERS","reason","detail","DOMAIN_FILTER_OPTIONS","map","value","label","DOMAIN_SEARCH_FIELDS","addedOn","entry","addedAtMs","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","listed","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","searchKey","searchWords","join","rows","data","$id","clauses","paths","words","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","status","what","length","fields","headers","setClauses","options","columns","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","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 { 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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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { OutreachEnrollmentAction } from '../model/outreach-api';
|
|
4
|
+
import type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types';
|
|
5
|
+
import type { OutreachApi } from './use-outreach-api';
|
|
6
|
+
/**
|
|
7
|
+
* WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place
|
|
8
|
+
* for the two places it is offered: a row's menu on the Enrollments tab, and
|
|
9
|
+
* the header of the person's own page (AGL-3332). The same actions, the same
|
|
10
|
+
* words, the same two confirmations — a Stop that asked first in one place
|
|
11
|
+
* and not in the other would be two products.
|
|
12
|
+
*/
|
|
13
|
+
/** The four the action route takes, and curating the next step (AGL-3324). */
|
|
14
|
+
export type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate';
|
|
15
|
+
/** What a member may do to an enrollment in the status it is in. */
|
|
16
|
+
export declare function outreachEnrollmentActionsFor(enrollment: OutreachEnrollment, steps: readonly OutreachSequenceStep[]): OutreachEnrollmentRowAction[];
|
|
17
|
+
export declare const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string>;
|
|
18
|
+
/** The two actions that end something for good. */
|
|
19
|
+
export declare const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[];
|
|
20
|
+
export interface OutreachEnrollmentActions {
|
|
21
|
+
/** The enrollment an action is running on, if any. */
|
|
22
|
+
busy: string | null;
|
|
23
|
+
/** Runs one action: straight away, after its confirmation, or in the curate dialog. */
|
|
24
|
+
choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void;
|
|
25
|
+
/** The actions as menu items, the destructive ones marked. */
|
|
26
|
+
menuItems(enrollment: OutreachEnrollment, only?: (action: OutreachEnrollmentRowAction) => boolean): RowActionsMenuItem[];
|
|
27
|
+
/** The confirmation and the curate dialog, rendered once wherever the actions are offered. */
|
|
28
|
+
dialogs: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export declare function useOutreachEnrollmentActions(input: {
|
|
31
|
+
api: OutreachApi;
|
|
32
|
+
steps: readonly OutreachSequenceStep[];
|
|
33
|
+
}): OutreachEnrollmentActions;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
19
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField } from "@mui/material";
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-step-dialog.js";
|
|
22
|
+
/** What a member may do to an enrollment in the status it is in. */ export function outreachEnrollmentActionsFor(enrollment, steps) {
|
|
23
|
+
const actions = [];
|
|
24
|
+
// Rewrite the next email for this one person, while one is still to go.
|
|
25
|
+
if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate');
|
|
26
|
+
if (enrollment.status === 'active') actions.push('pause', 'stop');
|
|
27
|
+
if (enrollment.status === 'paused') actions.push('resume', 'stop');
|
|
28
|
+
if (enrollment.status !== 'opted_out') actions.push('do_not_contact');
|
|
29
|
+
return actions;
|
|
30
|
+
}
|
|
31
|
+
export const OUTREACH_ENROLLMENT_ACTION_LABELS = {
|
|
32
|
+
curate: 'Curate next step',
|
|
33
|
+
pause: 'Pause',
|
|
34
|
+
resume: 'Resume',
|
|
35
|
+
stop: 'Stop',
|
|
36
|
+
do_not_contact: 'Mark do-not-contact'
|
|
37
|
+
};
|
|
38
|
+
/** The two actions that end something for good. */ export const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS = [
|
|
39
|
+
'stop',
|
|
40
|
+
'do_not_contact'
|
|
41
|
+
];
|
|
42
|
+
/** What the snackbar says once an action lands. */ const DONE_LABELS = {
|
|
43
|
+
pause: 'Paused.',
|
|
44
|
+
resume: 'Resumed.',
|
|
45
|
+
stop: 'Stopped.'
|
|
46
|
+
};
|
|
47
|
+
/** The two actions that end something for good ask first. */ const CONFIRM = {
|
|
48
|
+
stop: {
|
|
49
|
+
title: 'Stop this enrollment?',
|
|
50
|
+
body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',
|
|
51
|
+
label: 'Stop'
|
|
52
|
+
},
|
|
53
|
+
do_not_contact: {
|
|
54
|
+
title: 'Mark do-not-contact?',
|
|
55
|
+
body: 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' + 'every send, and they are stopped in every sequence they are in.',
|
|
56
|
+
label: 'Mark do-not-contact'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export function useOutreachEnrollmentActions(input) {
|
|
60
|
+
var _CONFIRM_confirming_action, _CONFIRM_confirming_action1, _CONFIRM_confirming_action2;
|
|
61
|
+
const { api, steps } = input;
|
|
62
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
63
|
+
const [confirming, setConfirming] = useState(null);
|
|
64
|
+
const [detail, setDetail] = useState('');
|
|
65
|
+
const [busy, setBusy] = useState(null);
|
|
66
|
+
const [curating, setCurating] = useState(null);
|
|
67
|
+
const act = async (enrollment, action, why)=>{
|
|
68
|
+
setBusy(enrollment.id);
|
|
69
|
+
try {
|
|
70
|
+
const answer = await api.actOnEnrollment(enrollment.id, action, why);
|
|
71
|
+
const others = answer.stoppedOthers;
|
|
72
|
+
enqueueSnackbar(action === 'do_not_contact' ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.` : answer.changed ? DONE_LABELS[action] : 'Nothing to change.', {
|
|
73
|
+
variant: 'success'
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
enqueueSnackbar(error.message, {
|
|
77
|
+
variant: 'error',
|
|
78
|
+
allowDuplicate: true
|
|
79
|
+
});
|
|
80
|
+
} finally{
|
|
81
|
+
setBusy(null);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const choose = (enrollment, action)=>{
|
|
85
|
+
if (action === 'curate') {
|
|
86
|
+
setCurating(enrollment);
|
|
87
|
+
} else if (CONFIRM[action]) {
|
|
88
|
+
setDetail('');
|
|
89
|
+
setConfirming({
|
|
90
|
+
enrollment,
|
|
91
|
+
action
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
void act(enrollment, action);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const menuItems = (enrollment, only)=>outreachEnrollmentActionsFor(enrollment, steps).filter((action)=>!only || only(action)).map((action)=>({
|
|
98
|
+
key: action,
|
|
99
|
+
label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],
|
|
100
|
+
onClick: ()=>choose(enrollment, action),
|
|
101
|
+
destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),
|
|
102
|
+
disabled: busy === enrollment.id
|
|
103
|
+
}));
|
|
104
|
+
const dialogs = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ _jsx(OutreachCurateStepDialog, {
|
|
107
|
+
enrollment: curating,
|
|
108
|
+
steps: steps,
|
|
109
|
+
api: api,
|
|
110
|
+
onClose: ()=>setCurating(null)
|
|
111
|
+
}),
|
|
112
|
+
/*#__PURE__*/ _jsx(Dialog, {
|
|
113
|
+
open: Boolean(confirming),
|
|
114
|
+
onClose: ()=>setConfirming(null),
|
|
115
|
+
fullWidth: true,
|
|
116
|
+
maxWidth: "xs",
|
|
117
|
+
children: confirming ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
118
|
+
children: [
|
|
119
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
120
|
+
children: (_CONFIRM_confirming_action = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action.title
|
|
121
|
+
}),
|
|
122
|
+
/*#__PURE__*/ _jsx(DialogContent, {
|
|
123
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
124
|
+
spacing: 2,
|
|
125
|
+
children: [
|
|
126
|
+
/*#__PURE__*/ _jsx(DialogContentText, {
|
|
127
|
+
children: (_CONFIRM_confirming_action1 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action1.body
|
|
128
|
+
}),
|
|
129
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
130
|
+
label: "Why (optional)",
|
|
131
|
+
value: detail,
|
|
132
|
+
onChange: (event)=>setDetail(event.target.value),
|
|
133
|
+
fullWidth: true,
|
|
134
|
+
slotProps: {
|
|
135
|
+
htmlInput: {
|
|
136
|
+
maxLength: 200
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
]
|
|
141
|
+
})
|
|
142
|
+
}),
|
|
143
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
144
|
+
children: [
|
|
145
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
146
|
+
onClick: ()=>setConfirming(null),
|
|
147
|
+
children: "Cancel"
|
|
148
|
+
}),
|
|
149
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
150
|
+
color: "error",
|
|
151
|
+
variant: "contained",
|
|
152
|
+
onClick: ()=>{
|
|
153
|
+
const { enrollment, action } = confirming;
|
|
154
|
+
setConfirming(null);
|
|
155
|
+
void act(enrollment, action, detail.trim() || undefined);
|
|
156
|
+
},
|
|
157
|
+
children: (_CONFIRM_confirming_action2 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action2.label
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
})
|
|
161
|
+
]
|
|
162
|
+
}) : null
|
|
163
|
+
})
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
busy,
|
|
168
|
+
choose,
|
|
169
|
+
menuItems,
|
|
170
|
+
dialogs
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=enrollment-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollment-actions.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 type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types'\nimport { OutreachCurateStepDialog, outreachNextEmailStepIndex } from './curate-step-dialog'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place\n * for the two places it is offered: a row's menu on the Enrollments tab, and\n * the header of the person's own page (AGL-3332). The same actions, the same\n * words, the same two confirmations — a Stop that asked first in one place\n * and not in the other would be two products.\n */\n\n/** The four the action route takes, and curating the next step (AGL-3324). */\nexport type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate'\n\n/** What a member may do to an enrollment in the status it is in. */\nexport function outreachEnrollmentActionsFor(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n): OutreachEnrollmentRowAction[] {\n const actions: OutreachEnrollmentRowAction[] = []\n // Rewrite the next email for this one person, while one is still to go.\n if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate')\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nexport const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string> = {\n curate: 'Curate next step',\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** The two actions that end something for good. */\nexport const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[] = [\n 'stop',\n 'do_not_contact',\n]\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<Exclude<OutreachEnrollmentAction, 'do_not_contact'>, string> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<Record<OutreachEnrollmentAction, { title: string; body: string; label: string }>> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\nexport interface OutreachEnrollmentActions {\n /** The enrollment an action is running on, if any. */\n busy: string | null\n /** Runs one action: straight away, after its confirmation, or in the curate dialog. */\n choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void\n /** The actions as menu items, the destructive ones marked. */\n menuItems(\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[]\n /** The confirmation and the curate dialog, rendered once wherever the actions are offered. */\n dialogs: ReactNode\n}\n\nexport function useOutreachEnrollmentActions(input: {\n api: OutreachApi\n steps: readonly OutreachSequenceStep[]\n}): OutreachEnrollmentActions {\n const { api, steps } = input\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [curating, setCurating] = useState<OutreachEnrollment | null>(null)\n\n const act = async (enrollment: OutreachEnrollment, action: OutreachEnrollmentAction, why?: string) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction) => {\n if (action === 'curate') {\n setCurating(enrollment)\n } else if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n const menuItems = (\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[] =>\n outreachEnrollmentActionsFor(enrollment, steps)\n .filter((action) => !only || only(action))\n .map((action) => ({\n key: action,\n label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),\n disabled: busy === enrollment.id,\n }))\n\n const dialogs = (\n <>\n <OutreachCurateStepDialog enrollment={curating} steps={steps} api={api} onClose={() => setCurating(null)} />\n <Dialog open={Boolean(confirming)} onClose={() => setConfirming(null)} fullWidth maxWidth=\"xs\">\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{CONFIRM[confirming.action]?.body}</DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </>\n )\n\n return { busy, choose, menuItems, dialogs }\n}\n"],"names":["useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","useState","OutreachCurateStepDialog","outreachNextEmailStepIndex","outreachEnrollmentActionsFor","enrollment","steps","actions","push","status","OUTREACH_ENROLLMENT_ACTION_LABELS","curate","pause","resume","stop","do_not_contact","OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS","DONE_LABELS","CONFIRM","title","body","label","useOutreachEnrollmentActions","input","api","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","curating","setCurating","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","variant","error","message","allowDuplicate","choose","menuItems","only","filter","map","key","onClick","destructive","includes","disabled","dialogs","onClose","open","Boolean","fullWidth","maxWidth","spacing","value","onChange","event","target","slotProps","htmlInput","maxLength","color","trim","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ConsolePluginOrgMount } from '@aglyn/aglyn';
|
|
2
|
+
import type { OutreachMailboxesResult } from './use-outreach-mailboxes';
|
|
3
|
+
export interface OutreachEnrollmentDetailProps {
|
|
4
|
+
orgId: string;
|
|
5
|
+
/** The org the hub is mounted under: its slug and sites, for the CRM record's address. */
|
|
6
|
+
orgMount?: ConsolePluginOrgMount;
|
|
7
|
+
/** `/[orgSlug]/outreach/sequences`. */
|
|
8
|
+
sectionPath: string;
|
|
9
|
+
sequenceId: string;
|
|
10
|
+
enrollmentId: string;
|
|
11
|
+
mailboxes: OutreachMailboxesResult;
|
|
12
|
+
}
|
|
13
|
+
/** Where the CRM record the person is lives — the site's own CRM when the site is known. */
|
|
14
|
+
export declare function outreachCrmRecordHref(kind: 'lead' | 'contact', id: string | null | undefined, hostId: string, orgMount: Pick<ConsolePluginOrgMount, 'orgSlug' | 'hosts'> | undefined): string | null;
|
|
15
|
+
/** A Gmail thread, opened in the mailbox's own account. */
|
|
16
|
+
export declare function outreachGmailThreadUrl(threadId: string, mailboxEmail: string | null | undefined): string;
|
|
17
|
+
export declare function OutreachEnrollmentDetail(props: OutreachEnrollmentDetailProps): import("react").JSX.Element;
|
|
18
|
+
export declare namespace OutreachEnrollmentDetail {
|
|
19
|
+
var displayName: string;
|
|
20
|
+
}
|
|
21
|
+
export default OutreachEnrollmentDetail;
|