@aglyn/plugins-outreach 1.0.0-beta.198 → 1.0.0-beta.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/do-not-contact-domains.js +60 -42
  3. package/src/lib/components/do-not-contact-domains.js.map +1 -1
  4. package/src/lib/components/enrollments-table.d.ts +4 -2
  5. package/src/lib/components/enrollments-table.js +70 -89
  6. package/src/lib/components/enrollments-table.js.map +1 -1
  7. package/src/lib/components/sequence-detail.js +3 -3
  8. package/src/lib/components/sequence-detail.js.map +1 -1
  9. package/src/lib/components/sequences-section.d.ts +9 -0
  10. package/src/lib/components/sequences-section.js +96 -82
  11. package/src/lib/components/sequences-section.js.map +1 -1
  12. package/src/lib/components/use-outreach-data.d.ts +30 -25
  13. package/src/lib/components/use-outreach-data.js +93 -104
  14. package/src/lib/components/use-outreach-data.js.map +1 -1
  15. package/src/lib/enrollment/enrollment-search.d.ts +32 -0
  16. package/src/lib/enrollment/enrollment-search.js +40 -0
  17. package/src/lib/enrollment/enrollment-search.js.map +1 -0
  18. package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
  19. package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
  20. package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
  21. package/src/lib/model/enrollment-list-query.d.ts +22 -0
  22. package/src/lib/model/enrollment-list-query.js +117 -0
  23. package/src/lib/model/enrollment-list-query.js.map +1 -0
  24. package/src/lib/model/outreach.types.d.ts +12 -0
  25. package/src/lib/model/outreach.types.js.map +1 -1
  26. package/src/lib/model/sequence-list-query.d.ts +22 -0
  27. package/src/lib/model/sequence-list-query.js +99 -0
  28. package/src/lib/model/sequence-list-query.js.map +1 -0
  29. package/src/lib/routes/enroll-routes.js +8 -0
  30. package/src/lib/routes/enroll-routes.js.map +1 -1
  31. package/src/lib/routes/sequence-routes.js +6 -1
  32. package/src/lib/routes/sequence-routes.js.map +1 -1
  33. package/src/lib/storage/do-not-contact-store.js +6 -1
  34. package/src/lib/storage/do-not-contact-store.js.map +1 -1
@@ -23,17 +23,19 @@ import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.com
23
23
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
24
24
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
25
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";
27
- import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
26
+ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
27
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
28
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
28
29
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
29
- import { Alert, Button, Card, CardContent, Chip, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
30
- import { useEffect, useMemo, useState } from "react";
30
+ import { Button, Card, CardContent, Chip, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
31
+ import { useMemo } from "react";
31
32
  import { OUTREACH_SEQUENCE_STATUSES } from "../model/outreach.types.js";
33
+ import { OUTREACH_SEQUENCE_LIST_QUERY } from "../model/sequence-list-query.js";
32
34
  import { OUTREACH_SEQUENCE_STATUS_LABELS, OutreachLink, OutreachLoading, OutreachLoadProblem, OutreachSequenceStatusChip, useOutreachNavigate } from "./outreach-ui.js";
33
35
  import { OutreachSequenceDetail } from "./sequence-detail.js";
34
36
  import { OutreachSequenceEditor } from "./sequence-editor.js";
35
37
  import { useOutreachApi } from "./use-outreach-api.js";
36
- import { useOutreachSequenceCounts, useOutreachSequences } from "./use-outreach-data.js";
38
+ import { useOutreachSequenceCounts, useOutreachSequenceList } from "./use-outreach-data.js";
37
39
  import { useOutreachMailboxes } from "./use-outreach-mailboxes.js";
38
40
  import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
39
41
  /** The new-sequence page's segment below the section. */ export const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new';
@@ -121,85 +123,76 @@ const COUNT_COLUMNS = [
121
123
  ]
122
124
  ];
123
125
  /*
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
- ];
126
+ * What the sequences grid's Filters panel offers (AGL-3317), each field on
127
+ * the list's Firestore query (AGL-3321, `OUTREACH_SEQUENCE_LIST_QUERY`).
128
+ */ const SEQUENCE_FILTER_FIELDS = OUTREACH_SEQUENCE_LIST_QUERY.fields;
132
129
  const SEQUENCE_FILTER_HEADERS = {
133
130
  name: 'Name',
134
131
  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
- }))
132
+ status: 'Status',
133
+ created: 'Created'
142
134
  };
