@aglyn/plugins-outreach 1.0.0-beta.199 → 1.0.0-beta.201

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 (108) 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/enrollment-actions.d.ts +33 -0
  5. package/src/lib/components/enrollment-actions.js +174 -0
  6. package/src/lib/components/enrollment-actions.js.map +1 -0
  7. package/src/lib/components/enrollment-detail.d.ts +21 -0
  8. package/src/lib/components/enrollment-detail.js +870 -0
  9. package/src/lib/components/enrollment-detail.js.map +1 -0
  10. package/src/lib/components/enrollment-filter-params.d.ts +58 -0
  11. package/src/lib/components/enrollment-filter-params.js +98 -0
  12. package/src/lib/components/enrollment-filter-params.js.map +1 -0
  13. package/src/lib/components/enrollments-table.d.ts +25 -2
  14. package/src/lib/components/enrollments-table.js +303 -283
  15. package/src/lib/components/enrollments-table.js.map +1 -1
  16. package/src/lib/components/outreach-console-page.js +10 -0
  17. package/src/lib/components/outreach-console-page.js.map +1 -1
  18. package/src/lib/components/sequence-detail.d.ts +2 -0
  19. package/src/lib/components/sequence-detail.js +73 -11
  20. package/src/lib/components/sequence-detail.js.map +1 -1
  21. package/src/lib/components/sequence-report-card.d.ts +17 -0
  22. package/src/lib/components/sequence-report-card.js +63 -15
  23. package/src/lib/components/sequence-report-card.js.map +1 -1
  24. package/src/lib/components/sequences-section.d.ts +13 -1
  25. package/src/lib/components/sequences-section.js +108 -83
  26. package/src/lib/components/sequences-section.js.map +1 -1
  27. package/src/lib/components/use-outreach-api.d.ts +3 -1
  28. package/src/lib/components/use-outreach-api.js +6 -0
  29. package/src/lib/components/use-outreach-api.js.map +1 -1
  30. package/src/lib/components/use-outreach-data.d.ts +30 -25
  31. package/src/lib/components/use-outreach-data.js +93 -104
  32. package/src/lib/components/use-outreach-data.js.map +1 -1
  33. package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
  34. package/src/lib/components/use-outreach-enrollment.js +133 -0
  35. package/src/lib/components/use-outreach-enrollment.js.map +1 -0
  36. package/src/lib/constants/api-routes.d.ts +2 -0
  37. package/src/lib/constants/api-routes.js +1 -0
  38. package/src/lib/constants/api-routes.js.map +1 -1
  39. package/src/lib/engine/gates.d.ts +4 -3
  40. package/src/lib/engine/gates.js.map +1 -1
  41. package/src/lib/engine/gateway-block.d.ts +4 -8
  42. package/src/lib/engine/gateway-block.js +6 -26
  43. package/src/lib/engine/gateway-block.js.map +1 -1
  44. package/src/lib/engine/mail-gateway.d.ts +18 -73
  45. package/src/lib/engine/mail-gateway.js +32 -206
  46. package/src/lib/engine/mail-gateway.js.map +1 -1
  47. package/src/lib/enrollment/enrollment-search.d.ts +32 -0
  48. package/src/lib/enrollment/enrollment-search.js +40 -0
  49. package/src/lib/enrollment/enrollment-search.js.map +1 -0
  50. package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
  51. package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
  52. package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
  53. package/src/lib/model/enrollment-engagement.d.ts +26 -2
  54. package/src/lib/model/enrollment-engagement.js +40 -3
  55. package/src/lib/model/enrollment-engagement.js.map +1 -1
  56. package/src/lib/model/enrollment-history.d.ts +40 -0
  57. package/src/lib/model/enrollment-history.js +95 -0
  58. package/src/lib/model/enrollment-history.js.map +1 -0
  59. package/src/lib/model/enrollment-list-query.d.ts +28 -0
  60. package/src/lib/model/enrollment-list-query.js +151 -0
  61. package/src/lib/model/enrollment-list-query.js.map +1 -0
  62. package/src/lib/model/enrollment-timeline.d.ts +78 -0
  63. package/src/lib/model/enrollment-timeline.js +430 -0
  64. package/src/lib/model/enrollment-timeline.js.map +1 -0
  65. package/src/lib/model/outreach-api.d.ts +24 -0
  66. package/src/lib/model/outreach-api.js.map +1 -1
  67. package/src/lib/model/outreach.types.d.ts +90 -29
  68. package/src/lib/model/outreach.types.js +33 -8
  69. package/src/lib/model/outreach.types.js.map +1 -1
  70. package/src/lib/model/sequence-list-query.d.ts +22 -0
  71. package/src/lib/model/sequence-list-query.js +99 -0
  72. package/src/lib/model/sequence-list-query.js.map +1 -0
  73. package/src/lib/routes/enroll-routes.js +34 -3
  74. package/src/lib/routes/enroll-routes.js.map +1 -1
  75. package/src/lib/routes/enrollment-routes.d.ts +10 -0
  76. package/src/lib/routes/enrollment-routes.js +82 -1
  77. package/src/lib/routes/enrollment-routes.js.map +1 -1
  78. package/src/lib/routes/register-routes.js +14 -4
  79. package/src/lib/routes/register-routes.js.map +1 -1
  80. package/src/lib/routes/route-deps.d.ts +3 -1
  81. package/src/lib/routes/route-deps.js.map +1 -1
  82. package/src/lib/routes/sequence-routes.js +6 -1
  83. package/src/lib/routes/sequence-routes.js.map +1 -1
  84. package/src/lib/runtime/click-events.d.ts +10 -5
  85. package/src/lib/runtime/click-events.js +45 -6
  86. package/src/lib/runtime/click-events.js.map +1 -1
  87. package/src/lib/runtime/gateway-ledger.d.ts +6 -0
  88. package/src/lib/runtime/gateway-ledger.js +9 -3
  89. package/src/lib/runtime/gateway-ledger.js.map +1 -1
  90. package/src/lib/runtime/host-suspension.d.ts +30 -0
  91. package/src/lib/runtime/host-suspension.js +35 -0
  92. package/src/lib/runtime/host-suspension.js.map +1 -0
  93. package/src/lib/runtime/person-erasure.d.ts +4 -1
  94. package/src/lib/runtime/person-erasure.js +14 -3
  95. package/src/lib/runtime/person-erasure.js.map +1 -1
  96. package/src/lib/runtime/platform-runtime-deps.js +34 -3
  97. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  98. package/src/lib/runtime/runtime-deps.d.ts +27 -1
  99. package/src/lib/runtime/runtime-deps.js.map +1 -1
  100. package/src/lib/runtime/send-job.js +46 -6
  101. package/src/lib/runtime/send-job.js.map +1 -1
  102. package/src/lib/runtime/sync-job.js +4 -0
  103. package/src/lib/runtime/sync-job.js.map +1 -1
  104. package/src/lib/storage/do-not-contact-store.js +6 -1
  105. package/src/lib/storage/do-not-contact-store.js.map +1 -1
  106. package/src/lib/storage/domain-intel-store.d.ts +61 -43
  107. package/src/lib/storage/domain-intel-store.js +138 -226
  108. package/src/lib/storage/domain-intel-store.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.199",
