@aglyn/plugins-outreach 1.0.0-beta.186 → 1.0.0-beta.188

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.186",
3
+ "version": "1.0.0-beta.188",
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.186",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.186",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.186",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.186",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.186",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.186",
34
- "@aglyn/shared-util-email": "1.0.0-beta.186",
35
- "@aglyn/shared-util-http": "1.0.0-beta.186",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.186",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.186",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.186",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.186",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.186",
28
+ "@aglyn/aglyn": "1.0.0-beta.188",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.188",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.188",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.188",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.188",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.188",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.188",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.188",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.188",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.188",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.188",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.188",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.188",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -1,4 +1,4 @@
1
- import type { OutreachDoNotContactReason } from '../model/outreach.types';
1
+ import { type OutreachDoNotContactReason } from '../model/outreach.types';
2
2
  export interface OutreachDoNotContactDomainsCardProps {
3
3
  /** The organization the hub is mounted under; `null` until the shell knows it. */
4
4
  orgId: string | null;
@@ -18,10 +18,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  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
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
+ import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
+ import { filterListRows, inMemoryListField, listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
24
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
21
25
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
22
- import { Button, Chip, IconButton, List, ListItem, ListItemText, Stack, TextField, Typography } from "@mui/material";
23
- import { useState } from "react";
26
+ import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
27
+ import { useMemo, useState } from "react";
24
28
  import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
29
+ import { OUTREACH_DO_NOT_CONTACT_REASONS } from "../model/outreach.types.js";
25
30
  import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
26
31
  import { useOutreachApi } from "./use-outreach-api.js";
27
32
  import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
@@ -38,12 +43,92 @@ import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
38
43
  remove: (domain)=>`Remove ${domain}`
39
44
  };
40
45
  /** 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
+ /*
47
+ * What the domains grid's Filters panel offers (AGL-3317). The card reads
48
+ * every listed domain, so the panel and the search answer over all of them.
49
+ */ const DOMAIN_FILTER_FIELDS = [
50
+ inMemoryListField('domain', 'text'),
51
+ inMemoryListField('reason', 'select'),
52
+ inMemoryListField('detail', 'text')
53
+ ];
54
+ const DOMAIN_FILTER_HEADERS = {
55
+ domain: 'Domain',
56
+ reason: 'Why',
57
+ detail: 'Detail'
58
+ };
59
+ const DOMAIN_FILTER_OPTIONS = {
60
+ reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason)=>({
61
+ value: reason,
62
+ label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason]
63
+ }))
64
+ };
65
+ /** What the quick search reads on a domain row. */ const DOMAIN_SEARCH_FIELDS = [
66
+ 'domain',
67
+ 'detail'
68
+ ];
41
69
  function addedOn(entry) {
42
70
  if (!entry.addedAtMs) return '';
43
71
  return new Intl.DateTimeFormat('en-US', {
44
72
  dateStyle: 'medium'
45
73
  }).format(entry.addedAtMs);
46
74
  }
75
+ /** The grid's columns; the remove button names its domain for a screen reader. */ function domainColumns(busy, remove) {
76
+ return listFilterGridColumns([
77
+ {
78
+ field: 'domain',
79
+ headerName: 'Domain',
80
+ flex: 1,
81
+ minWidth: 160,
82
+ renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
83
+ variant: "body2",
84
+ component: "span",
85
+ sx: {
86
+ fontWeight: 500
87
+ },
88
+ children: row.domain
89
+ })
90
+ },
91
+ {
92
+ field: 'reason',
93
+ headerName: 'Why',
94
+ flex: 1,
95
+ minWidth: 200,
96
+ renderCell: ({ row })=>{
97
+ var _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_row_reason;
98
+ return /*#__PURE__*/ _jsx(Chip, {
99
+ size: "small",
100
+ variant: "outlined",
101
+ color: row.reason === 'gateway_block' ? 'warning' : 'default',
102
+ label: (_OUTREACH_DO_NOT_CONTACT_REASON_LABELS_row_reason = OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason]) != null ? _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_row_reason : row.reason
103
+ });
104
+ }
105
+ },
106
+ {
107
+ field: 'addedAtMs',
108
+ headerName: 'Added',
109
+ width: 140,
110
+ renderCell: ({ row })=>row.added
111
+ },
112
+ {
113
+ field: 'detail',
114
+ headerName: 'Detail',
115
+ flex: 1,
116
+ minWidth: 160
117
+ },
118
+ listActionsColumn((row)=>/*#__PURE__*/ _jsx(IconButton, {
119
+ size: "small",
120
+ "aria-label": DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain),
121
+ disabled: busy,
122
+ onClick: ()=>remove(row.domain),
123
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
124
+ path: mdiTrashCanOutline.path,
125
+ fontSize: "small"
126
+ })
127
+ }), {
128
+ width: 72
129
+ })
130
+ ], DOMAIN_FILTER_FIELDS, DOMAIN_FILTER_OPTIONS, DOMAIN_FILTER_HEADERS);
131
+ }
47
132
  /**
48
133
  * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no
49
134
  * sequence emails anyone at, whoever enrolls them.
@@ -61,6 +146,32 @@ function addedOn(entry) {
61
146
  const [busy, setBusy] = useState(null);
62
147
  const normalized = normalizeOutreachDomain(draft);
63
148
  const invalid = draft.trim() !== '' && normalized === null;
149
+ const gridFilter = useListGridFilter({
150
+ selectFields: [
151
+ 'reason'
152
+ ]
153
+ });
154
+ const searchKey = gridFilter.searchWords.join(' ');
155
+ const rows = useMemo(()=>filterListRows(listed.data.map((entry)=>{
156
+ var _entry_addedAtMs, _entry_detail;
157
+ return {
158
+ $id: entry.domain,
159
+ domain: entry.domain,
160
+ reason: entry.reason,
161
+ added: addedOn(entry),
162
+ addedAtMs: (_entry_addedAtMs = entry.addedAtMs) != null ? _entry_addedAtMs : 0,
163
+ detail: (_entry_detail = entry.detail) != null ? _entry_detail : ''
164
+ };
165
+ }), DOMAIN_FILTER_FIELDS, gridFilter.clauses, {
166
+ paths: DOMAIN_SEARCH_FIELDS,
167
+ words: gridFilter.searchWords
168
+ }), // `searchKey` stands for the words, which are a new array each render.
169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
170
+ [
171
+ listed.data,
172
+ gridFilter.clauses,
173
+ searchKey
174
+ ]);
64
175
  const change = async (action, domain)=>{
65
176
  setBusy(domain);
66
177
  try {
@@ -139,64 +250,30 @@ function addedOn(entry) {
139
250
  variant: "body2",
140
251
  color: "text.secondary",
141
252
  children: "No domains yet."
142
- }) : /*#__PURE__*/ _jsx(List, {
143
- dense: true,
144
- disablePadding: true,
145
- "aria-label": "Do not contact domains",
146
- children: listed.data.map((entry)=>{
147
- var _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason;
148
- return /*#__PURE__*/ _jsx(ListItem, {
149
- disableGutters: true,
150
- secondaryAction: /*#__PURE__*/ _jsx(IconButton, {
151
- edge: "end",
152
- size: "small",
153
- "aria-label": DO_NOT_CONTACT_DOMAIN_LABELS.remove(entry.domain),
154
- disabled: busy !== null,
155
- onClick: ()=>void change('remove', entry.domain),
156
- children: /*#__PURE__*/ _jsx(MdiIcon, {
157
- path: mdiTrashCanOutline.path,
158
- fontSize: "small"
159
- })
160
- }),
161
- children: /*#__PURE__*/ _jsx(ListItemText, {
162
- primary: /*#__PURE__*/ _jsxs(Stack, {
163
- direction: "row",
164
- spacing: 1,
165
- sx: {
166
- alignItems: 'center',
167
- flexWrap: 'wrap'
168
- },
169
- children: [
170
- /*#__PURE__*/ _jsx(Typography, {
171
- variant: "body2",
172
- component: "span",
173
- sx: {
174
- fontWeight: 500
175
- },
176
- children: entry.domain
177
- }),
178
- /*#__PURE__*/ _jsx(Chip, {
179
- size: "small",
180
- variant: "outlined",
181
- color: entry.reason === 'gateway_block' ? 'warning' : 'default',
182
- label: (_OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason = OUTREACH_DO_NOT_CONTACT_REASON_LABELS[entry.reason]) != null ? _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason : entry.reason
183
- })
184
- ]
185
- }),
186
- secondary: [
187
- addedOn(entry) && `Added ${addedOn(entry)}`,
188
- entry.detail
189
- ].filter(Boolean).join(' · '),
190
- slotProps: {
191
- secondary: {
192
- sx: {
193
- wordBreak: 'break-word'
194
- }
195
- }
196
- }
197
- })
198
- }, entry.domain);
199
- })
253
+ }) : /*#__PURE__*/ _jsxs(Stack, {
254
+ spacing: 1,
255
+ children: [
256
+ /*#__PURE__*/ _jsx(ListFilterChips, {
257
+ fields: DOMAIN_FILTER_FIELDS,
258
+ headers: DOMAIN_FILTER_HEADERS,
259
+ clauses: gridFilter.clauses,
260
+ onChange: gridFilter.setClauses,
261
+ options: DOMAIN_FILTER_OPTIONS
262
+ }),
263
+ /*#__PURE__*/ _jsx(ListTable, {
264
+ "aria-label": "Do not contact domains",
265
+ columns: domainColumns(busy !== null, (domain)=>void change('remove', domain)),
266
+ rows: rows,
267
+ /*
268
+ * The panel and the search are the grid's; the card answers
269
+ * them over every listed domain (AGL-3317).
270
+ */ filterMode: "server",
271
+ filterModel: gridFilter.filterModel,
272
+ onFilterModelChange: gridFilter.onFilterModelChange,
273
+ quickFilter: true,
274
+ noRowsLabel: "No domains match these filters"
275
+ })
276
+ ]
200
277
  })