143
- /** What the quick search reads on a sequence row. */ const SEQUENCE_SEARCH_FIELDS = [
135
+ /** `created` is no column of its own: a filter-only column, never shown. */ const SEQUENCE_HIDDEN_COLUMNS = hiddenFilterVisibility(SEQUENCE_FILTER_FIELDS, [
144
136
  'name',
145
- 'mailbox'
146
- ];
137
+ 'mailbox',
138
+ 'status'
139
+ ]);
140
+ const SEQUENCE_STATUS_OPTIONS = OUTREACH_SEQUENCE_STATUSES.map((status)=>({
141
+ value: status,
142
+ label: OUTREACH_SEQUENCE_STATUS_LABELS[status]
143
+ }));
144
+ /** How a mailbox reads in the list: the address it sends as. */ const mailboxName = (mailbox)=>mailbox.sendAs || mailbox.email;
147
145
  /**
148
146
  * Every sequence: its mailbox, its status, and how many people it enrolled
149
147
  * and where they stand — a table on wider screens, cards on a phone.
148
+ *
149
+ * ## Asked of Firestore, a page at a time
150
+ *
151
+ * The Filters panel and the search box are the grid's; the clauses and the
152
+ * words go onto ONE query over the organization's sequences, newest first,
153
+ * and each page is a page of that query's matches (AGL-3321). A clause the
154
+ * query cannot hold beside the others is not applied, and the notice above
155
+ * the list says which and why — never answered over the sequences that
156
+ * happen to be loaded.
150
157
  */ export function OutreachSequenceList(props) {
151
158
  const { orgId, sectionPath } = props;
152
159
  const theme = useTheme();
153
160
  const narrow = useMediaQuery(theme.breakpoints.down('md'));
154
161
  const navigate = useOutreachNavigate();
155
- const sequences = useOutreachSequences(orgId);
156
- const [page, setPage] = useState(0);
157
- const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
158
162
  const gridFilter = useListGridFilter({
159
163
  selectFields: [
160
- 'status'
164
+ 'status',
165
+ 'mailbox'
161
166
  ]
162
167
  });
168
+ const sequences = useOutreachSequenceList(orgId, {
169
+ clauses: gridFilter.clauses,
170
+ search: gridFilter.searchWords
171
+ });
172
+ const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
173
+ const filterOptions = useMemo(()=>({
174
+ status: SEQUENCE_STATUS_OPTIONS,
175
+ mailbox: props.mailboxes.map((mailbox)=>({
176
+ value: mailbox.id,
177
+ label: mailboxName(mailbox)
178
+ }))
179
+ }), [
180
+ props.mailboxes
181
+ ]);
163
182
  const mailboxLabel = (sequence)=>{
164
183
  const mailbox = props.mailboxes.find((entry)=>entry.id === sequence.mailboxId);
165
- return mailbox ? mailbox.sendAs || mailbox.email : sequence.mailboxId ? 'Mailbox removed' : 'No mailbox yet';
184
+ return mailbox ? mailboxName(mailbox) : sequence.mailboxId ? 'Mailbox removed' : 'No mailbox yet';
166
185
  };
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);
186
+ const pageRows = sequences.rows;
194
187
  // Counted for the page on screen only: five aggregations a sequence.
195
188
  const counts = useOutreachSequenceCounts(orgId, pageRows.map((sequence)=>sequence.id));
196
189
  const footer = /*#__PURE__*/ _jsx(ListPagination, {
197
- page: page,
198
- pageSize: pageSize,
190
+ page: sequences.page,
191
+ pageSize: sequences.pageSize,
199
192
  rowCount: pageRows.length,
200
- count: matching.length,
201
- onPageChange: setPage,
202
- onPageSizeChange: setPageSize
193
+ hasMore: sequences.hasMore,
194
+ onPageChange: sequences.setPage,
195
+ onPageSizeChange: sequences.setPageSize
203
196
  });