3
+ "version": "1.0.0-beta.201",
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.199",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.199",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.199",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.199",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.199",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.199",
34
- "@aglyn/shared-util-email": "1.0.0-beta.199",
35
- "@aglyn/shared-util-http": "1.0.0-beta.199",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.199",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.199",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.199",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.199",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.199",
28
+ "@aglyn/aglyn": "1.0.0-beta.201",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.201",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.201",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.201",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.201",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.201",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.201",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.201",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.201",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.201",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.201",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.201",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -19,17 +19,20 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
19
19
  import { mdiTrashCanOutline } from "@aglyn/shared-data-mdi";
20
20
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
21
21
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
+ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
22
24
  import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
- import { filterListRows, inMemoryListField, listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
24
26
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
25
27
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
28
  import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
27
- import { useMemo, useState } from "react";
29
+ import { useState } from "react";
28
30
  import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
31
+ import { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from "../model/do-not-contact-domain-list-query.js";
29
32
  import { OUTREACH_DO_NOT_CONTACT_REASONS } from "../model/outreach.types.js";
30
33
  import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
31
34
  import { useOutreachApi } from "./use-outreach-api.js";
32
- import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
35
+ import { useOutreachDoNotContactDomainList } from "./use-outreach-data.js";
33
36
  /** Why a domain is on the list, as the card says it. */ export const OUTREACH_DO_NOT_CONTACT_REASON_LABELS = {
34
37
  manual: 'Added by a member',
35
38
  opt_out_reply: 'A reply asked not to be emailed',
@@ -44,17 +47,15 @@ import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
44
47
  };
45
48
  /** What the field says under a value that is not a domain. */ export const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.';
46
49
  /*
47
- * What the domains grid's Filters panel offers (AGL-3317). 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
- ];
50
+ * What the domains grid's Filters panel offers (AGL-3317), each field on the
51
+ * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):
52
+ * the whole domain, why it is listed, and when it was added. The search box
53
+ * reads the domain, each of its labels and the words of its detail.
54
+ */ const DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields;
54
55
  const DOMAIN_FILTER_HEADERS = {
55
56
  domain: 'Domain',
56
57
  reason: 'Why',
57
- detail: 'Detail'
58
+ addedAtMs: 'Added'
58
59
  };
59
60
  const DOMAIN_FILTER_OPTIONS = {
60
61
  reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason)=>({
@@ -62,10 +63,6 @@ const DOMAIN_FILTER_OPTIONS = {
62
63
  label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason]
63
64
  }))
64
65
  };