201
278
  ]
202
279
  })
@@ -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 { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n IconButton,\n List,\n ListItem,\n ListItemText,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport type {\n OutreachDoNotContactDomainEntry,\n 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\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/**\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\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 <List dense disablePadding aria-label=\"Do not contact domains\">\n {listed.data.map((entry) => (\n <ListItem\n key={entry.domain}\n disableGutters\n secondaryAction={\n <IconButton\n edge=\"end\"\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(entry.domain)}\n disabled={busy !== null}\n onClick={() => void change('remove', entry.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n }\n >\n <ListItemText\n primary={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {entry.domain}\n </Typography>\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={entry.reason === 'gateway_block' ? 'warning' : 'default'}\n label={OUTREACH_DO_NOT_CONTACT_REASON_LABELS[entry.reason] ?? entry.reason}\n />\n </Stack>\n }\n secondary={[addedOn(entry) && `Added ${addedOn(entry)}`, entry.detail].filter(Boolean).join(' · ')}\n slotProps={{ secondary: { sx: { wordBreak: 'break-word' } } }}\n />\n </ListItem>\n ))}\n </List>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","useSnackbar","Button","Chip","IconButton","List","ListItem","ListItemText","Stack","TextField","Typography","useState","normalizeOutreachDomain","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","addedOn","entry","addedAtMs","Intl","DateTimeFormat","dateStyle","format","OutreachDoNotContactDomainsCard","props","orgId","api","listed","enqueueSnackbar","draft","setDraft","busy","setBusy","normalized","invalid","trim","change","action","answer","changeDoNotContactDomain","changed","variant","error","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","color","direction","xs","sm","sx","alignItems","label","value","onChange","event","target","onKeyDown","key","preventDefault","helperText","size","flexGrow","disabled","onClick","status","what","data","length","dense","disablePadding","aria-label","map","disableGutters","secondaryAction","edge","path","fontSize","primary","flexWrap","component","fontWeight","reason","secondary","detail","filter","Boolean","join","slotProps","wordBreak","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 {\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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"}
@@ -16,15 +16,19 @@
16
16
  */ 'use client';
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
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
19
20
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
20
21
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
22
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
23
+ import { filterListRows, inMemoryListField, listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
21
24
  import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
25
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
22
26
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
27
  import { Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme, Chip } from "@mui/material";
24
- import { useState } from "react";
28
+ import { useEffect, useMemo, useState } from "react";
25
29
  import { readOutreachEngagement } from "../model/enrollment-engagement.js";
26
- import { OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
27
- import { formatOutreachTime, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
30
+ import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
31
+ import { formatOutreachTime, OUTREACH_ENROLLMENT_STATUS_LABELS, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
28
32
  /** The step an enrollment is on, as a person reads it. */ export function outreachCurrentStepLabel(enrollment, steps) {
29
33
  if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length) return 'All steps done';
30
34
  const step = steps[enrollment.stepIndex];
@@ -71,6 +75,51 @@ const ACTION_LABELS = {
71
75
  label: 'Mark do-not-contact'
72
76
  }
73
77
  };
78
+ /*
79
+ * What the enrollments grid's Filters panel offers (AGL-3317). Status and
80
+ * whether the person is a lead are picked; the name, the address and the
81
+ * stop reason are typed. `target` and `email` are no column of their own,
82
+ * so they are hidden columns the panel can still reach.
83
+ */ const ENROLLMENT_FILTER_FIELDS = [
84
+ inMemoryListField('status', 'select'),
85
+ inMemoryListField('target', 'select'),
86
+ inMemoryListField('contactName', 'text'),
87
+ inMemoryListField('email', 'text'),
88
+ inMemoryListField('stopReason', 'text')
89
+ ];
90
+ const ENROLLMENT_FILTER_HEADERS = {
91
+ status: 'Status',
92
+ target: 'Enrolled as',
93
+ contactName: 'Name',
94
+ email: 'Email',
95
+ stopReason: 'Stop reason'
96
+ };
97
+ const ENROLLMENT_FILTER_OPTIONS = {
98
+ status: OUTREACH_ENROLLMENT_STATUSES.map((status)=>({
99
+ value: status,
100
+ label: OUTREACH_ENROLLMENT_STATUS_LABELS[status]
101
+ })),
102
+ target: [
103
+ {
104
+ value: 'contact',
105
+ label: 'Contact'
106
+ },
107
+ {
108
+ value: 'lead',
109
+ label: 'Lead'
110
+ }
111
+ ]
112
+ };
113
+ /** What the quick search reads on an enrollment row. */ const ENROLLMENT_SEARCH_FIELDS = [
114
+ 'contactName',
115
+ 'email',
116
+ 'step',
117
+ 'stopReason'
118
+ ];
119
+ /** The filter-only columns never show. */ const ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [
120
+ 'status',
121
+ 'stopReason'
122
+ ]);
74
123
  /** The table's columns; the person and the actions are drawn from the row's enrollment. */ function columns(rowActions, trackClicks) {
75
124
  return [
76
125
  {
@@ -207,6 +256,43 @@ const ACTION_LABELS = {
207
256
  const [busy, setBusy] = useState(null);
208
257
  const [page, setPage] = useState(0);
209
258
  const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
259
+ const gridFilter = useListGridFilter({
260
+ selectFields: [
261
+ 'status',
262
+ 'target'
263
+ ]
264
+ });
265
+ const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0;
266
+ /*
267
+ * The rows the grid is handed, every clause and the search answered over
268
+ * the enrollments read so far — a window that grows as the reader pages
269
+ * past it, so a filter reaches further the further they go.
270
+ */ const searchKey = gridFilter.searchWords.join(' ');
271
+ const loadedData = enrollments.data;
272
+ const matching = useMemo(()=>filterListRows(loadedData.map((enrollment)=>({
273
+ enrollment,
274
+ status: enrollment.status,
275
+ target: enrollment.target === 'lead' ? 'lead' : 'contact',
276
+ contactName: enrollment.contactName,
277
+ email: enrollment.email,
278
+ step: outreachCurrentStepLabel(enrollment, steps),
279
+ stopReason: outreachStopLabel(enrollment)
280
+ })), ENROLLMENT_FILTER_FIELDS, gridFilter.clauses, {
281
+ paths: ENROLLMENT_SEARCH_FIELDS,
282
+ words: gridFilter.searchWords
283
+ }).map((row)=>row.enrollment), // `searchKey` stands for the words, which are a new array each render.
284
+ // eslint-disable-next-line react-hooks/exhaustive-deps
285
+ [
286
+ loadedData,
287
+ steps,
288
+ gridFilter.clauses,
289
+ searchKey
290
+ ]);
291
+ // A narrowed list starts again at its first page.
292
+ useEffect(()=>setPage(0), [
293
+ gridFilter.clauses,
294
+ searchKey
295
+ ]);
210
296
  const act = async (enrollment, action, why)=>{
211
297
  setBusy(enrollment.id);
212
298
  try {
@@ -293,7 +379,7 @@ const ACTION_LABELS = {
293
379
  }) : null
294
380
  ]
295
381
  });
296
- const loaded = enrollments.data;
382
+ const loaded = matching;
297
383
  const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize);
298
384
  const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1);
299
385
  /** Past the rows read so far, the next page reads another window first. */ const turnTo = (next)=>{
@@ -303,6 +389,18 @@ const ACTION_LABELS = {
303
389
  return /*#__PURE__*/ _jsxs(Stack, {
304
390
  spacing: 1.5,
305
391
  children: [
392
+ /*#__PURE__*/ _jsx(ListFilterChips, {
393
+ fields: ENROLLMENT_FILTER_FIELDS,
394
+ headers: ENROLLMENT_FILTER_HEADERS,
395
+ clauses: gridFilter.clauses,
396
+ onChange: gridFilter.setClauses,
397
+ options: ENROLLMENT_FILTER_OPTIONS
398
+ }),
399
+ filtering && enrollments.hasMore ? /*#__PURE__*/ _jsx(Typography, {
400
+ variant: "caption",
401
+ color: "text.secondary",
402
+ children: `Filtering the ${enrollments.data.length} enrollments read so far — the next page reads more.`
403
+ }) : null,
306
404
  narrow ? /*#__PURE__*/ _jsx(Stack, {
307
405
  spacing: 1,
308
406
  component: "ul",
@@ -365,17 +463,32 @@ const ACTION_LABELS = {
365
463
  }, enrollment.id))
366
464
  }) : /*#__PURE__*/ _jsx(ListTable, {
367
465
  "aria-label": "Enrollments",
368
- columns: columns(rowActions, trackClicks),
466
+ columns: listFilterGridColumns(columns(rowActions, trackClicks), ENROLLMENT_FILTER_FIELDS, ENROLLMENT_FILTER_OPTIONS, ENROLLMENT_FILTER_HEADERS),
467
+ initialState: {
468
+ columns: {
469
+ columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS
470
+ }
471
+ },
369
472
  rows: pageRows.map((enrollment)=>({
370
473
  $id: enrollment.id,
371
474
  enrollment,
372
475
  status: enrollment.status,
476
+ target: enrollment.target === 'lead' ? 'lead' : 'contact',
477
+ contactName: enrollment.contactName,
478
+ email: enrollment.email,
373
479
  step: outreachCurrentStepLabel(enrollment, steps),
374
480
  nextSend: nextSend(enrollment),
375
481
  lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
376
482
  stopReason: outreachStopLabel(enrollment) || '—'
377
483
  })),
378
- quickFilter: false,
484
+ /*
485
+ * The panel and the search are the grid's; the table answers them
486
+ * over the enrollments it read (AGL-3317).
487
+ */ filterMode: "server",
488
+ filterModel: gridFilter.filterModel,
489
+ onFilterModelChange: gridFilter.onFilterModelChange,
490
+ quickFilter: true,
491
+ noRowsLabel: "No enrollments match these filters",
379
492
  hideFooter: true
380
493
  }),
381
494
  /*#__PURE__*/ _jsx(ListPagination, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollments-table.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 EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n Chip,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport { readOutreachEngagement } from '../model/enrollment-engagement'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /**\n * Whether this sequence counts link clicks (AGL-3239). The Clicks column\n * is shown only when it does: a column of dashes for a sequence that\n * measures nothing reads as \"nobody clicked\", which is a different claim\n * and a false one.\n */\n trackClicks?: boolean\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n): OutreachEnrollmentAction[] {\n const actions: OutreachEnrollmentAction[] = []\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<OutreachEnrollmentAction, string> = {\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n trackClicks: boolean,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n { field: 'step', headerName: 'Current step', flex: 1, minWidth: 170 },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: { enrollment: OutreachEnrollment } }) => {\n const engagement = readOutreachEngagement(row.enrollment.engagement)\n if (!engagement.clicks) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n return (\n <Stack spacing={0} sx={{ justifyContent: 'center', height: '100%' }}>\n <Typography variant=\"body2\">\n {engagement.clicks === 1 ? '1 click' : `${engagement.clicks} clicks`}\n </Typography>\n {engagement.lastClickUrl ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {engagement.lastClickUrl}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n ]\n : []),\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const trackClicks = props.trackClicks === true\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n ) => {\n if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = enrollments.data\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {trackClicks && readOutreachEngagement(enrollment.engagement).clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicked ${readOutreachEngagement(enrollment.engagement).clicks} ${\n readOutreachEngagement(enrollment.engagement).clicks === 1 ? 'time' : 'times'\n }`}\n </Typography>\n ) : null}\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={columns(rowActions, trackClicks)}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n quickFilter={false}\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_PAGE_SIZE_DEFAULT","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","Chip","useState","readOutreachEngagement","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","pause","resume","stop","do_not_contact","DONE_LABELS","CONFIRM","title","body","label","columns","rowActions","trackClicks","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","direction","alignItems","variant","noWrap","contactName","email","target","size","color","width","engagement","clicks","lastClickUrl","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","page","setPage","pageSize","setPageSize","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","data","description","enrollAction","items","map","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","component","listStyle","p","m","aria-label","flexGrow","rows","$id","lastActivity","quickFilter","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","open","Boolean","onClose","fullWidth","maxWidth","value","onChange","event","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollments-table.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 EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Card,\n CardContent,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n Chip,\n} from '@mui/material'\nimport { type ReactNode, useEffect, useMemo, useState } from 'react'\nimport { readOutreachEngagement } from '../model/enrollment-engagement'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport {\n OUTREACH_ENROLLMENT_STATUSES,\n OUTREACH_TASK_KIND_LABELS,\n type OutreachEnrollment,\n type OutreachSequenceStep,\n} from '../model/outreach.types'\nimport {\n formatOutreachTime,\n OUTREACH_ENROLLMENT_STATUS_LABELS,\n OUTREACH_STOP_REASON_LABELS,\n OutreachEnrollmentStatusChip,\n OutreachLoading,\n OutreachLoadProblem,\n} from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\nimport type { OutreachEnrollmentsLoad } from './use-outreach-data'\n\nexport interface OutreachEnrollmentsTableProps {\n enrollments: OutreachEnrollmentsLoad\n steps: readonly OutreachSequenceStep[]\n /**\n * Whether this sequence counts link clicks (AGL-3239). The Clicks column\n * is shown only when it does: a column of dashes for a sequence that\n * measures nothing reads as \"nobody clicked\", which is a different claim\n * and a false one.\n */\n trackClicks?: boolean\n /** The mailbox's IANA zone, which next sends are read in; `null` for the reader's own. */\n timeZone: string | null\n api: OutreachApi\n /** Shown in the empty state when the sequence takes people. */\n enrollAction?: ReactNode\n}\n\n/** The step an enrollment is on, as a person reads it. */\nexport function outreachCurrentStepLabel(\n enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex'>,\n steps: readonly OutreachSequenceStep[],\n): string {\n if (enrollment.status === 'finished' || enrollment.stepIndex >= steps.length)\n return 'All steps done'\n const step = steps[enrollment.stepIndex]\n const kind =\n step?.kind === 'email'\n ? 'Email'\n : step?.kind === 'task'\n ? OUTREACH_TASK_KIND_LABELS[step.taskKind]\n : 'Step'\n return `Step ${enrollment.stepIndex + 1} of ${steps.length} · ${kind}`\n}\n\n/** Why an enrollment stopped, as the table says it; `''` while it runs. */\nexport function outreachStopLabel(\n enrollment: Pick<OutreachEnrollment, 'stopReason' | 'stopDetail'>,\n): string {\n if (!enrollment.stopReason) return ''\n const reason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason] ?? ''\n return enrollment.stopDetail ? `${reason} — ${enrollment.stopDetail}` : reason\n}\n\n/** The last thing that happened: a send, a stop, or the enrollment itself. */\nfunction lastActivityMs(enrollment: OutreachEnrollment): number {\n return Math.max(\n enrollment.lastSentAtMs ?? 0,\n enrollment.stoppedAtMs ?? 0,\n enrollment.createdAtMs ?? 0,\n )\n}\n\n/** What a member may do to an enrollment in the status it is in. */\nfunction actionsFor(\n enrollment: OutreachEnrollment,\n): OutreachEnrollmentAction[] {\n const actions: OutreachEnrollmentAction[] = []\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nconst ACTION_LABELS: Record<OutreachEnrollmentAction, string> = {\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<\n Exclude<OutreachEnrollmentAction, 'do_not_contact'>,\n string\n> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<\n Record<\n OutreachEnrollmentAction,\n { title: string; body: string; label: string }\n >\n> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\n/*\n * What the enrollments grid's Filters panel offers (AGL-3317). Status and\n * whether the person is a lead are picked; the name, the address and the\n * stop reason are typed. `target` and `email` are no column of their own,\n * so they are hidden columns the panel can still reach.\n */\nconst ENROLLMENT_FILTER_FIELDS = [\n inMemoryListField('status', 'select'),\n inMemoryListField('target', 'select'),\n inMemoryListField('contactName', 'text'),\n inMemoryListField('email', 'text'),\n inMemoryListField('stopReason', 'text'),\n]\nconst ENROLLMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n target: 'Enrolled as',\n contactName: 'Name',\n email: 'Email',\n stopReason: 'Stop reason',\n}\nconst ENROLLMENT_FILTER_OPTIONS = {\n status: OUTREACH_ENROLLMENT_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_ENROLLMENT_STATUS_LABELS[status],\n })),\n target: [\n { value: 'contact', label: 'Contact' },\n { value: 'lead', label: 'Lead' },\n ],\n}\n/** What the quick search reads on an enrollment row. */\nconst ENROLLMENT_SEARCH_FIELDS = ['contactName', 'email', 'step', 'stopReason'] as const\n/** The filter-only columns never show. */\nconst ENROLLMENT_HIDDEN_COLUMNS = hiddenFilterVisibility(ENROLLMENT_FILTER_FIELDS, [\n 'status',\n 'stopReason',\n])\n\n/** The table's columns; the person and the actions are drawn from the row's enrollment. */\nfunction columns(\n rowActions: (enrollment: OutreachEnrollment) => ReactNode,\n trackClicks: boolean,\n): NonNullable<ListTableProps['columns']> {\n return [\n {\n field: 'person',\n headerName: 'Person',\n flex: 1,\n minWidth: 200,\n sortable: false,\n renderCell: ({ row }) => (\n <Stack\n spacing={0}\n sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}\n >\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.enrollment.contactName || row.enrollment.email}\n </Typography>\n {row.enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {row.enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }) => (\n <OutreachEnrollmentStatusChip status={row.status} />\n ),\n },\n { field: 'step', headerName: 'Current step', flex: 1, minWidth: 170 },\n { field: 'nextSend', headerName: 'Next send', width: 190, sortable: false },\n {\n field: 'lastActivity',\n headerName: 'Last activity',\n width: 190,\n sortable: false,\n },\n ...(trackClicks\n ? [\n {\n field: 'clicks',\n headerName: 'Clicks',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: { enrollment: OutreachEnrollment } }) => {\n const engagement = readOutreachEngagement(row.enrollment.engagement)\n if (!engagement.clicks) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n —\n </Typography>\n )\n }\n return (\n <Stack spacing={0} sx={{ justifyContent: 'center', height: '100%' }}>\n <Typography variant=\"body2\">\n {engagement.clicks === 1 ? '1 click' : `${engagement.clicks} clicks`}\n </Typography>\n {engagement.lastClickUrl ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {engagement.lastClickUrl}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n ]\n : []),\n { field: 'stopReason', headerName: 'Stop reason', flex: 1, minWidth: 170 },\n listActionsColumn((row) => rowActions(row.enrollment), { width: 72 }),\n ]\n}\n\n/**\n * A sequence's enrollments (AGL-2980): who is in it, where each person\n * stands, when their next step goes out in the mailbox's timezone, and the\n * four things a member can do about it — pause, resume, stop, and mark\n * do-not-contact. A table on wider screens, a list of cards on a phone.\n */\nexport function OutreachEnrollmentsTable(props: OutreachEnrollmentsTableProps) {\n const { enrollments, steps, timeZone, api } = props\n const trackClicks = props.trackClicks === true\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status', 'target'] })\n const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0\n /*\n * The rows the grid is handed, every clause and the search answered over\n * the enrollments read so far — a window that grows as the reader pages\n * past it, so a filter reaches further the further they go.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const loadedData = enrollments.data\n const matching = useMemo(\n () =>\n filterListRows(\n loadedData.map((enrollment) => ({\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n stopReason: outreachStopLabel(enrollment),\n })),\n ENROLLMENT_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: ENROLLMENT_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.enrollment),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [loadedData, steps, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n\n const act = async (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n why?: string,\n ) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (\n enrollment: OutreachEnrollment,\n action: OutreachEnrollmentAction,\n ) => {\n if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n if (enrollments.status === 'loading')\n return <OutreachLoading label=\"Loading enrollments…\" />\n if (enrollments.status === 'error' || enrollments.status === 'refused') {\n return (\n <OutreachLoadProblem status={enrollments.status} what=\"enrollments\" />\n )\n }\n if (!enrollments.data.length) {\n return (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"Nobody is enrolled yet\"\n description=\"People you enroll show here, with where each of them is in the sequence.\"\n action={props.enrollAction}\n />\n </Card>\n )\n }\n\n const rowActions = (enrollment: OutreachEnrollment) => (\n <ListRowActions\n label={enrollment.contactName || enrollment.email}\n items={actionsFor(enrollment).map((action) => ({\n key: action,\n label: ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: action === 'stop' || action === 'do_not_contact',\n disabled: busy === enrollment.id,\n }))}\n />\n )\n const nextSend = (enrollment: OutreachEnrollment) =>\n enrollment.status === 'active' || enrollment.status === 'paused'\n ? formatOutreachTime(enrollment.nextDueAtMs, timeZone)\n : '—'\n const person = (enrollment: OutreachEnrollment) => (\n <Stack spacing={0}>\n <Stack direction=\"row\" spacing={0.75} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">\n {enrollment.contactName || enrollment.email}\n </Typography>\n {enrollment.target === 'lead' ? (\n <Chip size=\"small\" variant=\"outlined\" label=\"Lead\" />\n ) : null}\n </Stack>\n {enrollment.contactName ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {enrollment.email}\n </Typography>\n ) : null}\n </Stack>\n )\n\n const loaded = matching\n const pageRows = loaded.slice(page * pageSize, (page + 1) * pageSize)\n const lastLoadedPage = Math.max(0, Math.ceil(loaded.length / pageSize) - 1)\n /** Past the rows read so far, the next page reads another window first. */\n const turnTo = (next: number) => {\n if ((next + 1) * pageSize > loaded.length && enrollments.hasMore)\n enrollments.showMore()\n setPage(next)\n }\n\n return (\n <Stack spacing={1.5}>\n <ListFilterChips\n fields={ENROLLMENT_FILTER_FIELDS}\n headers={ENROLLMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={ENROLLMENT_FILTER_OPTIONS}\n />\n {filtering && enrollments.hasMore ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Filtering the ${enrollments.data.length} enrollments read so far — the next page reads more.`}\n </Typography>\n ) : null}\n {narrow ? (\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Enrollments\"\n >\n {pageRows.map((enrollment) => (\n <Card key={enrollment.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n >\n <Stack sx={{ flexGrow: 1, minWidth: 0 }}>\n {person(enrollment)}\n </Stack>\n <OutreachEnrollmentStatusChip status={enrollment.status} />\n {rowActions(enrollment)}\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachCurrentStepLabel(enrollment, steps)}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`}\n </Typography>\n {trackClicks && readOutreachEngagement(enrollment.engagement).clicks ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Clicked ${readOutreachEngagement(enrollment.engagement).clicks} ${\n readOutreachEngagement(enrollment.engagement).clicks === 1 ? 'time' : 'times'\n }`}\n </Typography>\n ) : null}\n {outreachStopLabel(enrollment) ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {outreachStopLabel(enrollment)}\n </Typography>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n ) : (\n <ListTable\n aria-label=\"Enrollments\"\n columns={listFilterGridColumns(\n columns(rowActions, trackClicks),\n ENROLLMENT_FILTER_FIELDS,\n ENROLLMENT_FILTER_OPTIONS,\n ENROLLMENT_FILTER_HEADERS,\n )}\n initialState={{ columns: { columnVisibilityModel: ENROLLMENT_HIDDEN_COLUMNS } }}\n rows={pageRows.map((enrollment) => ({\n $id: enrollment.id,\n enrollment,\n status: enrollment.status,\n target: enrollment.target === 'lead' ? 'lead' : 'contact',\n contactName: enrollment.contactName,\n email: enrollment.email,\n step: outreachCurrentStepLabel(enrollment, steps),\n nextSend: nextSend(enrollment),\n lastActivity: formatOutreachTime(\n lastActivityMs(enrollment),\n timeZone,\n ),\n stopReason: outreachStopLabel(enrollment) || '—',\n }))}\n /*\n * The panel and the search are the grid's; the table answers them\n * over the enrollments it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No enrollments match these filters\"\n hideFooter\n />\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={enrollments.hasMore ? undefined : loaded.length}\n hasMore={enrollments.hasMore || page < lastLoadedPage}\n onPageChange={turnTo}\n onPageSizeChange={setPageSize}\n />\n\n <Dialog\n open={Boolean(confirming)}\n onClose={() => setConfirming(null)}\n fullWidth\n maxWidth=\"xs\"\n >\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>\n {CONFIRM[confirming.action]?.body}\n </DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </Stack>\n )\n}\nOutreachEnrollmentsTable.displayName = 'OutreachEnrollmentsTable'\n\nexport default OutreachEnrollmentsTable\n"],"names":["EmptyStateComponent","ListFilterChips","ListPagination","ListRowActions","ListTable","listActionsColumn","hiddenFilterVisibility","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","useSnackbar","Button","Card","CardContent","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","Chip","useEffect","useMemo","useState","readOutreachEngagement","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_TASK_KIND_LABELS","formatOutreachTime","OUTREACH_ENROLLMENT_STATUS_LABELS","OUTREACH_STOP_REASON_LABELS","OutreachEnrollmentStatusChip","OutreachLoading","OutreachLoadProblem","outreachCurrentStepLabel","enrollment","steps","status","stepIndex","length","step","kind","taskKind","outreachStopLabel","stopReason","reason","stopDetail","lastActivityMs","Math","max","lastSentAtMs","stoppedAtMs","createdAtMs","actionsFor","actions","push","ACTION_LABELS","pause","resume","stop","do_not_contact","DONE_LABELS","CONFIRM","title","body","label","ENROLLMENT_FILTER_FIELDS","ENROLLMENT_FILTER_HEADERS","target","contactName","email","ENROLLMENT_FILTER_OPTIONS","map","value","ENROLLMENT_SEARCH_FIELDS","ENROLLMENT_HIDDEN_COLUMNS","columns","rowActions","trackClicks","field","headerName","flex","minWidth","sortable","renderCell","row","spacing","sx","justifyContent","height","direction","alignItems","variant","noWrap","size","color","width","engagement","clicks","lastClickUrl","OutreachEnrollmentsTable","props","enrollments","timeZone","api","theme","narrow","breakpoints","down","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","page","setPage","pageSize","setPageSize","gridFilter","selectFields","filtering","clauses","searchWords","searchKey","join","loadedData","data","matching","paths","words","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","error","message","allowDuplicate","choose","what","description","enrollAction","items","key","onClick","destructive","disabled","nextSend","nextDueAtMs","person","loaded","pageRows","slice","lastLoadedPage","ceil","turnTo","next","hasMore","showMore","fields","headers","onChange","setClauses","options","component","listStyle","p","m","aria-label","flexGrow","initialState","columnVisibilityModel","rows","$id","lastActivity","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","rowCount","count","undefined","onPageChange","onPageSizeChange","open","Boolean","onClose","fullWidth","maxWidth","event","slotProps","htmlInput","maxLength","trim","displayName"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { type ConsolePluginOrgMount } from '@aglyn/aglyn';
2
- import type { OutreachMailbox } from '../model/outreach.types';
2
+ import { type OutreachMailbox } from '../model/outreach.types';
3
3
  export interface OutreachSequencesSectionProps {
4
4
  /** The organization the hub is mounted under; `null` until the shell knows it. */
5
5
  orgId: string | null;
@@ -20,12 +20,16 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { mdiPlus } from "@aglyn/shared-data-mdi";
21
21
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
22
22
  import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
23
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
23
24
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
24
25
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
26
+ import { filterListRows, inMemoryListField, listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
27
  import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
28
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
26
29
  import { Button, Card, CardContent, Chip, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
27
- import { useState } from "react";
28
- import { OutreachLink, OutreachLoading, OutreachLoadProblem, OutreachSequenceStatusChip, useOutreachNavigate } from "./outreach-ui.js";
30
+ import { useEffect, useMemo, useState } from "react";
31
+ import { OUTREACH_SEQUENCE_STATUSES } from "../model/outreach.types.js";
32
+ import { OUTREACH_SEQUENCE_STATUS_LABELS, OutreachLink, OutreachLoading, OutreachLoadProblem, OutreachSequenceStatusChip, useOutreachNavigate } from "./outreach-ui.js";
29
33
  import { OutreachSequenceDetail } from "./sequence-detail.js";
30
34
  import { OutreachSequenceEditor } from "./sequence-editor.js";
31
35
  import { useOutreachApi } from "./use-outreach-api.js";
@@ -116,6 +120,30 @@ const COUNT_COLUMNS = [
116
120
  'Opted out'
117
121
  ]
118
122
  ];
123
+ /*
124
+ * What the sequences grid's Filters panel offers (AGL-3317). The list holds
125
+ * every sequence it read, so each clause and the quick search are answered
126
+ * over that whole set before it is paged, never over the page on screen.
127
+ */ const SEQUENCE_FILTER_FIELDS = [
128
+ inMemoryListField('name', 'text'),
129
+ inMemoryListField('mailbox', 'text'),
130
+ inMemoryListField('status', 'select')
131
+ ];
132
+ const SEQUENCE_FILTER_HEADERS = {
133
+ name: 'Name',
134
+ mailbox: 'Mailbox',
135
+ status: 'Status'
136
+ };
137
+ const SEQUENCE_FILTER_OPTIONS = {
138
+ status: OUTREACH_SEQUENCE_STATUSES.map((status)=>({
139
+ value: status,
140
+ label: OUTREACH_SEQUENCE_STATUS_LABELS[status]
141
+ }))
142
+ };
143
+ /** What the quick search reads on a sequence row. */ const SEQUENCE_SEARCH_FIELDS = [
144
+ 'name',
145
+ 'mailbox'
146
+ ];
119
147
  /**
120
148
  * Every sequence: its mailbox, its status, and how many people it enrolled
121
149
  * and where they stand — a table on wider screens, cards on a phone.
@@ -127,14 +155,49 @@ const COUNT_COLUMNS = [
127
155
  const sequences = useOutreachSequences(orgId);
128
156
  const [page, setPage] = useState(0);
129
157
  const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
130
- const pageRows = sequences.data.slice(page * pageSize, (page + 1) * pageSize);
158
+ const gridFilter = useListGridFilter({
159
+ selectFields: [
160
+ 'status'
161
+ ]
162
+ });
163
+ const mailboxLabel = (sequence)=>{
164
+ const mailbox = props.mailboxes.find((entry)=>entry.id === sequence.mailboxId);
165
+ return mailbox ? mailbox.sendAs || mailbox.email : sequence.mailboxId ? 'Mailbox removed' : 'No mailbox yet';
166
+ };
167
+ /*
168
+ * Filtered BEFORE the page is cut: the list read every sequence (under
169
+ * `OUTREACH_SEQUENCES_LIMIT`), so a clause or a search word answers over
170
+ * all of them, and the footer counts the matches.
171
+ */ const searchKey = gridFilter.searchWords.join(' ');
172
+ const matching = useMemo(()=>filterListRows(sequences.data.map((sequence)=>({
173
+ sequence,
174
+ name: sequence.name || 'Untitled',
175
+ mailbox: mailboxLabel(sequence),
176
+ status: sequence.status
177
+ })), SEQUENCE_FILTER_FIELDS, gridFilter.clauses, {
178
+ paths: SEQUENCE_SEARCH_FIELDS,
179
+ words: gridFilter.searchWords
180
+ }).map((row)=>row.sequence), // `searchKey` stands for the words, which are a new array each render.
181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
182
+ [
183
+ sequences.data,
184
+ props.mailboxes,
185
+ gridFilter.clauses,
186
+ searchKey
187
+ ]);
188
+ // A narrowed list starts again at its first page.
189
+ useEffect(()=>setPage(0), [
190
+ gridFilter.clauses,
191
+ searchKey
192
+ ]);
193
+ const pageRows = matching.slice(page * pageSize, (page + 1) * pageSize);
131
194
  // Counted for the page on screen only: five aggregations a sequence.
132
195
  const counts = useOutreachSequenceCounts(orgId, pageRows.map((sequence)=>sequence.id));
133
196
  const footer = /*#__PURE__*/ _jsx(ListPagination, {
134
197
  page: page,
135
198
  pageSize: pageSize,
136
199
  rowCount: pageRows.length,
137
- count: sequences.data.length,
200
+ count: matching.length,
138
201
  onPageChange: setPage,
139
202
  onPageSizeChange: setPageSize
140
203
  });
@@ -152,11 +215,14 @@ const COUNT_COLUMNS = [
152
215
  },
153
216
  children: "New sequence"
154
217
  });
155
- const mailboxLabel = (sequence)=>{
156
- const mailbox = props.mailboxes.find((entry)=>entry.id === sequence.mailboxId);
157
- return mailbox ? mailbox.sendAs || mailbox.email : sequence.mailboxId ? 'Mailbox removed' : 'No mailbox yet';
158
- };
159
218
  const count = (sequence, key)=>counts[sequence.id] ? String(counts[sequence.id][key]) : '—';
219
+ const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
220
+ fields: SEQUENCE_FILTER_FIELDS,
221
+ headers: SEQUENCE_FILTER_HEADERS,
222
+ clauses: gridFilter.clauses,
223
+ onChange: gridFilter.setClauses,
224
+ options: SEQUENCE_FILTER_OPTIONS
225
+ });
160
226
  let body;
161
227
  if (sequences.status === 'loading') body = /*#__PURE__*/ _jsx(OutreachLoading, {
162
228
  label: "Loading sequences…"
@@ -179,6 +245,7 @@ const COUNT_COLUMNS = [
179
245
  body = /*#__PURE__*/ _jsxs(Stack, {
180
246
  spacing: 1,
181
247
  children: [
248
+ chips,
182
249
  /*#__PURE__*/ _jsx(Stack, {
183
250
  spacing: 1,
184
251
  component: "ul",
@@ -239,7 +306,7 @@ const COUNT_COLUMNS = [
239
306
  ]
240
307
  });
241
308
  } else {
242
- const columns = [
309
+ const columns = listFilterGridColumns([
243
310
  {
244
311
  field: 'name',
245
312
  headerName: 'Name',
@@ -267,10 +334,11 @@ const COUNT_COLUMNS = [
267
334
  align: 'right',
268
335
  headerAlign: 'right'
269
336
  }))
270
- ];
337
+ ], SEQUENCE_FILTER_FIELDS, SEQUENCE_FILTER_OPTIONS, SEQUENCE_FILTER_HEADERS);
271
338
  body = /*#__PURE__*/ _jsxs(Stack, {
272
339
  spacing: 1,
273
340
  children: [
341
+ chips,
274
342
  /*#__PURE__*/ _jsx(ListTable, {
275
343
  "aria-label": "Sequences",
276
344
  columns: columns,
@@ -284,7 +352,14 @@ const COUNT_COLUMNS = [
284
352
  count(sequence, key)
285
353
  ])))),
286
354
  onOpen: (id)=>navigate(`${sectionPath}/${id}`),
287
- quickFilter: false,
355
+ /*
356
+ * The panel and the search are the grid's; the list answers them
357
+ * over every sequence it read (AGL-3317).
358
+ */ filterMode: "server",
359
+ filterModel: gridFilter.filterModel,
360
+ onFilterModelChange: gridFilter.onFilterModelChange,
361
+ quickFilter: true,
362
+ noRowsLabel: "No sequences match these filters",
288
363
  hideFooter: true
289
364
  }),
290
365
  footer
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-section.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, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useState } from 'react'\nimport type { OutreachMailbox, OutreachSequence } from '../model/outreach.types'\nimport {\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequences,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const sequences = useOutreachSequences(orgId)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const pageRows = sequences.data.slice(page * pageSize, (page + 1) * pageSize)\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={sequences.data.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailbox.sendAs || mailbox.email\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n let body\n if (sequences.status === 'loading')\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n } else if (!sequences.data.length) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n {footer}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = [\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ]\n body = (\n <Stack spacing={1}>\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n quickFilter={false}\n hideFooter\n />\n {footer}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action:\n sequences.status === 'ready' && sequences.data.length\n ? newButton\n : undefined,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListPagination","ListTable","TABLE_PAGE_SIZE_DEFAULT","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useState","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequences","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","org","sequenceId","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","theme","narrow","breakpoints","down","sequences","page","setPage","pageSize","setPageSize","pageRows","data","slice","counts","map","footer","rowCount","length","count","onPageChange","onPageSizeChange","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","mailboxLabel","mailbox","find","entry","mailboxId","sendAs","email","key","String","body","status","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","name","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","row","align","headerAlign","rows","$id","Object","fromEntries","onOpen","quickFilter","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,cAAc,QAAoC,eAAc;AACzE,SAASC,OAAO,QAAQ,yBAAwB;AAChD,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,OAAOC,yBAAyB,wDAAuD;AACvF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,SAAS,QAEJ,uDAAsD;AAC7D,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SACEC,MAAM,EACNC,IAAI,EACJC,WAAW,EACXC,IAAI,EACJC,KAAK,EACLC,UAAU,EACVC,aAAa,EACbC,QAAQ,QACH,gBAAe;AACtB,SAASC,QAAQ,QAAQ,QAAO;AAEhC,SACEC,YAAY,EACZC,eAAe,EACfC,mBAAmB,EACnBC,0BAA0B,EAC1BC,mBAAmB,QACd,mBAAe;AACtB,SACEC,sBAAsB,QAEjB,uBAAmB;AAC1B,SAASC,sBAAsB,QAAQ,uBAAmB;AAC1D,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAEEC,yBAAyB,EACzBC,oBAAoB,QACf,yBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,8BAA0B;AAC/D,SAASC,6BAA6B,QAAQ,6BAAyB;AAiBvE,uDAAuD,GACvD,OAAO,MAAMC,gCAAgC,MAAK;AAElD;;;;CAIC,GACD,OAAO,SAASC,yBAAyBC,KAAoC;IAC3E,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAEC,OAAO,EAAE,GAAGH;IACxC,MAAMI,MAAMX,eAAeQ;IAC3B,MAAMI,WAAWR,8BAA8BO,KAAKH;IACpD,MAAMK,YAAYV,qBAAqBK;IACvC,MAAMM,WAAWjB;IAEjB,IAAI,CAACW,OAAO,qBAAO,KAACd;QAAgBqB,OAAM;;IAC1C,MAAM,CAACC,OAAOC,OAAO,GAAGP;IACxB,IAAIM,UAAUX,+BAA+B;QAC3C,qBACE,MAACjB;YAAM8B,SAAS;;8BACd,KAACzB;oBAAa0B,MAAMV;8BAAa;;8BACjC,KAACpB;oBAAW+B,SAAQ;oBAAKC,WAAU;8BAAK;;8BAGxC,KAACtB;oBACCS,OAAOA;oBACPc,UAAUf,MAAMe,QAAQ;oBACxBC,UAAU;oBACVX,UAAUA;oBACVC,WAAWA;oBACXW,eAAejB,MAAMiB,aAAa;oBAClCC,SAAS,CAACF,WAAaT,SAAS,GAAGL,YAAY,CAAC,EAAEc,SAASG,EAAE,EAAE;oBAC/DC,UAAU,IAAMb,SAASL;;;;IAIjC;IACA,IAAIO,OAAO;QACT,qBACE,KAAClB;YACCU,OAAOA;YACPc,UAAUf,MAAMe,QAAQ;YACxBM,KAAKrB,MAAMqB,GAAG;YACdnB,aAAaA;YACboB,YAAYb;YACZc,KACGb,WAAW,gBACR,gBACA;YAENL,UAAUA;YACVC,WAAWA;YACXW,eAAejB,MAAMiB,aAAa;YAClCO,gBAAgBxB,MAAMwB,cAAc;;IAG1C;IACA,qBACE,KAACC;QACCxB,OAAOA;QACPC,aAAaA;QACbI,WAAWA,UAAUA,SAAS;;AAGpC;AACAP,yBAAyB2B,WAAW,GAAG;AAEvC,MAAMC,gBAAuE;IAC3E;QAAC;QAAY;KAAW;IACxB;QAAC;QAAU;KAAS;IACpB;QAAC;QAAW;KAAU;IACtB;QAAC;QAAW;KAAU;IACtB;QAAC;QAAY;KAAY;CAC1B;AAED;;;CAGC,GACD,OAAO,SAASF,qBAAqBzB,KAIpC;IACC,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAE,GAAGF;IAC/B,MAAM4B,QAAQ5C;IACd,MAAM6C,SAAS9C,cAAc6C,MAAME,WAAW,CAACC,IAAI,CAAC;IACpD,MAAMxB,WAAWjB;IACjB,MAAM0C,YAAYrC,qBAAqBM;IACvC,MAAM,CAACgC,MAAMC,QAAQ,GAAGjD,SAAS;IACjC,MAAM,CAACkD,UAAUC,YAAY,GAAGnD,SAAST;IACzC,MAAM6D,WAAWL,UAAUM,IAAI,CAACC,KAAK,CAACN,OAAOE,UAAU,AAACF,CAAAA,OAAO,CAAA,IAAKE;IACpE,qEAAqE;IACrE,MAAMK,SAAS9C,0BACbO,OACAoC,SAASI,GAAG,CAAC,CAACzB,WAAaA,SAASG,EAAE;IAExC,MAAMuB,uBACJ,KAACpE;QACC2D,MAAMA;QACNE,UAAUA;QACVQ,UAAUN,SAASO,MAAM;QACzBC,OAAOb,UAAUM,IAAI,CAACM,MAAM;QAC5BE,cAAcZ;QACda,kBAAkBX;;IAGtB,MAAMY,UAAU,GAAG9C,YAAY,CAAC,EAAEJ,+BAA+B;IACjE,MAAMmD,0BACJ,KAACxE;QACCoC,SAAQ;QACRqC,yBAAW,KAAC9E;YAAQ+E,MAAMjF,QAAQiF,IAAI;YAAEC,UAAS;;QACjDxC,MAAMoC;QACNK,SAAS,CAACC;YACRA,MAAMC,cAAc;YACpBhD,SAASyC;QACX;kBACD;;IAIH,MAAMQ,eAAe,CAACxC;QACpB,MAAMyC,UAAUzD,MAAMM,SAAS,CAACoD,IAAI,CAClC,CAACC,QAAUA,MAAMxC,EAAE,KAAKH,SAAS4C,SAAS;QAE5C,OAAOH,UACHA,QAAQI,MAAM,IAAIJ,QAAQK,KAAK,GAC/B9C,SAAS4C,SAAS,GAChB,oBACA;IACR;IACA,MAAMf,QAAQ,CACZ7B,UACA+C,MACIvB,MAAM,CAACxB,SAASG,EAAE,CAAC,GAAG6C,OAAOxB,MAAM,CAACxB,SAASG,EAAE,CAAC,CAAC4C,IAAI,IAAI;IAE/D,IAAIE;IACJ,IAAIjC,UAAUkC,MAAM,KAAK,WACvBD,qBAAO,KAAC9E;QAAgBqB,OAAM;;SAC3B,IAAIwB,UAAUkC,MAAM,KAAK,WAAWlC,UAAUkC,MAAM,KAAK,WAAW;QACvED,qBAAO,KAAC7E;YAAoB8E,QAAQlC,UAAUkC,MAAM;YAAEC,MAAK;;IAC7D,OAAO,IAAI,CAACnC,UAAUM,IAAI,CAACM,MAAM,EAAE;QACjCqB,qBACE,KAACvF;YAAKmC,SAAQ;sBACZ,cAAA,KAACxC;gBACCmC,OAAM;gBACN4D,aAAY;gBACZC,QAAQpB;;;IAIhB,OAAO,IAAIpB,QAAQ;QACjBoC,qBACE,MAACpF;YAAM8B,SAAS;;8BACd,KAAC9B;oBACC8B,SAAS;oBACTG,WAAU;oBACVwD,IAAI;wBAAEC,WAAW;wBAAQC,GAAG;wBAAGC,GAAG;oBAAE;oBACpCC,cAAW;8BAEVrC,SAASI,GAAG,CAAC,CAACzB,yBACb,KAACtC;4BAAuBmC,SAAQ;4BAAWC,WAAU;sCACnD,cAAA,KAACnC;0CACC,cAAA,MAACE;oCAAM8B,SAAS;;sDACd,MAAC9B;4CACC8F,WAAU;4CACVhE,SAAS;4CACT2D,IAAI;gDACFM,YAAY;gDACZC,gBAAgB;4CAClB;;8DAEA,KAAC3F;oDAAa0B,MAAM,GAAGV,YAAY,CAAC,EAAEc,SAASG,EAAE,EAAE;8DAChDH,SAAS8D,IAAI,IAAI;;8DAEpB,KAACzF;oDAA2B6E,QAAQlD,SAASkD,MAAM;;;;sDAErD,KAACpF;4CAAW+B,SAAQ;4CAAQkE,OAAM;sDAC/BvB,aAAaxC;;sDAEhB,KAACnC;4CACC8F,WAAU;4CACVhE,SAAS;4CACT2D,IAAI;gDAAEU,UAAU;gDAAQC,QAAQ;4CAAI;sDAEnCtD,cAAcc,GAAG,CAAC,CAAC,CAACsB,KAAKvD,MAAM,iBAC9B,KAAC5B;oDAECsG,MAAK;oDACLrE,SAAQ;oDACRL,OAAO,GAAGA,MAAM,CAAC,EAAEqC,MAAM7B,UAAU+C,MAAM;mDAHpCA;;;;;2BA1BN/C,SAASG,EAAE;;gBAsCzBuB;;;IAGP,OAAO;QACL,MAAMyC,UAAkD;YACtD;gBAAEC,OAAO;gBAAQC,YAAY;gBAAQC,MAAM;gBAAGC,UAAU;YAAI;YAC5D;gBAAEH,OAAO;gBAAWC,YAAY;gBAAWC,MAAM;gBAAGC,UAAU;YAAI;YAClE;gBACEH,OAAO;gBACPC,YAAY;gBACZG,OAAO;gBACPC,YAAY,CAAC,EAAEC,GAAG,EAAE,iBAClB,KAACrG;wBAA2B6E,QAAQwB,IAAIxB,MAAM;;YAElD;eACGvC,cAAcc,GAAG,CAAC,CAAC,CAACsB,KAAKvD,MAAM,GAAM,CAAA;oBACtC4E,OAAOrB;oBACPsB,YAAY7E;oBACZgF,OAAO;oBACPG,OAAO;oBACPC,aAAa;gBACf,CAAA;SACD;QACD3B,qBACE,MAACpF;YAAM8B,SAAS;;8BACd,KAACpC;oBACCmG,cAAW;oBACXS,SAASA;oBACTU,MAAMxD,SAASI,GAAG,CAAC,CAACzB,WAAc;4BAChC8E,KAAK9E,SAASG,EAAE;4BAChB2D,MAAM9D,SAAS8D,IAAI,IAAI;4BACvBrB,SAASD,aAAaxC;4BACtBkD,QAAQlD,SAASkD,MAAM;2BACpB6B,OAAOC,WAAW,CACnBrE,cAAcc,GAAG,CAAC,CAAC,CAACsB,IAAI,GAAK;gCAACA;gCAAKlB,MAAM7B,UAAU+C;6BAAK;oBAG5DkC,QAAQ,CAAC9E,KAAOZ,SAAS,GAAGL,YAAY,CAAC,EAAEiB,IAAI;oBAC/C+E,aAAa;oBACbC,UAAU;;gBAEXzD;;;IAGP;IAEA,qBACE,KAACvE;QACCiI,QAAO;QACPC,MAAMpI,eAAe,aAAa;YAAEqI,QAAQ;QAAa;QACzDC,aAAa;YACXlC,QACErC,UAAUkC,MAAM,KAAK,WAAWlC,UAAUM,IAAI,CAACM,MAAM,GACjDK,YACAuD;QACR;QACAC,cAAc;QACdC,cAAc;kBAEbzC;;AAGP;AACAxC,qBAAqBC,WAAW,GAAG;AAEnC,eAAe3B,yBAAwB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-section.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, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\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 { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n OUTREACH_SEQUENCE_STATUS_LABELS,\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequences,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/*\n * What the sequences grid's Filters panel offers (AGL-3317). The list holds\n * every sequence it read, so each clause and the quick search are answered\n * over that whole set before it is paged, never over the page on screen.\n */\nconst SEQUENCE_FILTER_FIELDS = [\n inMemoryListField('name', 'text'),\n inMemoryListField('mailbox', 'text'),\n inMemoryListField('status', 'select'),\n]\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n}\nconst SEQUENCE_FILTER_OPTIONS = {\n status: OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n })),\n}\n/** What the quick search reads on a sequence row. */\nconst SEQUENCE_SEARCH_FIELDS = ['name', 'mailbox'] as const\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const sequences = useOutreachSequences(orgId)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status'] })\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailbox.sendAs || mailbox.email\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n /*\n * Filtered BEFORE the page is cut: the list read every sequence (under\n * `OUTREACH_SEQUENCES_LIMIT`), so a clause or a search word answers over\n * all of them, and the footer counts the matches.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const matching = useMemo(\n () =>\n filterListRows(\n sequences.data.map((sequence) => ({\n sequence,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n })),\n SEQUENCE_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: SEQUENCE_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.sequence),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [sequences.data, props.mailboxes, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n const pageRows = matching.slice(page * pageSize, (page + 1) * pageSize)\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={matching.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n const chips = (\n <ListFilterChips\n fields={SEQUENCE_FILTER_FIELDS}\n headers={SEQUENCE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SEQUENCE_FILTER_OPTIONS}\n />\n )\n let body\n if (sequences.status === 'loading')\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n } else if (!sequences.data.length) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n {chips}\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n {footer}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = listFilterGridColumns([\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ], SEQUENCE_FILTER_FIELDS, SEQUENCE_FILTER_OPTIONS, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n /*\n * The panel and the search are the grid's; the list answers them\n * over every sequence it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No sequences match these filters\"\n hideFooter\n />\n {footer}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action:\n sequences.status === 'ready' && sequences.data.length\n ? newButton\n : undefined,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListFilterChips","ListPagination","ListTable","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequences","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","org","sequenceId","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","SEQUENCE_FILTER_FIELDS","SEQUENCE_FILTER_HEADERS","name","mailbox","status","SEQUENCE_FILTER_OPTIONS","map","value","SEQUENCE_SEARCH_FIELDS","theme","narrow","breakpoints","down","sequences","page","setPage","pageSize","setPageSize","gridFilter","selectFields","mailboxLabel","find","entry","mailboxId","sendAs","email","searchKey","searchWords","join","matching","data","clauses","paths","words","row","pageRows","slice","counts","footer","rowCount","length","count","onPageChange","onPageSizeChange","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","key","String","chips","fields","headers","onChange","setClauses","options","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","align","headerAlign","rows","$id","Object","fromEntries","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"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"}