204
197
  const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`;
205
198
  const newButton = /*#__PURE__*/ _jsx(Button, {
@@ -216,30 +209,38 @@ const SEQUENCE_FILTER_OPTIONS = {
216
209
  children: "New sequence"
217
210
  });
218
211
  const count = (sequence, key)=>counts[sequence.id] ? String(counts[sequence.id][key]) : '—';
219
- /*
220
- * The read stops at `OUTREACH_SEQUENCES_LIMIT`, newest first, and probes one
221
- * past it, so this shows only when older sequences really exist.
222
- */ const truncatedNotice = sequences.truncated ? /*#__PURE__*/ _jsx(Alert, {
223
- severity: "info",
224
- children: `Showing the ${sequences.data.length.toLocaleString()} newest sequences. ` + 'The filters and the search narrow these only; older sequences are ' + 'not listed here.'
225
- }) : null;
226
212
  const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
227
213
  fields: SEQUENCE_FILTER_FIELDS,
228
214
  headers: SEQUENCE_FILTER_HEADERS,
229
215
  clauses: gridFilter.clauses,
230
216
  onChange: gridFilter.setClauses,
231
- options: SEQUENCE_FILTER_OPTIONS
217
+ options: filterOptions
218
+ });
219
+ const notices = /*#__PURE__*/ _jsx(ListQueryNotices, {
220
+ refused: listQueryRefusals(sequences.plan.refused, {
221
+ fields: SEQUENCE_FILTER_FIELDS,
222
+ headers: SEQUENCE_FILTER_HEADERS,
223
+ options: filterOptions
224
+ }),
225
+ notices: sequences.plan.notices
232
226
  });
227
+ /** Nothing stored at all — not a filter that matched nothing. */ const none = sequences.status === 'ready' && !pageRows.length && !filtering && sequences.page === 0;
233
228
  let body;
234
- if (sequences.status === 'loading') body = /*#__PURE__*/ _jsx(OutreachLoading, {
229
+ if (sequences.status === 'loading' && !filtering) body = /*#__PURE__*/ _jsx(OutreachLoading, {
235
230
  label: "Loading sequences…"
236
231
  });
237
232
  else if (sequences.status === 'error' || sequences.status === 'refused') {
238
- body = /*#__PURE__*/ _jsx(OutreachLoadProblem, {
239
- status: sequences.status,
240
- what: "sequences"
233
+ body = /*#__PURE__*/ _jsxs(Stack, {
234
+ spacing: 1,
235
+ children: [
236
+ chips,
237
+ /*#__PURE__*/ _jsx(OutreachLoadProblem, {
238
+ status: sequences.status,
239
+ what: "sequences"
240
+ })
241
+ ]
241
242
  });
242
- } else if (!sequences.data.length) {
243
+ } else if (none) {
243
244
  body = /*#__PURE__*/ _jsx(Card, {
244
245
  variant: "outlined",
245
246
  children: /*#__PURE__*/ _jsx(EmptyStateComponent, {
@@ -253,7 +254,8 @@ const SEQUENCE_FILTER_OPTIONS = {
253
254
  spacing: 1,
254
255
  children: [
255
256
  chips,
256
- /*#__PURE__*/ _jsx(Stack, {
257
+ notices,
258
+ pageRows.length ? /*#__PURE__*/ _jsx(Stack, {
257
259
  spacing: 1,
258
260
  component: "ul",
259
261
  sx: {
@@ -308,9 +310,12 @@ const SEQUENCE_FILTER_OPTIONS = {
308
310
  })
309
311
  })
310
312
  }, sequence.id))
313
+ }) : /*#__PURE__*/ _jsx(Typography, {
314
+ variant: "body2",
315
+ color: "text.secondary",
316
+ children: sequences.status === 'loading' ? 'Loading sequences…' : 'No sequences match these filters'
311
317
  }),
312
- footer,
313
- truncatedNotice
318
+ footer
314
319
  ]
315
320
  });
316
321
  } else {
@@ -342,14 +347,20 @@ const SEQUENCE_FILTER_OPTIONS = {
342
347
  align: 'right',
343
348
  headerAlign: 'right'
344
349
  }))
345
- ], SEQUENCE_FILTER_FIELDS, SEQUENCE_FILTER_OPTIONS, SEQUENCE_FILTER_HEADERS);
350
+ ], SEQUENCE_FILTER_FIELDS, filterOptions, SEQUENCE_FILTER_HEADERS);
346
351
  body = /*#__PURE__*/ _jsxs(Stack, {
347
352
  spacing: 1,
348
353
  children: [
349
354
  chips,
355
+ notices,
350
356
  /*#__PURE__*/ _jsx(ListTable, {
351
357
  "aria-label": "Sequences",
352
358
  columns: columns,
359
+ initialState: {
360
+ columns: {
361
+ columnVisibilityModel: SEQUENCE_HIDDEN_COLUMNS
362
+ }
363
+ },
353
364
  rows: pageRows.map((sequence)=>_extends({
354
365
  $id: sequence.id,
355
366
  name: sequence.name || 'Untitled',
@@ -360,18 +371,21 @@ const SEQUENCE_FILTER_OPTIONS = {
360
371
  count(sequence, key)
361
372
  ])))),
362
373
  onOpen: (id)=>navigate(`${sectionPath}/${id}`),
374
+ loading: sequences.status === 'loading',
363
375
  /*
364
- * The panel and the search are the grid's; the list answers them
365
- * over every sequence it read (AGL-3317).
376
+ * The panel and the search are the grid's; every clause and the
377
+ * search word are on the list's query (AGL-3321), and the grid
378
+ * neither filters nor sorts the page it is handed: the query's
379
+ * one order, newest first, is the list's.
366
380
  */ filterMode: "server",
367
381
  filterModel: gridFilter.filterModel,
368
382
  onFilterModelChange: gridFilter.onFilterModelChange,
369
383
  quickFilter: true,
384
+ disableColumnSorting: true,
370
385
  noRowsLabel: "No sequences match these filters",
371
386
  hideFooter: true
372
387
  }),
373
- footer,
374
- truncatedNotice
388
+ footer
375
389
  ]
376
390
  });
377
391
  }
@@ -381,7 +395,7 @@ const SEQUENCE_FILTER_OPTIONS = {
381
395
  anchor: '#sequences'
382
396
  }),
383
397
  HeaderProps: {
384
- action: sequences.status === 'ready' && sequences.data.length ? newButton : undefined
398
+ action: none || sequences.status === 'loading' ? undefined : newButton
385
399
  },
386
400
  contentGutterX: true,
387
401
  contentGutterY: true,
@@ -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 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 Alert,\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 /*\n * The read stops at `OUTREACH_SEQUENCES_LIMIT`, newest first, and probes one\n * past it, so this shows only when older sequences really exist.\n */\n const truncatedNotice = sequences.truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${sequences.data.length.toLocaleString()} newest sequences. ` +\n 'The filters and the search narrow these only; older sequences are ' +\n 'not listed here.'}\n </Alert>\n ) : null\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 {truncatedNotice}\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 {truncatedNotice}\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","Alert","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","truncatedNotice","truncated","severity","toLocaleString","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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"}
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 ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\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 { useMemo } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { OUTREACH_SEQUENCE_LIST_QUERY } from '../model/sequence-list-query'\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 useOutreachSequenceList,\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), each field on\n * the list's Firestore query (AGL-3321, `OUTREACH_SEQUENCE_LIST_QUERY`).\n */\nconst SEQUENCE_FILTER_FIELDS = OUTREACH_SEQUENCE_LIST_QUERY.fields\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n created: 'Created',\n}\n/** `created` is no column of its own: a filter-only column, never shown. */\nconst SEQUENCE_HIDDEN_COLUMNS = hiddenFilterVisibility(SEQUENCE_FILTER_FIELDS, [\n 'name',\n 'mailbox',\n 'status',\n])\nconst SEQUENCE_STATUS_OPTIONS = OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n}))\n\n/** How a mailbox reads in the list: the address it sends as. */\nconst mailboxName = (mailbox: OutreachMailbox) => mailbox.sendAs || mailbox.email\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 *\n * ## Asked of Firestore, a page at a time\n *\n * The Filters panel and the search box are the grid's; the clauses and the\n * words go onto ONE query over the organization's sequences, newest first,\n * and each page is a page of that query's matches (AGL-3321). A clause the\n * query cannot hold beside the others is not applied, and the notice above\n * the list says which and why — never answered over the sequences that\n * happen to be loaded.\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 gridFilter = useListGridFilter({ selectFields: ['status', 'mailbox'] })\n const sequences = useOutreachSequenceList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n const filterOptions = useMemo(\n () => ({\n status: SEQUENCE_STATUS_OPTIONS,\n mailbox: props.mailboxes.map((mailbox) => ({\n value: mailbox.id,\n label: mailboxName(mailbox),\n })),\n }),\n [props.mailboxes],\n )\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailboxName(mailbox)\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n const pageRows = sequences.rows\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={sequences.page}\n pageSize={sequences.pageSize}\n rowCount={pageRows.length}\n hasMore={sequences.hasMore}\n onPageChange={sequences.setPage}\n onPageSizeChange={sequences.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={filterOptions}\n />\n )\n const notices = (\n <ListQueryNotices\n refused={listQueryRefusals(sequences.plan.refused, {\n fields: SEQUENCE_FILTER_FIELDS,\n headers: SEQUENCE_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={sequences.plan.notices}\n />\n )\n /** Nothing stored at all — not a filter that matched nothing. */\n const none =\n sequences.status === 'ready' &&\n !pageRows.length &&\n !filtering &&\n sequences.page === 0\n let body\n if (sequences.status === 'loading' && !filtering)\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = (\n <Stack spacing={1}>\n {chips}\n <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n </Stack>\n )\n } else if (none) {\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 {notices}\n {pageRows.length ? (\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 ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {sequences.status === 'loading'\n ? 'Loading sequences…'\n : 'No sequences match these filters'}\n </Typography>\n )}\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, filterOptions, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n {notices}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n initialState={{ columns: { columnVisibilityModel: SEQUENCE_HIDDEN_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 loading={sequences.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and the\n * search word are on the list's query (AGL-3321), and the grid\n * neither filters nor sorts the page it is handed: the query's\n * one order, newest first, is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\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: none || sequences.status === 'loading' ? undefined : newButton,\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","ListQueryNotices","listQueryRefusals","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useMemo","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_LIST_QUERY","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequenceList","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","fields","SEQUENCE_FILTER_HEADERS","name","mailbox","status","created","SEQUENCE_HIDDEN_COLUMNS","SEQUENCE_STATUS_OPTIONS","map","value","mailboxName","sendAs","email","theme","narrow","breakpoints","down","gridFilter","selectFields","sequences","clauses","search","searchWords","filtering","length","some","word","trim","filterOptions","mailboxLabel","find","entry","mailboxId","pageRows","rows","counts","footer","page","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","count","key","String","chips","headers","onChange","setClauses","options","notices","refused","plan","none","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","row","align","headerAlign","initialState","columnVisibilityModel","$id","Object","fromEntries","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model';
2
+ import type { ListQueryPlan, ListQueryRequest } from '@aglyn/shared-ui-jsx/const/list-query-plan';
2
3
  import { type OutreachDoNotContactDomainEntry, type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
3
4
  /**
4
5
  * The Outreach documents the console reads LIVE (AGL-2980): sequences,
@@ -18,15 +19,27 @@ export interface OutreachLoad<T> {
18
19
  status: OutreachLoadStatus;
19
20
  data: T;
20
21
  }
21
- /** The most sequences the list reads. */
22
- export declare const OUTREACH_SEQUENCES_LIMIT = 200;
23
- /** The sequences read, and whether the organization holds older ones. */
24
- export interface OutreachSequencesLoad extends OutreachLoad<OutreachSequence[]> {
25
- /** True when there are sequences older than the `OUTREACH_SEQUENCES_LIMIT` read. */
26
- truncated?: boolean;
22
+ /**
23
+ * One page of a list whose every filter and search word is on its Firestore
24
+ * query (AGL-3321): the rows of the page on screen, whether another page
25
+ * exists, the pager, and the plan — what the query holds and what it could
26
+ * not take, which the list says above itself.
27
+ */
28
+ export interface OutreachListPage<T> {
29
+ status: OutreachLoadStatus;
30
+ rows: T[];
31
+ hasMore: boolean;
32
+ page: number;
33
+ setPage(page: number): void;
34
+ pageSize: number;
35
+ setPageSize(pageSize: number): void;
36
+ plan: ListQueryPlan;
27
37
  }
28
- /** The organization's sequences, newest first. */
29
- export declare function useOutreachSequences(orgId: string | null): OutreachSequencesLoad;
38
+ /**
39
+ * The organization's sequences, newest first, one page at a time — each
40
+ * clause and the search on the query (`OUTREACH_SEQUENCE_LIST_QUERY`).
41
+ */
42
+ export declare function useOutreachSequenceList(orgId: string | null, request: ListQueryRequest): OutreachListPage<OutreachSequence>;
30
43
  /** One sequence, or `null` once it is known not to exist. */
31
44
  export declare function useOutreachSequence(orgId: string | null, sequenceId: string | null): OutreachLoad<OutreachSequence | null>;
32
45
  /**
@@ -41,26 +54,18 @@ export type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>;
41
54
  export declare function useOutreachSequenceLinks(orgId: string | null, sequenceId: string | null): OutreachSequenceLinksLoad;
42
55
  /**
43
56
  * The domains on the organization's do-not-contact list (AGL-3244),
44
- * alphabetically, live: an add or a remove through the route shows up here
45
- * without a reload, and so does a domain the sending runtime files after a
46
- * gateway block.
57
+ * alphabetically — the document id is the domain — one page at a time, live:
58
+ * an add or a remove through the route shows up without a reload, and so
59
+ * does a domain the sending runtime files after a gateway block. Each clause
60
+ * and the search are on the query (`OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`).
47
61
  */
48
- export declare function useOutreachDoNotContactDomains(orgId: string | null): OutreachLoad<OutreachDoNotContactDomainEntry[]>;
49
- /** How many enrollments one page of the table reads; "Show more" reads another. */
50
- export declare const OUTREACH_ENROLLMENTS_PAGE = 200;
51
- export interface OutreachEnrollmentsLoad extends OutreachLoad<OutreachEnrollment[]> {
52
- /** Whether a further page may exist. */
53
- hasMore: boolean;
54
- showMore(): void;
55
- }
62
+ export declare function useOutreachDoNotContactDomainList(orgId: string | null, request: ListQueryRequest): OutreachListPage<OutreachDoNotContactDomainEntry>;
56
63
  /**
57
- * A sequence's enrollments, the most recently enrolled first.
58
- *
59
- * Paged by document id — an equality on `sequenceId` in the id's own order
60
- * needs no composite index — and sorted by enrollment time once read, so a
61
- * table of up to a page reads in the order people were enrolled.
64
+ * A sequence's enrollments, the most recently enrolled first, one page at a
65
+ * time — the sequence is the query's base (`sequenceId ==`), and each clause
66
+ * and the search are on it beside that (`OUTREACH_ENROLLMENT_LIST_QUERY`).
62
67
  */
63
- export declare function useOutreachEnrollments(orgId: string | null, sequenceId: string | null): OutreachEnrollmentsLoad;
68
+ export declare function useOutreachEnrollmentList(orgId: string | null, sequenceId: string | null, request: Omit<ListQueryRequest, 'base'>): OutreachListPage<OutreachEnrollment>;
64
69
  /** The counts the sequence list shows beside each sequence. */
65
70
  export interface OutreachSequenceCounts {
66
71
  enrolled: number;