@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.232",
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.232",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.232",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.232",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.232",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.232",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.232",
34
- "@aglyn/shared-util-email": "1.0.0-beta.232",
35
- "@aglyn/shared-util-http": "1.0.0-beta.232",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.232",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.232",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.232",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.232",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.232",
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
- */ const listed = useOutreachDoNotContactDomainList(orgId, {
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: listed.plan.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: the query's
333
- * order — alphabetical, or newest added while a date applies —
334
- * is the list's.
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
- disableColumnSorting: true,
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 enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {
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: enrollments.plan.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: pageRows.map((enrollment)=>{
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: the query's
735
- * one order, newest enrolled first, is the table's.
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
- disableColumnSorting: true,
785
+ columnSort: columnSort,
741
786
  noRowsLabel: "No enrollments match these filters",
742
787
  hideFooter: true
743
788
  }),