65
- /** What the quick search reads on a domain row. */ const DOMAIN_SEARCH_FIELDS = [
66
- 'domain',
67
- 'detail'
68
- ];
69
66
  function addedOn(entry) {
70
67
  if (!entry.addedAtMs) return '';
71
68
  return new Intl.DateTimeFormat('en-US', {
@@ -140,7 +137,6 @@ function addedOn(entry) {
140
137
  */ export function OutreachDoNotContactDomainsCard(props) {
141
138
  const { orgId } = props;
142
139
  const api = useOutreachApi(orgId);
143
- const listed = useOutreachDoNotContactDomains(orgId);
144
140
  const { enqueueSnackbar } = useSnackbar();
145
141
  const [draft, setDraft] = useState('');
146
142
  const [busy, setBusy] = useState(null);
@@ -151,27 +147,27 @@ function addedOn(entry) {
151
147
  'reason'
152
148
  ]
153
149
  });
154
- const 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
- ]);
150
+ const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
151
+ /*
152
+ * One page of the domains, alphabetical, with every clause and the search
153
+ * word on the query (AGL-3321) — never matched over the domains that
154
+ * happen to be loaded.
155
+ */ const listed = useOutreachDoNotContactDomainList(orgId, {
156
+ clauses: gridFilter.clauses,
157
+ search: gridFilter.searchWords
158
+ });
159
+ const rows = listed.rows.map((entry)=>{
160
+ var _entry_addedAtMs, _entry_detail;
161
+ return {
162
+ $id: entry.domain,
163
+ domain: entry.domain,
164
+ reason: entry.reason,
165
+ added: addedOn(entry),
166
+ addedAtMs: (_entry_addedAtMs = entry.addedAtMs) != null ? _entry_addedAtMs : 0,
167
+ detail: (_entry_detail = entry.detail) != null ? _entry_detail : ''
168
+ };
169
+ });
170
+ /** Nothing listed at all — not a filter that matched nothing. */ const none = listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0;
175
171
  const change = async (action, domain)=>{
176
172
  setBusy(domain);
177
173
  try {
@@ -241,12 +237,12 @@ function addedOn(entry) {
241
237
  })
242
238
  ]
243
239
  }),
244
- listed.status === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
240
+ listed.status === 'loading' && !filtering ? /*#__PURE__*/ _jsx(OutreachLoading, {
245
241
  label: "Loading domains…"
246
242
  }) : listed.status === 'error' || listed.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
247
243
  status: listed.status,
248
244
  what: "the do-not-contact domains"
