@aglyn/plugins-outreach 1.0.0-beta.232 → 1.0.0-beta.233
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 +23 -8
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.js +93 -48
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequences-section.js +46 -15
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/model/do-not-contact-domain-list-query.js +60 -2
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -1
- package/src/lib/model/enrollment-list-query.js +55 -2
- package/src/lib/model/enrollment-list-query.js.map +1 -1
- package/src/lib/model/sequence-list-query.js +36 -2
- package/src/lib/model/sequence-list-query.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.233",
|
|
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.233",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.233",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.233",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.233",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.233",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.233",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.233",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.233",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.233",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.233",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.233",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.233",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -26,6 +26,7 @@ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/compon
|
|
|
26
26
|
import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
27
27
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
28
28
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
29
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
29
30
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
31
|
import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
|
|
31
32
|
import { useState } from "react";
|
|
@@ -162,9 +163,12 @@ function addedOn(entry) {
|
|
|
162
163
|
* One page of the domains, alphabetical, with every clause and the search
|
|
163
164
|
* word on the query (AGL-3321) — never matched over the domains that
|
|
164
165
|
* happen to be loaded.
|
|
165
|
-
*/
|
|
166
|
+
*/ // Every header orders the query (AGL-3680): `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts`.
|
|
167
|
+
const [askedSort, setAskedSort] = useState(null);
|
|
168
|
+
const listed = useOutreachDoNotContactDomainList(orgId, {
|
|
166
169
|
clauses: gridFilter.clauses,
|
|
167
|
-
search: gridFilter.searchWords
|
|
170
|
+
search: gridFilter.searchWords,
|
|
171
|
+
sort: askedSort
|
|
168
172
|
});
|
|
169
173
|
const rows = listed.rows.map((entry)=>{
|
|
170
174
|
var _entry_addedAtMs, _entry_detail;
|
|
@@ -177,6 +181,14 @@ function addedOn(entry) {
|
|
|
177
181
|
detail: (_entry_detail = entry.detail) != null ? _entry_detail : ''
|
|
178
182
|
};
|
|
179
183
|
});
|
|
184
|
+
const columnSort = useListColumnSort({
|
|
185
|
+
sorts: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts,
|
|
186
|
+
defaultSort: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts[0],
|
|
187
|
+
sort: askedSort,
|
|
188
|
+
onSortChange: setAskedSort,
|
|
189
|
+
orderBy: listed.plan.orderBy,
|
|
190
|
+
rows
|
|
191
|
+
});
|
|
180
192
|
/** Nothing listed at all — not a filter that matched nothing. */ const none = listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0;
|
|
181
193
|
/*
|
|
182
194
|
* Import adds domains and addresses from a file; Export downloads the
|
|
@@ -319,24 +331,27 @@ function addedOn(entry) {
|
|
|
319
331
|
headers: DOMAIN_FILTER_HEADERS,
|
|
320
332
|
options: DOMAIN_FILTER_OPTIONS
|
|
321
333
|
}),
|
|
322
|
-
notices:
|
|
334
|
+
notices: [
|
|
335
|
+
...listed.plan.notices,
|
|
336
|
+
...columnSort.notices
|
|
337
|
+
]
|
|
323
338
|
}),
|
|
324
339
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
325
340
|
"aria-label": "Do not contact domains",
|
|
326
341
|
columns: domainColumns(busy !== null, (domain)=>void change('remove', domain)),
|
|
327
|
-
rows: rows,
|
|
342
|
+
rows: columnSort.rows,
|
|
328
343
|
loading: listed.status === 'loading',
|
|
329
344
|
/*
|
|
330
345
|
* The panel and the search are the grid's; every clause and
|
|
331
346
|
* the search word are on the list's query (AGL-3321). The grid
|
|
332
|
-
* neither filters nor sorts the page it is handed:
|
|
333
|
-
*
|
|
334
|
-
*
|
|
347
|
+
* neither filters nor sorts the page it is handed: every header
|
|
348
|
+
* orders the query (AGL-3680) — alphabetical by default, newest
|
|
349
|
+
* added while a date applies.
|
|
335
350
|
*/ filterMode: "server",
|
|
336
351
|
filterModel: gridFilter.filterModel,
|
|
337
352
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
338
353
|
quickFilter: true,
|
|
339
|
-
|
|
354
|
+
columnSort: columnSort,
|
|
340
355
|
noRowsLabel: "No domains match these filters",
|
|
341
356
|
/*
|
|
342
357
|
* The grid's own export writes the page on screen; with the
|
|
@@ -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 { 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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"}
|
|
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 { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\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 // Every header orders the query (AGL-3680): `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts`.\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(null)\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\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 const columnSort = useListColumnSort({\n sorts: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts,\n defaultSort: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: listed.plan.orderBy,\n rows,\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, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={columnSort.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: every header\n * orders the query (AGL-3680) — alphabetical by default, newest\n * added while a date applies.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n columnSort={columnSort}\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","useListColumnSort","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","askedSort","setAskedSort","listed","search","sort","rows","$id","detail","columnSort","sorts","defaultSort","onSortChange","orderBy","plan","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","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","slotProps","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
|
|
@@ -24,8 +24,9 @@ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-j
|
|
|
24
24
|
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
25
25
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
26
26
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
27
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
27
28
|
import { Card, CardContent, Chip, Link, Stack, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
28
|
-
import { useMemo } from "react";
|
|
29
|
+
import { useMemo, useState } from "react";
|
|
29
30
|
import { outreachClickCountLabel, outreachClickSummary, outreachMachineOpenLabel, outreachOpenCountLabel, outreachOpenSummary } from "../model/enrollment-engagement.js";
|
|
30
31
|
import { OUTREACH_ENROLLMENT_CLICK_FIELDS, OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
|
|
31
32
|
import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_STOP_REASONS, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
|
|
@@ -132,6 +133,33 @@ const CLICKED_OPTIONS = [
|
|
|
132
133
|
link: clicks.followed
|
|
133
134
|
};
|
|
134
135
|
}
|
|
136
|
+
/*
|
|
137
|
+
* The columns read from `engagement.*` — absent until a person's first
|
|
138
|
+
* event — and the derived Last activity sort the page on screen (AGL-3680);
|
|
139
|
+
* Person, Status, Current step, Next send and Stop reason order the query
|
|
140
|
+
* (`OUTREACH_ENROLLMENT_LIST_QUERY.sorts`).
|
|
141
|
+
*/ const ENROLLMENT_PAGE_SORTS = {
|
|
142
|
+
lastActivity: (row)=>lastActivityMs(row.enrollment) || null,
|
|
143
|
+
opens: (row)=>row.openSummary.opens,
|
|
144
|
+
lastOpen: (row)=>row.openSummary.lastOpenAtMs,
|
|
145
|
+
clicks: (row)=>row.clickSummary.clicks,
|
|
146
|
+
linksFollowed: (row)=>row.clickSummary.followed.length,
|
|
147
|
+
lastClick: (row)=>{
|
|
148
|
+
var _ref;
|
|
149
|
+
var _row_enrollment_engagement;
|
|
150
|
+
return (_ref = (_row_enrollment_engagement = row.enrollment.engagement) == null ? void 0 : _row_enrollment_engagement.lastClickAtMs) != null ? _ref : null;
|
|
151
|
+
},
|
|
152
|
+
scannerClicks: (row)=>row.clickSummary.machineClicks
|
|
153
|
+
};
|
|
154
|
+
const ENROLLMENT_PAGE_SORT_HEADERS = {
|
|
155
|
+
lastActivity: 'Last activity',
|
|
156
|
+
opens: 'Opens',
|
|
157
|
+
lastOpen: 'Last open',
|
|
158
|
+
clicks: 'Clicks',
|
|
159
|
+
linksFollowed: 'Links followed',
|
|
160
|
+
lastClick: 'Last click',
|
|
161
|
+
scannerClicks: 'Scanner clicks'
|
|
162
|
+
};
|
|
135
163
|
/** The stop reason: its short label, with every word of it on hover and on the person's page. */ function StopReasonCell(props) {
|
|
136
164
|
var _OUTREACH_STOP_REASON_LABELS_enrollment_stopReason;
|
|
137
165
|
const { enrollment } = props;
|
|
@@ -413,9 +441,11 @@ const CLICKED_OPTIONS = [
|
|
|
413
441
|
* and nothing is matched over the rows that happen to be loaded. The
|
|
414
442
|
* click filters (AGL-3332) are on it too, answered by the `clicked` flag
|
|
415
443
|
* and the destinations the click route stores on each enrollment.
|
|
416
|
-
*/ const
|
|
444
|
+
*/ const [askedSort, setAskedSort] = useState(null);
|
|
445
|
+
const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {
|
|
417
446
|
clauses: gridFilter.clauses,
|
|
418
|
-
search: gridFilter.searchWords
|
|
447
|
+
search: gridFilter.searchWords,
|
|
448
|
+
sort: askedSort
|
|
419
449
|
});
|
|
420
450
|
const loadedData = enrollments.rows;
|
|
421
451
|
const summaries = useMemo(()=>new Map(loadedData.map((enrollment)=>[
|
|
@@ -485,6 +515,57 @@ const CLICKED_OPTIONS = [
|
|
|
485
515
|
onChange: gridFilter.setClauses,
|
|
486
516
|
options: filterOptions
|
|
487
517
|
});
|
|
518
|
+
/*
|
|
519
|
+
* A next send whose time has come says it is queued, and why, rather than
|
|
520
|
+
* showing a time already past (AGL-3366): the mailbox sends its share of
|
|
521
|
+
* the day each run, follow-ups first, so a first email can wait its turn
|
|
522
|
+
* for an hour or more without anything having failed.
|
|
523
|
+
*/ const nextSendState = (enrollment)=>{
|
|
524
|
+
var _props_sequence;
|
|
525
|
+
return outreachNextSendState({
|
|
526
|
+
enrollment,
|
|
527
|
+
sequence: (_props_sequence = props.sequence) != null ? _props_sequence : null,
|
|
528
|
+
mailbox: props.mailbox,
|
|
529
|
+
nowMs
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
const pageRows = enrollments.rows;
|
|
533
|
+
const tableRows = pageRows.map((enrollment)=>{
|
|
534
|
+
var _enrollment_engagement;
|
|
535
|
+
const clicks = clicksOf(enrollment);
|
|
536
|
+
const opens = outreachOpenSummary(enrollment.engagement);
|
|
537
|
+
const next = nextSendState(enrollment);
|
|
538
|
+
return _extends({
|
|
539
|
+
$id: enrollment.id
|
|
540
|
+
}, filterRow(enrollment, steps, clicks), {
|
|
541
|
+
nextSendState: next,
|
|
542
|
+
// What the export writes: the label, and for a queued send, why.
|
|
543
|
+
nextSend: [
|
|
544
|
+
outreachNextSendLabel(next, timeZone),
|
|
545
|
+
outreachNextSendDetail(next, timeZone)
|
|
546
|
+
].filter(Boolean).join(' — '),
|
|
547
|
+
lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
|
|
548
|
+
// What the export writes; the cells draw these their own way.
|
|
549
|
+
openSummary: opens,
|
|
550
|
+
clickSummary: clicks,
|
|
551
|
+
opens: outreachOpenCountLabel(opens),
|
|
552
|
+
lastOpen: opens.opens ? formatOutreachTime(opens.lastOpenAtMs, timeZone) : '—',
|
|
553
|
+
clicks: outreachClickCountLabel(clicks),
|
|
554
|
+
linksFollowed: clicks.followed.join(' ') || '—',
|
|
555
|
+
lastClick: clicks.clicks ? formatOutreachTime((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.lastClickAtMs, timeZone) : '—',
|
|
556
|
+
scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',
|
|
557
|
+
stopReason: outreachStopLabel(enrollment) || '—'
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
const columnSort = useListColumnSort({
|
|
561
|
+
sorts: OUTREACH_ENROLLMENT_LIST_QUERY.sorts,
|
|
562
|
+
sort: askedSort,
|
|
563
|
+
onSortChange: setAskedSort,
|
|
564
|
+
orderBy: enrollments.plan.orderBy,
|
|
565
|
+
rows: tableRows,
|
|
566
|
+
pageSorts: ENROLLMENT_PAGE_SORTS,
|
|
567
|
+
headers: ENROLLMENT_PAGE_SORT_HEADERS
|
|
568
|
+
});
|
|
488
569
|
if (enrollments.status === 'loading' && !filtering) return /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
489
570
|
label: "Loading enrollments…"
|
|
490
571
|
});
|
|
@@ -514,20 +595,6 @@ const CLICKED_OPTIONS = [
|
|
|
514
595
|
label: enrollment.contactName || enrollment.email,
|
|
515
596
|
items: actions.menuItems(enrollment)
|
|
516
597
|
});
|
|
517
|
-
/*
|
|
518
|
-
* A next send whose time has come says it is queued, and why, rather than
|
|
519
|
-
* showing a time already past (AGL-3366): the mailbox sends its share of
|
|
520
|
-
* the day each run, follow-ups first, so a first email can wait its turn
|
|
521
|
-
* for an hour or more without anything having failed.
|
|
522
|
-
*/ const nextSendState = (enrollment)=>{
|
|
523
|
-
var _props_sequence;
|
|
524
|
-
return outreachNextSendState({
|
|
525
|
-
enrollment,
|
|
526
|
-
sequence: (_props_sequence = props.sequence) != null ? _props_sequence : null,
|
|
527
|
-
mailbox: props.mailbox,
|
|
528
|
-
nowMs
|
|
529
|
-
});
|
|
530
|
-
};
|
|
531
598
|
const personName = (enrollment)=>{
|
|
532
599
|
const name = enrollment.contactName || enrollment.email;
|
|
533
600
|
if (!hrefFor) return /*#__PURE__*/ _jsx(Typography, {
|
|
@@ -571,7 +638,6 @@ const CLICKED_OPTIONS = [
|
|
|
571
638
|
}) : null
|
|
572
639
|
]
|
|
573
640
|
});
|
|
574
|
-
const pageRows = enrollments.rows;
|
|
575
641
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
576
642
|
spacing: 1.5,
|
|
577
643
|
children: [
|
|
@@ -582,7 +648,10 @@ const CLICKED_OPTIONS = [
|
|
|
582
648
|
headers: ENROLLMENT_FILTER_HEADERS,
|
|
583
649
|
options: filterOptions
|
|
584
650
|
}),
|
|
585
|
-
notices:
|
|
651
|
+
notices: [
|
|
652
|
+
...enrollments.plan.notices,
|
|
653
|
+
...columnSort.notices
|
|
654
|
+
]
|
|
586
655
|
}),
|
|
587
656
|
linkFilterMayMiss ? /*#__PURE__*/ _jsx(Typography, {
|
|
588
657
|
variant: "caption",
|
|
@@ -701,43 +770,19 @@ const CLICKED_OPTIONS = [
|
|
|
701
770
|
}
|
|
702
771
|
},
|
|
703
772
|
loading: enrollments.status === 'loading',
|
|
704
|
-
rows:
|
|
705
|
-
var _enrollment_engagement;
|
|
706
|
-
const clicks = clicksOf(enrollment);
|
|
707
|
-
const opens = outreachOpenSummary(enrollment.engagement);
|
|
708
|
-
const next = nextSendState(enrollment);
|
|
709
|
-
return _extends({
|
|
710
|
-
$id: enrollment.id
|
|
711
|
-
}, filterRow(enrollment, steps, clicks), {
|
|
712
|
-
nextSendState: next,
|
|
713
|
-
// What the export writes: the label, and for a queued send, why.
|
|
714
|
-
nextSend: [
|
|
715
|
-
outreachNextSendLabel(next, timeZone),
|
|
716
|
-
outreachNextSendDetail(next, timeZone)
|
|
717
|
-
].filter(Boolean).join(' — '),
|
|
718
|
-
lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
|
|
719
|
-
// What the export writes; the cells draw these their own way.
|
|
720
|
-
openSummary: opens,
|
|
721
|
-
opens: outreachOpenCountLabel(opens),
|
|
722
|
-
lastOpen: opens.opens ? formatOutreachTime(opens.lastOpenAtMs, timeZone) : '—',
|
|
723
|
-
clicks: outreachClickCountLabel(clicks),
|
|
724
|
-
linksFollowed: clicks.followed.join(' ') || '—',
|
|
725
|
-
lastClick: clicks.clicks ? formatOutreachTime((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.lastClickAtMs, timeZone) : '—',
|
|
726
|
-
scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',
|
|
727
|
-
stopReason: outreachStopLabel(enrollment) || '—'
|
|
728
|
-
});
|
|
729
|
-
}),
|
|
773
|
+
rows: columnSort.rows,
|
|
730
774
|
onOpen: onOpen ? (_id, row)=>onOpen(row.enrollment) : undefined,
|
|
731
775
|
/*
|
|
732
776
|
* The panel and the search are the grid's; every clause and the
|
|
733
777
|
* search word are on the table's query (AGL-3321). The grid
|
|
734
|
-
* neither filters nor sorts the page it is handed:
|
|
735
|
-
*
|
|
778
|
+
* neither filters nor sorts the page it is handed: a stored
|
|
779
|
+
* column's header orders the query, an engagement one the page
|
|
780
|
+
* (AGL-3680), newest enrolled first until one is clicked.
|
|
736
781
|
*/ filterMode: "server",
|
|
737
782
|
filterModel: gridFilter.filterModel,
|
|
738
783
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
739
784
|
quickFilter: true,
|
|
740
|
-
|
|
785
|
+
columnSort: columnSort,
|
|
741
786
|
noRowsLabel: "No enrollments match these filters",
|
|
742
787
|
hideFooter: true
|
|
743
788
|
}),
|