@aglyn/plugins-outreach 1.0.0-beta.198 → 1.0.0-beta.200
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/enrollments-table.d.ts +4 -2
- package/src/lib/components/enrollments-table.js +70 -89
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -3
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +9 -0
- package/src/lib/components/sequences-section.js +96 -82
- package/src/lib/components/sequences-section.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/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-list-query.d.ts +22 -0
- package/src/lib/model/enrollment-list-query.js +117 -0
- package/src/lib/model/enrollment-list-query.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +12 -0
- 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 +8 -0
- package/src/lib/routes/enroll-routes.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/storage/do-not-contact-store.js +6 -1
- package/src/lib/storage/do-not-contact-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.200",
|
|
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.200",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.200",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.200",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.200",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.200",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.200",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.200",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.200",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.200",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.200",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.200",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.200",
|
|
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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,OAAOC,oBACLC,iBAAiB,QACZ,+DAA8D;AACrE,SACEC,SAAS,EACTC,iBAAiB,QAEZ,uDAAsD;AAC7D,SAASC,qBAAqB,QAAQ,8CAA6C;AACnF,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAEC,KAAK,EAAEC,SAAS,EAAEC,UAAU,QAAQ,gBAAe;AACtF,SAASC,QAAQ,QAAQ,QAAO;AAChC,SAASC,uBAAuB,QAAQ,qCAAiC;AACzE,SAASC,yCAAyC,QAAQ,+CAA2C;AACrG,SACEC,+BAA+B,QAG1B,6BAAyB;AAChC,SAASC,eAAe,EAAEC,mBAAmB,QAAQ,mBAAe;AACpE,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,iCAAiC,QAAQ,yBAAqB;AAOvE,sDAAsD,GACtD,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,eAAe;IACfC,aAAa;IACbC,aAAa;IACbC,eAAe;AACjB,EAAC;AAED,yEAAyE,GACzE,OAAO,MAAMC,+BAA+B;IAC1CC,OAAO;IACPC,KAAK;IACLC,QAAQ,CAACC,SAAmB,CAAC,OAAO,EAAEA,QAAQ;AAChD,EAAU;AAEV,4DAA4D,GAC5D,OAAO,MAAMC,6BAA6B,iEAAgE;AAE1G;;;;;CAKC,GACD,MAAMC,uBAAuBlB,0CAA0CmB,MAAM;AAC7E,MAAMC,wBAA0D;IAC9DJ,QAAQ;IACRK,QAAQ;IACRC,WAAW;AACb;AACA,MAAMC,wBAAwB;IAC5BF,QAAQpB,gCAAgCuB,GAAG,CAAC,CAACH,SAAY,CAAA;YACvDI,OAAOJ;YACPK,OAAOpB,qCAAqC,CAACe,OAAO;QACtD,CAAA;AACF;AAEA,SAASM,QAAQC,KAAsC;IACrD,IAAI,CAACA,MAAMN,SAAS,EAAE,OAAO;IAC7B,OAAO,IAAIO,KAAKC,cAAc,CAAC,SAAS;QAAEC,WAAW;IAAS,GAAGC,MAAM,CAACJ,MAAMN,SAAS;AACzF;AAEA,gFAAgF,GAChF,SAASW,cACPC,IAAa,EACbnB,MAAgC;IAEhC,OAAO1B,sBACL;QACE;YACEwB,OAAO;YACPsB,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,YAAY,CAAC,EAAEC,GAAG,EAAE,iBAClB,KAAC1C;oBAAW2C,SAAQ;oBAAQC,WAAU;oBAAOC,IAAI;wBAAEC,YAAY;oBAAI;8BAChEJ,IAAIvB,MAAM;;QAGjB;QACA;YACEH,OAAO;YACPsB,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,YAAY,CAAC,EAAEC,GAAG,EAAE;oBAMdjC;qCALJ,KAACb;oBACCmD,MAAK;oBACLJ,SAAQ;oBACRK,OAAON,IAAIlB,MAAM,KAAK,kBAAkB,YAAY;oBACpDK,KAAK,GACHpB,oDAAAA,qCAAqC,CAACiC,IAAIlB,MAAM,CAA+B,YAA/Ef,oDACAiC,IAAIlB,MAAM;;;QAIlB;QACA;YACER,OAAO;YACPsB,YAAY;YACZW,OAAO;YACPR,YAAY,CAAC,EAAEC,GAAG,EAAE,GAAKA,IAAIQ,KAAK;QACpC;QACA;YAAElC,OAAO;YAAUsB,YAAY;YAAUC,MAAM;YAAGC,UAAU;QAAI;QAChEjD,kBACE,CAACmD,oBACC,KAAC7C;gBACCkD,MAAK;gBACLI,cAAYpC,6BAA6BG,MAAM,CAACwB,IAAIvB,MAAM;gBAC1DiC,UAAUf;gBACVgB,SAAS,IAAMnC,OAAOwB,IAAIvB,MAAM;0BAEhC,cAAA,KAAClC;oBAAQqE,MAAMvE,mBAAmBuE,IAAI;oBAAEC,UAAS;;gBAGrD;YAAEN,OAAO;QAAG;KAEf,EACD5B,sBACAK,uBACAH;AAEJ;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASiC,gCAAgCC,KAA2C;IACzF,MAAM,EAAEC,KAAK,EAAE,GAAGD;IAClB,MAAME,MAAMpD,eAAemD;IAC3B,MAAM,EAAEE,eAAe,EAAE,GAAGlE;IAC5B,MAAM,CAACmE,OAAOC,SAAS,GAAG7D,SAAS;IACnC,MAAM,CAACoC,MAAM0B,QAAQ,GAAG9D,SAAwB;IAChD,MAAM+D,aAAa9D,wBAAwB2D;IAC3C,MAAMI,UAAUJ,MAAMK,IAAI,OAAO,MAAMF,eAAe;IACtD,MAAMG,aAAa1E,kBAAkB;QAAE2E,cAAc;YAAC;SAAS;IAAC;IAChE,MAAMC,YACJF,WAAWG,OAAO,CAACC,MAAM,GAAG,KAAKJ,WAAWK,WAAW,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKR,IAAI;IAClF;;;;GAIC,GACD,MAAMS,SAASnE,kCAAkCkD,OAAO;QACtDY,SAASH,WAAWG,OAAO;QAC3BM,QAAQT,WAAWK,WAAW;IAChC;IACA,MAAMK,OAAOF,OAAOE,IAAI,CAAClD,GAAG,CAAC,CAACI;YAKjBA,kBACHA;eAN+B;YACvC+C,KAAK/C,MAAMZ,MAAM;YACjBA,QAAQY,MAAMZ,MAAM;YACpBK,QAAQO,MAAMP,MAAM;YACpB0B,OAAOpB,QAAQC;YACfN,SAAS,GAAEM,mBAAAA,MAAMN,SAAS,YAAfM,mBAAmB;YAC9BgD,MAAM,GAAEhD,gBAAAA,MAAMgD,MAAM,YAAZhD,gBAAgB;QAC1B;;IACA,+DAA+D,GAC/D,MAAMiD,OACJL,OAAOM,MAAM,KAAK,WAAW,CAACN,OAAOE,IAAI,CAACN,MAAM,IAAI,CAACF,aAAaM,OAAOO,IAAI,KAAK;IAEpF,MAAMC,SAAS,OAAOC,QAA0BjE;QAC9C4C,QAAQ5C;QACR,IAAI;YACF,MAAMkE,SAAS,MAAM1B,IAAI2B,wBAAwB,CAACF,QAAQjE;YAC1D,IAAIiE,WAAW,OAAOtB,SAAS;YAC/BF,gBACEyB,OAAOE,OAAO,GACVH,WAAW,QACT,GAAGC,OAAOlE,MAAM,CAAC,+BAA+B,CAAC,GACjD,GAAGkE,OAAOlE,MAAM,CAAC,gCAAgC,CAAC,GACpDiE,WAAW,QACT,GAAGC,OAAOlE,MAAM,CAAC,yBAAyB,CAAC,GAC3C,GAAGkE,OAAOlE,MAAM,CAAC,qBAAqB,CAAC,EAC7C;gBAAEwB,SAAS0C,OAAOE,OAAO,GAAG,YAAY;YAAO;QAEnD,EAAE,OAAOC,OAAO;YACd5B,gBAAgB,AAAC4B,MAAgBC,OAAO,EAAE;gBAAE9C,SAAS;gBAAS+C,gBAAgB;YAAK;QACrF,SAAU;YACR3B,QAAQ;QACV;IACF;IAEA,qBACE,KAAC/E;QACC2G,QAAO;QACPC,MAAM9G,eAAe,aAAa;YAAE+G,QAAQ;QAA0B;QACtEC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAACjG;YAAMkG,SAAS;;8BACd,KAAChG;oBAAW2C,SAAQ;oBAAQK,OAAM;8BAC/B,oGACC,+FACA,iGACA;;8BAEJ,MAAClD;oBAAMmG,WAAW;wBAAEC,IAAI;wBAAUC,IAAI;oBAAM;oBAAGH,SAAS;oBAAGnD,IAAI;wBAAEuD,YAAY;4BAAED,IAAI;wBAAa;oBAAE;;sCAChG,KAACpG;4BACC8B,OAAOd,6BAA6BC,KAAK;4BACzCY,OAAOiC;4BACPwC,UAAU,CAACC,QAAUxC,SAASwC,MAAMC,MAAM,CAAC3E,KAAK;4BAChD4E,WAAW,CAACF;gCACV,IAAIA,MAAMG,GAAG,KAAK,WAAWzC,cAAc,CAAC3B,MAAM;oCAChDiE,MAAMI,cAAc;oCACpB,KAAKvB,OAAO,OAAOnB;gCACrB;4BACF;4BACAwB,OAAOvB;4BACP0C,YAAY1C,UAAU,0BAA0B7C;4BAChD2B,MAAK;4BACLF,IAAI;gCAAE+D,UAAU;4BAAE;;sCAEpB,KAACjH;4BACCgD,SAAQ;4BACRS,UAAU,CAACY,cAAc3B,SAAS;4BAClCgB,SAAS,IAAMW,cAAc,KAAKmB,OAAO,OAAOnB;sCAE/CjD,6BAA6BE,GAAG;;;;gBAGpC0D,OAAOM,MAAM,KAAK,aAAa,CAACZ,0BAC/B,KAAChE;oBAAgBwB,OAAM;qBACrB8C,OAAOM,MAAM,KAAK,WAAWN,OAAOM,MAAM,KAAK,0BACjD,KAAC3E;oBAAoB2E,QAAQN,OAAOM,MAAM;oBAAE4B,MAAK;qBAC/C7B,qBACF,KAAChF;oBAAW2C,SAAQ;oBAAQK,OAAM;8BAAiB;mCAInD,MAAClD;oBAAMkG,SAAS;;sCACd,KAAC9G;4BACCoC,QAAQD;4BACRyF,SAASvF;4BACT+C,SAASH,WAAWG,OAAO;4BAC3B+B,UAAUlC,WAAW4C,UAAU;4BAC/BC,SAAStF;;sCAEX,KAACtC;4BACC6H,SAAS5H,kBAAkBsF,OAAOuC,IAAI,CAACD,OAAO,EAAE;gCAC9C3F,QAAQD;gCACRyF,SAASvF;gCACTyF,SAAStF;4BACX;4BACAyF,SAASxC,OAAOuC,IAAI,CAACC,OAAO;;sCAE9B,KAAC7H;4BACC6D,cAAW;4BACXiE,SAAShF,cAAcC,SAAS,MAAM,CAAClB,SAAW,KAAKgE,OAAO,UAAUhE;4BACxE0D,MAAMA;4BACNwC,SAAS1C,OAAOM,MAAM,KAAK;4BAC3B;;;;;;eAMC,GACDqC,YAAW;4BACXC,aAAapD,WAAWoD,WAAW;4BACnCC,qBAAqBrD,WAAWqD,mBAAmB;4BACnDC,WAAW;4BACXC,oBAAoB;4BACpBC,aAAY;4BACZC,UAAU;;sCAEZ,KAACzI;4BACC+F,MAAMP,OAAOO,IAAI;4BACjB2C,UAAUlD,OAAOkD,QAAQ;4BACzBC,UAAUjD,KAAKN,MAAM;4BACrBwD,SAASpD,OAAOoD,OAAO;4BACvBC,cAAcrD,OAAOsD,OAAO;4BAC5BC,kBAAkBvD,OAAOwD,WAAW;;;;;;;AAOlD;AACA3E,gCAAgC4E,WAAW,GAAG;AAE9C,eAAe5E,gCAA+B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type OutreachEnrollment, type OutreachSequenceStep } from '../model/outreach.types';
|
|
3
3
|
import type { OutreachApi } from './use-outreach-api';
|
|
4
|
-
import type { OutreachEnrollmentsLoad } from './use-outreach-data';
|
|
5
4
|
export interface OutreachEnrollmentsTableProps {
|
|
6
|
-
enrollments
|
|
5
|
+
/** The organization, whose enrollments the table asks for. */
|
|
6
|
+
orgId: string;
|
|
7
|
+
/** The sequence whose people these are: every page is narrowed to it. */
|
|
8
|
+
sequenceId: string;
|
|
7
9
|
steps: readonly OutreachSequenceStep[];
|
|
8
10
|
/**
|
|
9
11
|
* Whether this sequence counts link clicks (AGL-3239). The Clicks column
|
|
@@ -17,19 +17,21 @@
|
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
19
19
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
20
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
20
21
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
21
22
|
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
22
23
|
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
23
|
-
import {
|
|
24
|
-
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
24
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
25
25
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
26
26
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
27
27
|
import { Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme, Chip } from "@mui/material";
|
|
28
|
-
import {
|
|
28
|
+
import { useState } from "react";
|
|
29
29
|
import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-step-dialog.js";
|
|
30
30
|
import { readOutreachEngagement } from "../model/enrollment-engagement.js";
|
|
31
|
-
import {
|
|
31
|
+
import { OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
|
|
32
|
+
import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_STOP_REASONS, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
|
|
32
33
|
import { formatOutreachTime, OUTREACH_ENROLLMENT_STATUS_LABELS, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
34
|
+
import { useOutreachEnrollmentList } from "./use-outreach-data.js";
|
|
33
35
|
/** The step an enrollment is on, as a person reads it. */ export function outreachCurrentStepLabel(enrollment, steps) {
|
|
34
36
|
if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length) return 'All steps done';
|
|
35
37
|
const step = steps[enrollment.stepIndex];
|
|
@@ -84,23 +86,19 @@ const ACTION_LABELS = {
|
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
88
|
/*
|
|
87
|
-
* What the enrollments grid's Filters panel offers (AGL-3317)
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
inMemoryListField('contactName', 'text'),
|
|
95
|
-
inMemoryListField('email', 'text'),
|
|
96
|
-
inMemoryListField('stopReason', 'text')
|
|
97
|
-
];
|
|
89
|
+
* What the enrollments grid's Filters panel offers (AGL-3317), each field on
|
|
90
|
+
* the table's Firestore query (AGL-3321, `OUTREACH_ENROLLMENT_LIST_QUERY`).
|
|
91
|
+
* Status, whether the person is a lead and the stop reason are picked; the
|
|
92
|
+
* address is typed whole. `target` and `email` are no column of their own,
|
|
93
|
+
* so they are hidden columns the panel can still reach; the search box
|
|
94
|
+
* reads the person's name and address.
|
|
95
|
+
*/ const ENROLLMENT_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields;
|
|
98
96
|
const ENROLLMENT_FILTER_HEADERS = {
|
|
99
97
|
status: 'Status',
|
|
100
98
|
target: 'Enrolled as',
|
|
101
|
-
|
|
99
|
+
stopReason: 'Stop reason',
|
|
102
100
|
email: 'Email',
|
|
103
|
-
|
|
101
|
+
enrolled: 'Enrolled'
|
|
104
102
|
};
|
|
105
103
|
const ENROLLMENT_FILTER_OPTIONS = {
|
|
106
104
|
status: OUTREACH_ENROLLMENT_STATUSES.map((status)=>({
|
|
@@ -116,14 +114,12 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
116
114
|
value: 'lead',
|
|
117
115
|
label: 'Lead'
|
|
118
116
|
}
|
|
119
|
-
]
|
|
117
|
+
],
|
|
118
|
+
stopReason: OUTREACH_STOP_REASONS.map((reason)=>({
|
|
119
|
+
value: reason,
|
|
120
|
+
label: OUTREACH_STOP_REASON_LABELS[reason]
|
|
121
|
+
}))
|
|
120
122
|
};
|
|
121
|
-
/** What the quick search reads on an enrollment row. */ const ENROLLMENT_SEARCH_FIELDS = [
|
|
122
|
-
'contactName',
|
|
123
|
-
'email',
|
|
124
|
-
'step',
|
|
125
|
-
'stopReason'
|
|
126
|
-
];
|
|
127
123
|
/** The filter-only columns never show. */ const ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [
|
|
128
124
|
'status',
|
|
129
125
|
'stopReason'
|
|
@@ -276,7 +272,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
276
272
|
* do-not-contact. A table on wider screens, a list of cards on a phone.
|
|
277
273
|
*/ export function OutreachEnrollmentsTable(props) {
|
|
278
274
|
var _CONFIRM_confirming_action, _CONFIRM_confirming_action1, _CONFIRM_confirming_action2;
|
|
279
|
-
const {
|
|
275
|
+
const { steps, timeZone, api } = props;
|
|
280
276
|
const trackClicks = props.trackClicks === true;
|
|
281
277
|
const theme = useTheme();
|
|
282
278
|
const narrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
@@ -285,45 +281,23 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
285
281
|
const [detail, setDetail] = useState('');
|
|
286
282
|
const [busy, setBusy] = useState(null);
|
|
287
283
|
const [curating, setCurating] = useState(null);
|
|
288
|
-
const [page, setPage] = useState(0);
|
|
289
|
-
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
290
284
|
const gridFilter = useListGridFilter({
|
|
291
285
|
selectFields: [
|
|
292
286
|
'status',
|
|
293
|
-
'target'
|
|
287
|
+
'target',
|
|
288
|
+
'stopReason'
|
|
294
289
|
]
|
|
295
290
|
});
|
|
296
|
-
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.
|
|
291
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
297
292
|
/*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
target: enrollment.target === 'lead' ? 'lead' : 'contact',
|
|
307
|
-
contactName: enrollment.contactName,
|
|
308
|
-
email: enrollment.email,
|
|
309
|
-
step: outreachCurrentStepLabel(enrollment, steps),
|
|
310
|
-
stopReason: outreachStopLabel(enrollment)
|
|
311
|
-
})), ENROLLMENT_FILTER_FIELDS, gridFilter.clauses, {
|
|
312
|
-
paths: ENROLLMENT_SEARCH_FIELDS,
|
|
313
|
-
words: gridFilter.searchWords
|
|
314
|
-
}).map((row)=>row.enrollment), // `searchKey` stands for the words, which are a new array each render.
|
|
315
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
316
|
-
[
|
|
317
|
-
loadedData,
|
|
318
|
-
steps,
|
|
319
|
-
gridFilter.clauses,
|
|
320
|
-
searchKey
|
|
321
|
-
]);
|
|
322
|
-
// A narrowed list starts again at its first page.
|
|
323
|
-
useEffect(()=>setPage(0), [
|
|
324
|
-
gridFilter.clauses,
|
|
325
|
-
searchKey
|
|
326
|
-
]);
|
|
293
|
+
* One page of the sequence's enrollments, newest enrolled first, with
|
|
294
|
+
* every clause and the search word on the query (AGL-3321): a filter
|
|
295
|
+
* reaches the enrollment on page forty as surely as the one on page one,
|
|
296
|
+
* and nothing is matched over the rows that happen to be loaded.
|
|
297
|
+
*/ const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {
|
|
298
|
+
clauses: gridFilter.clauses,
|
|
299
|
+
search: gridFilter.searchWords
|
|
300
|
+
});
|
|
327
301
|
const act = async (enrollment, action, why)=>{
|
|
328
302
|
setBusy(enrollment.id);
|
|
329
303
|
try {
|
|
@@ -354,16 +328,29 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
354
328
|
void act(enrollment, action);
|
|
355
329
|
}
|
|
356
330
|
};
|
|
357
|
-
|
|
331
|
+
const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
|
|
332
|
+
fields: ENROLLMENT_FILTER_FIELDS,
|
|
333
|
+
headers: ENROLLMENT_FILTER_HEADERS,
|
|
334
|
+
clauses: gridFilter.clauses,
|
|
335
|
+
onChange: gridFilter.setClauses,
|
|
336
|
+
options: ENROLLMENT_FILTER_OPTIONS
|
|
337
|
+
});
|
|
338
|
+
if (enrollments.status === 'loading' && !filtering) return /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
358
339
|
label: "Loading enrollments…"
|
|
359
340
|
});
|
|
360
341
|
if (enrollments.status === 'error' || enrollments.status === 'refused') {
|
|
361
|
-
return /*#__PURE__*/
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
343
|
+
spacing: 1.5,
|
|
344
|
+
children: [
|
|
345
|
+
chips,
|
|
346
|
+
/*#__PURE__*/ _jsx(OutreachLoadProblem, {
|
|
347
|
+
status: enrollments.status,
|
|
348
|
+
what: "enrollments"
|
|
349
|
+
})
|
|
350
|
+
]
|
|
364
351
|
});
|
|
365
352
|
}
|
|
366
|
-
if (!enrollments.
|
|
353
|
+
if (enrollments.status === 'ready' && !enrollments.rows.length && !filtering && enrollments.page === 0) {
|
|
367
354
|
return /*#__PURE__*/ _jsx(Card, {
|
|
368
355
|
variant: "outlined",
|
|
369
356
|
children: /*#__PURE__*/ _jsx(EmptyStateComponent, {
|
|
@@ -412,28 +399,19 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
412
399
|
}) : null
|
|
413
400
|
]
|
|
414
401
|
});
|
|
415
|
-
const
|
|
416
|
-
const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize);
|
|
417
|
-
const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1);
|
|
418
|
-
/** Past the rows read so far, the next page reads another window first. */ const turnTo = (next)=>{
|
|
419
|
-
if ((next + 1) * pageSize > loaded.length && enrollments.hasMore) enrollments.showMore();
|
|
420
|
-
setPage(next);
|
|
421
|
-
};
|
|
402
|
+
const pageRows = enrollments.rows;
|
|
422
403
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
423
404
|
spacing: 1.5,
|
|
424
405
|
children: [
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
406
|
+
chips,
|
|
407
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
408
|
+
refused: listQueryRefusals(enrollments.plan.refused, {
|
|
409
|
+
fields: ENROLLMENT_FILTER_FIELDS,
|
|
410
|
+
headers: ENROLLMENT_FILTER_HEADERS,
|
|
411
|
+
options: ENROLLMENT_FILTER_OPTIONS
|
|
412
|
+
}),
|
|
413
|
+
notices: enrollments.plan.notices
|
|
431
414
|
}),
|
|
432
|
-
filtering && enrollments.hasMore ? /*#__PURE__*/ _jsx(Typography, {
|
|
433
|
-
variant: "caption",
|
|
434
|
-
color: "text.secondary",
|
|
435
|
-
children: `Filtering the ${enrollments.data.length} enrollments read so far — the next page reads more.`
|
|
436
|
-
}) : null,
|
|
437
415
|
narrow ? /*#__PURE__*/ _jsx(Stack, {
|
|
438
416
|
spacing: 1,
|
|
439
417
|
component: "ul",
|
|
@@ -517,6 +495,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
517
495
|
columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS
|
|
518
496
|
}
|
|
519
497
|
},
|
|
498
|
+
loading: enrollments.status === 'loading',
|
|
520
499
|
rows: pageRows.map((enrollment)=>({
|
|
521
500
|
$id: enrollment.id,
|
|
522
501
|
enrollment,
|
|
@@ -530,23 +509,25 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
530
509
|
stopReason: outreachStopLabel(enrollment) || '—'
|
|
531
510
|
})),
|
|
532
511
|
/*
|
|
533
|
-
* The panel and the search are the grid's;
|
|
534
|
-
*
|
|
512
|
+
* The panel and the search are the grid's; every clause and the
|
|
513
|
+
* search word are on the table's query (AGL-3321). The grid
|
|
514
|
+
* neither filters nor sorts the page it is handed: the query's
|
|
515
|
+
* one order, newest enrolled first, is the table's.
|
|
535
516
|
*/ filterMode: "server",
|
|
536
517
|
filterModel: gridFilter.filterModel,
|
|
537
518
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
538
519
|
quickFilter: true,
|
|
520
|
+
disableColumnSorting: true,
|
|
539
521
|
noRowsLabel: "No enrollments match these filters",
|
|
540
522
|
hideFooter: true
|
|
541
523
|
}),
|
|
542
524
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
543
|
-
page: page,
|
|
544
|
-
pageSize: pageSize,
|
|
525
|
+
page: enrollments.page,
|
|
526
|
+
pageSize: enrollments.pageSize,
|
|
545
527
|
rowCount: pageRows.length,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
onPageSizeChange: setPageSize
|
|
528
|
+
hasMore: enrollments.hasMore,
|
|
529
|
+
onPageChange: enrollments.setPage,
|
|
530
|
+
onPageSizeChange: enrollments.setPageSize
|
|
550
531
|
}),
|
|
551
532
|
/*#__PURE__*/ _jsx(OutreachCurateStepDialog, {
|
|
552
533
|
enrollment: curating,
|