249
- }) : listed.data.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
245
+ }) : none ? /*#__PURE__*/ _jsx(Typography, {
250
246
  variant: "body2",
251
247
  color: "text.secondary",
252
248
  children: "No domains yet."
@@ -260,18 +256,40 @@ function addedOn(entry) {
260
256
  onChange: gridFilter.setClauses,
261
257
  options: DOMAIN_FILTER_OPTIONS
262
258
  }),
259
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
260
+ refused: listQueryRefusals(listed.plan.refused, {
261
+ fields: DOMAIN_FILTER_FIELDS,
262
+ headers: DOMAIN_FILTER_HEADERS,
263
+ options: DOMAIN_FILTER_OPTIONS
264
+ }),
265
+ notices: listed.plan.notices
266
+ }),
263
267
  /*#__PURE__*/ _jsx(ListTable, {
264
268
  "aria-label": "Do not contact domains",
265
269
  columns: domainColumns(busy !== null, (domain)=>void change('remove', domain)),
266
270
  rows: rows,
271
+ loading: listed.status === 'loading',
267
272
  /*
268
- * The panel and the search are the grid's; the card answers
269
- * them over every listed domain (AGL-3317).
273
+ * The panel and the search are the grid's; every clause and
274
+ * the search word are on the list's query (AGL-3321). The grid
275
+ * neither filters nor sorts the page it is handed: the query's
276
+ * order — alphabetical, or newest added while a date applies —
277
+ * is the list's.
270
278
  */ filterMode: "server",
271
279
  filterModel: gridFilter.filterModel,
272
280
  onFilterModelChange: gridFilter.onFilterModelChange,
273
281
  quickFilter: true,
274
- noRowsLabel: "No domains match these filters"
282
+ disableColumnSorting: true,
283
+ noRowsLabel: "No domains match these filters",
284
+ hideFooter: true
285
+ }),
286
+ /*#__PURE__*/ _jsx(ListPagination, {
287
+ page: listed.page,
288
+ pageSize: listed.pageSize,
289
+ rowCount: rows.length,
290
+ hasMore: listed.hasMore,
291
+ onPageChange: listed.setPage,
292
+ onPageSizeChange: listed.setPageSize
275
293
  })
276
294
  ]
277
295
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomains } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n} as const\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317). The card reads\n * every listed domain, so the panel and the search answer over all of them.\n */\nconst DOMAIN_FILTER_FIELDS = [\n inMemoryListField('domain', 'text'),\n inMemoryListField('reason', 'select'),\n inMemoryListField('detail', 'text'),\n]\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n detail: 'Detail',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n/** What the quick search reads on a domain row. */\nconst DOMAIN_SEARCH_FIELDS = ['domain', 'detail'] as const\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const listed = useOutreachDoNotContactDomains(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const searchKey = gridFilter.searchWords.join(' ')\n const rows = useMemo(\n () =>\n filterListRows(\n listed.data.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n })),\n DOMAIN_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: DOMAIN_SEARCH_FIELDS, words: gridFilter.searchWords },\n ),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [listed.data, gridFilter.clauses, searchKey],\n )\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : listed.data.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n /*\n * The panel and the search are the grid's; the card answers\n * them over every listed domain (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No domains match these filters\"\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListTable","listActionsColumn","filterListRows","inMemoryListField","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useMemo","useState","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomains","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","DOMAIN_FILTER_HEADERS","reason","detail","DOMAIN_FILTER_OPTIONS","map","value","label","DOMAIN_SEARCH_FIELDS","addedOn","entry","addedAtMs","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","listed","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","searchKey","searchWords","join","rows","data","$id","clauses","paths","words","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","xs","sm","alignItems","onChange","event","target","onKeyDown","key","preventDefault","helperText","flexGrow","status","what","length","fields","headers","setClauses","options","columns","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomainList } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n} as const\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317), each field on the\n * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):\n * the whole domain, why it is listed, and when it was added. The search box\n * reads the domain, each of its labels and the words of its detail.\n */\nconst DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n addedAtMs: 'Added',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the domains, alphabetical, with every clause and the search\n * word on the query (AGL-3321) — never matched over the domains that\n * happen to be loaded.\n */\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const rows = listed.rows.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n }))\n /** Nothing listed at all — not a filter that matched nothing. */\n const none =\n listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' && !filtering ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : none ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(listed.plan.refused, {\n fields: DOMAIN_FILTER_FIELDS,\n headers: DOMAIN_FILTER_HEADERS,\n options: DOMAIN_FILTER_OPTIONS,\n })}\n notices={listed.plan.notices}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n loading={listed.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and\n * the search word are on the list's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * order — alphabetical, or newest added while a date applies —\n * is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No domains match these filters\"\n hideFooter\n />\n <ListPagination\n page={listed.page}\n pageSize={listed.pageSize}\n rowCount={rows.length}\n hasMore={listed.hasMore}\n onPageChange={listed.setPage}\n onPageSizeChange={listed.setPageSize}\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useState","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomainList","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","fields","DOMAIN_FILTER_HEADERS","reason","addedAtMs","DOMAIN_FILTER_OPTIONS","map","value","label","addedOn","entry","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","listed","search","rows","$id","detail","none","status","page","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","xs","sm","alignItems","onChange","event","target","onKeyDown","key","preventDefault","helperText","flexGrow","what","headers","setClauses","options","refused","plan","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component';
2
+ import { type ReactNode } from 'react';
3
+ import type { OutreachEnrollmentAction } from '../model/outreach-api';
4
+ import type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types';
5
+ import type { OutreachApi } from './use-outreach-api';
6
+ /**
7
+ * WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place
8
+ * for the two places it is offered: a row's menu on the Enrollments tab, and
9
+ * the header of the person's own page (AGL-3332). The same actions, the same
10
+ * words, the same two confirmations — a Stop that asked first in one place
11
+ * and not in the other would be two products.
12
+ */
13
+ /** The four the action route takes, and curating the next step (AGL-3324). */
14
+ export type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate';
15
+ /** What a member may do to an enrollment in the status it is in. */
16
+ export declare function outreachEnrollmentActionsFor(enrollment: OutreachEnrollment, steps: readonly OutreachSequenceStep[]): OutreachEnrollmentRowAction[];
17
+ export declare const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string>;
18
+ /** The two actions that end something for good. */
19
+ export declare const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[];
20
+ export interface OutreachEnrollmentActions {
21
+ /** The enrollment an action is running on, if any. */
22
+ busy: string | null;
23
+ /** Runs one action: straight away, after its confirmation, or in the curate dialog. */
24
+ choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void;
25
+ /** The actions as menu items, the destructive ones marked. */
26
+ menuItems(enrollment: OutreachEnrollment, only?: (action: OutreachEnrollmentRowAction) => boolean): RowActionsMenuItem[];
27
+ /** The confirmation and the curate dialog, rendered once wherever the actions are offered. */
28
+ dialogs: ReactNode;
29
+ }
30
+ export declare function useOutreachEnrollmentActions(input: {
31
+ api: OutreachApi;
32
+ steps: readonly OutreachSequenceStep[];
33
+ }): OutreachEnrollmentActions;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
19
+ import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField } from "@mui/material";
20
+ import { useState } from "react";
21
+ import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-step-dialog.js";
22
+ /** What a member may do to an enrollment in the status it is in. */ export function outreachEnrollmentActionsFor(enrollment, steps) {
23
+ const actions = [];
24
+ // Rewrite the next email for this one person, while one is still to go.
25
+ if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate');
26
+ if (enrollment.status === 'active') actions.push('pause', 'stop');
27
+ if (enrollment.status === 'paused') actions.push('resume', 'stop');
28
+ if (enrollment.status !== 'opted_out') actions.push('do_not_contact');
29
+ return actions;
30
+ }
31
+ export const OUTREACH_ENROLLMENT_ACTION_LABELS = {
32
+ curate: 'Curate next step',
33
+ pause: 'Pause',
34
+ resume: 'Resume',
35
+ stop: 'Stop',
36
+ do_not_contact: 'Mark do-not-contact'
37
+ };
38
+ /** The two actions that end something for good. */ export const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS = [
39
+ 'stop',
40
+ 'do_not_contact'
41
+ ];
42
+ /** What the snackbar says once an action lands. */ const DONE_LABELS = {
43
+ pause: 'Paused.',
44
+ resume: 'Resumed.',
45
+ stop: 'Stopped.'
46
+ };
47
+ /** The two actions that end something for good ask first. */ const CONFIRM = {
48
+ stop: {
49
+ title: 'Stop this enrollment?',
50
+ body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',
51
+ label: 'Stop'
52
+ },
53
+ do_not_contact: {
54
+ title: 'Mark do-not-contact?',
55
+ body: 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' + 'every send, and they are stopped in every sequence they are in.',
56
+ label: 'Mark do-not-contact'
57
+ }
58
+ };
59
+ export function useOutreachEnrollmentActions(input) {
60
+ var _CONFIRM_confirming_action, _CONFIRM_confirming_action1, _CONFIRM_confirming_action2;
61
+ const { api, steps } = input;
62
+ const { enqueueSnackbar } = useSnackbar();
63
+ const [confirming, setConfirming] = useState(null);
64
+ const [detail, setDetail] = useState('');
65
+ const [busy, setBusy] = useState(null);
66
+ const [curating, setCurating] = useState(null);
67
+ const act = async (enrollment, action, why)=>{
68
+ setBusy(enrollment.id);
69
+ try {
70
+ const answer = await api.actOnEnrollment(enrollment.id, action, why);
71
+ const others = answer.stoppedOthers;
72
+ enqueueSnackbar(action === 'do_not_contact' ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.` : answer.changed ? DONE_LABELS[action] : 'Nothing to change.', {
73
+ variant: 'success'
74
+ });
75
+ } catch (error) {
76
+ enqueueSnackbar(error.message, {
77
+ variant: 'error',
78
+ allowDuplicate: true
79
+ });
80
+ } finally{
81
+ setBusy(null);
82
+ }
83
+ };
84
+ const choose = (enrollment, action)=>{
85
+ if (action === 'curate') {
86
+ setCurating(enrollment);
87
+ } else if (CONFIRM[action]) {
88
+ setDetail('');
89
+ setConfirming({
90
+ enrollment,
91
+ action
92
+ });
93
+ } else {
94
+ void act(enrollment, action);
95
+ }
96
+ };
97
+ const menuItems = (enrollment, only)=>outreachEnrollmentActionsFor(enrollment, steps).filter((action)=>!only || only(action)).map((action)=>({
98
+ key: action,
99
+ label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],
100
+ onClick: ()=>choose(enrollment, action),
101
+ destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),
102
+ disabled: busy === enrollment.id
103
+ }));
104
+ const dialogs = /*#__PURE__*/ _jsxs(_Fragment, {
105
+ children: [
106
+ /*#__PURE__*/ _jsx(OutreachCurateStepDialog, {
107
+ enrollment: curating,
108
+ steps: steps,
109
+ api: api,
110
+ onClose: ()=>setCurating(null)
111
+ }),
112
+ /*#__PURE__*/ _jsx(Dialog, {
113
+ open: Boolean(confirming),
114
+ onClose: ()=>setConfirming(null),
115
+ fullWidth: true,
116
+ maxWidth: "xs",
117
+ children: confirming ? /*#__PURE__*/ _jsxs(_Fragment, {
118
+ children: [
119
+ /*#__PURE__*/ _jsx(DialogTitle, {
120
+ children: (_CONFIRM_confirming_action = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action.title
121
+ }),
122
+ /*#__PURE__*/ _jsx(DialogContent, {
123
+ children: /*#__PURE__*/ _jsxs(Stack, {
124
+ spacing: 2,
125
+ children: [
126
+ /*#__PURE__*/ _jsx(DialogContentText, {
127
+ children: (_CONFIRM_confirming_action1 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action1.body
128
+ }),
129
+ /*#__PURE__*/ _jsx(TextField, {
130
+ label: "Why (optional)",
131
+ value: detail,
132
+ onChange: (event)=>setDetail(event.target.value),
133
+ fullWidth: true,
134
+ slotProps: {
135
+ htmlInput: {
136
+ maxLength: 200
137
+ }
138
+ }
139
+ })
140
+ ]
141
+ })
142
+ }),
143
+ /*#__PURE__*/ _jsxs(DialogActions, {
144
+ children: [
145
+ /*#__PURE__*/ _jsx(Button, {
146
+ onClick: ()=>setConfirming(null),
147
+ children: "Cancel"
148
+ }),
149
+ /*#__PURE__*/ _jsx(Button, {
150
+ color: "error",
151
+ variant: "contained",
152
+ onClick: ()=>{
153
+ const { enrollment, action } = confirming;
154
+ setConfirming(null);
155
+ void act(enrollment, action, detail.trim() || undefined);
156
+ },
157
+ children: (_CONFIRM_confirming_action2 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action2.label
158
+ })
159
+ ]
160
+ })
161
+ ]
162
+ }) : null
163
+ })
164
+ ]
165
+ });
166
+ return {
167
+ busy,
168
+ choose,
169
+ menuItems,
170
+ dialogs
171
+ };
172
+ }
173
+
174
+ //# sourceMappingURL=enrollment-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollment-actions.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 type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types'\nimport { OutreachCurateStepDialog, outreachNextEmailStepIndex } from './curate-step-dialog'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place\n * for the two places it is offered: a row's menu on the Enrollments tab, and\n * the header of the person's own page (AGL-3332). The same actions, the same\n * words, the same two confirmations — a Stop that asked first in one place\n * and not in the other would be two products.\n */\n\n/** The four the action route takes, and curating the next step (AGL-3324). */\nexport type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate'\n\n/** What a member may do to an enrollment in the status it is in. */\nexport function outreachEnrollmentActionsFor(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n): OutreachEnrollmentRowAction[] {\n const actions: OutreachEnrollmentRowAction[] = []\n // Rewrite the next email for this one person, while one is still to go.\n if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate')\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\nexport const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string> = {\n curate: 'Curate next step',\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** The two actions that end something for good. */\nexport const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[] = [\n 'stop',\n 'do_not_contact',\n]\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<Exclude<OutreachEnrollmentAction, 'do_not_contact'>, string> = {\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<Record<OutreachEnrollmentAction, { title: string; body: string; label: string }>> = {\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\nexport interface OutreachEnrollmentActions {\n /** The enrollment an action is running on, if any. */\n busy: string | null\n /** Runs one action: straight away, after its confirmation, or in the curate dialog. */\n choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void\n /** The actions as menu items, the destructive ones marked. */\n menuItems(\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[]\n /** The confirmation and the curate dialog, rendered once wherever the actions are offered. */\n dialogs: ReactNode\n}\n\nexport function useOutreachEnrollmentActions(input: {\n api: OutreachApi\n steps: readonly OutreachSequenceStep[]\n}): OutreachEnrollmentActions {\n const { api, steps } = input\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 [curating, setCurating] = useState<OutreachEnrollment | null>(null)\n\n const act = async (enrollment: OutreachEnrollment, action: OutreachEnrollmentAction, why?: string) => {\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, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction) => {\n if (action === 'curate') {\n setCurating(enrollment)\n } else if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n const menuItems = (\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[] =>\n outreachEnrollmentActionsFor(enrollment, steps)\n .filter((action) => !only || only(action))\n .map((action) => ({\n key: action,\n label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),\n disabled: busy === enrollment.id,\n }))\n\n const dialogs = (\n <>\n <OutreachCurateStepDialog enrollment={curating} steps={steps} api={api} onClose={() => setCurating(null)} />\n <Dialog open={Boolean(confirming)} onClose={() => setConfirming(null)} fullWidth maxWidth=\"xs\">\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{CONFIRM[confirming.action]?.body}</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 </>\n )\n\n return { busy, choose, menuItems, dialogs }\n}\n"],"names":["useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","useState","OutreachCurateStepDialog","outreachNextEmailStepIndex","outreachEnrollmentActionsFor","enrollment","steps","actions","push","status","OUTREACH_ENROLLMENT_ACTION_LABELS","curate","pause","resume","stop","do_not_contact","OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS","DONE_LABELS","CONFIRM","title","body","label","useOutreachEnrollmentActions","input","api","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","curating","setCurating","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","variant","error","message","allowDuplicate","choose","menuItems","only","filter","map","key","onClick","destructive","includes","disabled","dialogs","onClose","open","Boolean","fullWidth","maxWidth","spacing","value","onChange","event","target","slotProps","htmlInput","maxLength","color","trim","undefined"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type ConsolePluginOrgMount } from '@aglyn/aglyn';
2
+ import type { OutreachMailboxesResult } from './use-outreach-mailboxes';
3
+ export interface OutreachEnrollmentDetailProps {
4
+ orgId: string;
5
+ /** The org the hub is mounted under: its slug and sites, for the CRM record's address. */
6
+ orgMount?: ConsolePluginOrgMount;
7
+ /** `/[orgSlug]/outreach/sequences`. */
8
+ sectionPath: string;
9
+ sequenceId: string;
10
+ enrollmentId: string;
11
+ mailboxes: OutreachMailboxesResult;
12
+ }
13
+ /** Where the CRM record the person is lives — the site's own CRM when the site is known. */
14
+ export declare function outreachCrmRecordHref(kind: 'lead' | 'contact', id: string | null | undefined, hostId: string, orgMount: Pick<ConsolePluginOrgMount, 'orgSlug' | 'hosts'> | undefined): string | null;
15
+ /** A Gmail thread, opened in the mailbox's own account. */
16
+ export declare function outreachGmailThreadUrl(threadId: string, mailboxEmail: string | null | undefined): string;
17
+ export declare function OutreachEnrollmentDetail(props: OutreachEnrollmentDetailProps): import("react").JSX.Element;
18
+ export declare namespace OutreachEnrollmentDetail {
19
+ var displayName: string;
20
+ }
21
+ export default OutreachEnrollmentDetail;