@aglyn/plugins-outreach 1.0.0-beta.218 → 1.0.0-beta.219

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 (72) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/compliance-section.js +72 -34
  3. package/src/lib/components/compliance-section.js.map +1 -1
  4. package/src/lib/components/enroll-dialog.d.ts +12 -2
  5. package/src/lib/components/enroll-dialog.js +72 -18
  6. package/src/lib/components/enroll-dialog.js.map +1 -1
  7. package/src/lib/components/enrollment-detail.d.ts +5 -1
  8. package/src/lib/components/enrollment-detail.js +12 -6
  9. package/src/lib/components/enrollment-detail.js.map +1 -1
  10. package/src/lib/components/outreach-console-sections.d.ts +1 -1
  11. package/src/lib/components/outreach-console-sections.js +1 -1
  12. package/src/lib/components/outreach-console-sections.js.map +1 -1
  13. package/src/lib/components/sequence-editor.js +85 -40
  14. package/src/lib/components/sequence-editor.js.map +1 -1
  15. package/src/lib/components/use-outreach-api.d.ts +2 -1
  16. package/src/lib/components/use-outreach-api.js +5 -3
  17. package/src/lib/components/use-outreach-api.js.map +1 -1
  18. package/src/lib/engine/compose.d.ts +1 -1
  19. package/src/lib/engine/compose.js +8 -4
  20. package/src/lib/engine/compose.js.map +1 -1
  21. package/src/lib/engine/enrollment-activity.d.ts +9 -1
  22. package/src/lib/engine/enrollment-activity.js +15 -2
  23. package/src/lib/engine/enrollment-activity.js.map +1 -1
  24. package/src/lib/engine/enrollment-state.d.ts +33 -2
  25. package/src/lib/engine/enrollment-state.js +66 -7
  26. package/src/lib/engine/enrollment-state.js.map +1 -1
  27. package/src/lib/engine/sending-capacity.d.ts +1 -1
  28. package/src/lib/engine/sending-capacity.js +1 -1
  29. package/src/lib/engine/sending-capacity.js.map +1 -1
  30. package/src/lib/engine/sequence-validation.d.ts +20 -3
  31. package/src/lib/engine/sequence-validation.js +30 -4
  32. package/src/lib/engine/sequence-validation.js.map +1 -1
  33. package/src/lib/enrollment/enroll-people.js +6 -4
  34. package/src/lib/enrollment/enroll-people.js.map +1 -1
  35. package/src/lib/enrollment/step-overrides.d.ts +4 -2
  36. package/src/lib/enrollment/step-overrides.js +11 -4
  37. package/src/lib/enrollment/step-overrides.js.map +1 -1
  38. package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
  39. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  40. package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
  41. package/src/lib/mailboxes/mailbox-routes.js +10 -7
  42. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  43. package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
  44. package/src/lib/mailboxes/mailbox-settings.js +1 -1
  45. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  46. package/src/lib/model/enrollment-timeline.d.ts +1 -1
  47. package/src/lib/model/enrollment-timeline.js +22 -3
  48. package/src/lib/model/enrollment-timeline.js.map +1 -1
  49. package/src/lib/model/outreach-api.d.ts +19 -1
  50. package/src/lib/model/outreach-api.js.map +1 -1
  51. package/src/lib/model/outreach.types.d.ts +25 -0
  52. package/src/lib/model/outreach.types.js.map +1 -1
  53. package/src/lib/model/step-preview.d.ts +2 -0
  54. package/src/lib/model/step-preview.js +11 -7
  55. package/src/lib/model/step-preview.js.map +1 -1
  56. package/src/lib/model/stored-records.js +26 -0
  57. package/src/lib/model/stored-records.js.map +1 -1
  58. package/src/lib/plugin.js +1 -1
  59. package/src/lib/plugin.js.map +1 -1
  60. package/src/lib/routes/enroll-routes.d.ts +6 -0
  61. package/src/lib/routes/enroll-routes.js +21 -6
  62. package/src/lib/routes/enroll-routes.js.map +1 -1
  63. package/src/lib/routes/preview-routes.js +14 -5
  64. package/src/lib/routes/preview-routes.js.map +1 -1
  65. package/src/lib/routes/step-test-routes.js +3 -3
  66. package/src/lib/routes/step-test-routes.js.map +1 -1
  67. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  68. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  69. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  70. package/src/lib/transport/gmail-errors.d.ts +27 -0
  71. package/src/lib/transport/gmail-errors.js +56 -0
  72. package/src/lib/transport/gmail-errors.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.218",
3
+ "version": "1.0.0-beta.219",
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.218",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.218",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.218",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.218",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.218",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.218",
34
- "@aglyn/shared-util-email": "1.0.0-beta.218",
35
- "@aglyn/shared-util-http": "1.0.0-beta.218",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.218",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.218",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.218",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.218",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.218",
28
+ "@aglyn/aglyn": "1.0.0-beta.219",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.219",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.219",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.219",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.219",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.219",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.219",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.219",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.219",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.219",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.219",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.219",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.219",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -21,7 +21,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
21
21
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
22
22
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
23
  import { Alert, Autocomplete, Button, Chip, Paper, Stack, TextField, Typography } from "@mui/material";
24
- import { useEffect, useMemo, useRef, useState } from "react";
24
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
25
25
  import { composeOutreachFooter } from "../engine/compose.js";
26
26
  import { outreachComplianceSettingsEqual, outreachCountryLabel, outreachCountryOptions, OUTREACH_BRAND_NAME_MAX, OUTREACH_LEGAL_NAME_MAX, OUTREACH_POSTAL_ADDRESS_MAX, validateOutreachComplianceSettings } from "../model/compliance-settings.js";
27
27
  import { OutreachDoNotContactDomainsCard } from "./do-not-contact-domains.js";
@@ -66,6 +66,49 @@ function pickCard(settings, card) {
66
66
  function cardEqual(card, a, b) {
67
67
  return outreachComplianceSettingsEqual(_extends({}, b, pickCard(a, card)), b);
68
68
  }
69
+ const countryName = (option)=>option.name;
70
+ const sameCountry = (option, value)=>option.code === value.code;
71
+ /**
72
+ * The allowed countries, as chips — MEMOIZED, and every prop it takes is
73
+ * stable while the countries are (AGL-3423).
74
+ *
75
+ * A multiple Autocomplete hands its input a new chip array as
76
+ * `startAdornment` on every render, and MUI's `InputBase` copies that into
77
+ * its `FormControl` from a passive effect: a state update left pending after
78
+ * every commit the field takes part in. React 19 counts each such commit as a
79
+ * nested update, and keystrokes delivered back to back commit one after
80
+ * another with nothing to clear the count, so the fifty-first throws
81
+ * "Maximum update depth exceeded" (#185) — the sequence editor's countries
82
+ * field did exactly that. The legal name and address share one form with
83
+ * this list, so each keystroke in them re-rendered it; kept out of those
84
+ * renders, it commits only when it changes.
85
+ */ const ComplianceCountriesField = /*#__PURE__*/ memo(function ComplianceCountriesField(props) {
86
+ const { onChange } = props;
87
+ return /*#__PURE__*/ _jsx(Autocomplete, {
88
+ multiple: true,
89
+ options: props.options,
90
+ value: props.value,
91
+ getOptionLabel: countryName,
92
+ isOptionEqualToValue: sameCountry,
93
+ onChange: (_event, next)=>onChange(next),
94
+ renderValue: (value, getItemProps)=>value.map((option, index)=>{
95
+ const _getItemProps = getItemProps({
96
+ index
97
+ }), { key } = _getItemProps, item = _object_without_properties_loose(_getItemProps, [
98
+ "key"
99
+ ]);
100
+ return /*#__PURE__*/ _jsx(Chip, _extends({
101
+ size: "small",
102
+ label: option.name
103
+ }, item), key);
104
+ }),
105
+ renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
106
+ label: "Countries",
107
+ error: Boolean(props.issue),
108
+ helperText: props.issue
109
+ }))
110
+ });
111
+ });
69
112
  /**
70
113
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
71
114
  * countries a sequence may send to at all, and — below the settings, with
@@ -135,6 +178,31 @@ function cardEqual(card, a, b) {
135
178
  }
136
179
  return next;
137
180
  });
181
+ // Stable, for the memoized countries field: an edit clears the card's
182
+ // issues and replaces the list in whatever form it lands on.
183
+ const setCountries = useCallback((next)=>{
184
+ setServerIssues((previous)=>{
185
+ const cleared = _extends({}, previous);
186
+ for (const field of CARDS.countries.fields){
187
+ delete cleared[field];
188
+ }
189
+ return cleared;
190
+ });
191
+ setForm((previous)=>_extends({}, previous, {
192
+ allowedCountries: next.map((option)=>option.code)
193
+ }));
194
+ }, []);
195
+ const allowedCountries = form.allowedCountries;
196
+ const selectedCountries = useMemo(()=>allowedCountries.map((code)=>{
197
+ var _countries_find;
198
+ return (_countries_find = countries.find((option)=>option.code === code)) != null ? _countries_find : {
199
+ code,
200
+ name: outreachCountryLabel(code)
201
+ };
202
+ }), [
203
+ allowedCountries,
204
+ countries
205
+ ]);
138
206
  const discard = (card)=>{
139
207
  clearIssues(card);
140
208
  if (stored) setForm((previous)=>_extends({}, previous, pickCard(stored, card)));
@@ -177,13 +245,6 @@ function cardEqual(card, a, b) {
177
245
  onRetry: loaded.reload
178
246
  });
179
247
  }
180
- const selectedCountries = form.allowedCountries.map((code)=>{
181
- var _countries_find;
182
- return (_countries_find = countries.find((option)=>option.code === code)) != null ? _countries_find : {
183
- code,
184
- name: outreachCountryLabel(code)
185
- };
186
- });
187
248
  const cardActions = (card)=>{
188
249
  const dirty = cardDirty(card);
189
250
  return /*#__PURE__*/ _jsxs(Stack, {
@@ -317,34 +378,11 @@ function cardEqual(card, a, b) {
317
378
  color: "text.secondary",
318
379
  children: 'Sequences may send only to people in these countries, and every sequence only to the ones ' + 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' + 'of the European Union require a consent basis that an email to someone who never contacted ' + 'you does not have, so a cold email never goes outside the United States whatever this list ' + 'says. Add another country only for people who came to you first.'
319
380
  }),
320
- /*#__PURE__*/ _jsx(Autocomplete, {
321
- multiple: true,
381
+ /*#__PURE__*/ _jsx(ComplianceCountriesField, {
322
382
  options: countries,
323
383
  value: selectedCountries,
324
- getOptionLabel: (option)=>option.name,
325
- isOptionEqualToValue: (option, value)=>option.code === value.code,
326
- onChange: (_event, next)=>{
327
- clearIssues('countries');
328
- setForm((previous)=>_extends({}, previous, {
329
- allowedCountries: next.map((option)=>option.code)
330
- }));
331
- },
332
- renderValue: (value, getItemProps)=>value.map((option, index)=>{
333
- const _getItemProps = getItemProps({
334
- index
335
- }), { key } = _getItemProps, item = _object_without_properties_loose(_getItemProps, [
336
- "key"
337
- ]);
338
- return /*#__PURE__*/ _jsx(Chip, _extends({
339
- size: "small",
340
- label: option.name
341
- }, item), key);
342
- }),
343
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
344
- label: "Countries",
345
- error: Boolean(fieldIssue('allowedCountries')),
346
- helperText: fieldIssue('allowedCountries')
347
- }))
384
+ issue: fieldIssue('allowedCountries'),
385
+ onChange: setCountries
348
386
  })
349
387
  ]
350
388
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-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 } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\ntype ComplianceCardKey = 'identity' | 'countries'\n\n/**\n * The page's cards and the settings each one owns. Every card saves only its\n * own fields onto what is stored, so saving the countries never commits a\n * half-edited sender identity, and the other way round.\n */\nconst CARDS: Record<\n ComplianceCardKey,\n { fields: readonly (keyof OutreachComplianceSettings)[]; saved: string }\n> = {\n identity: {\n fields: ['legalName', 'brandName', 'postalAddress'],\n saved: 'Sender identity saved.',\n },\n countries: {\n fields: ['allowedCountries'],\n saved: 'Allowed countries saved.',\n },\n}\n\nfunction pickCard(\n settings: OutreachComplianceSettings,\n card: ComplianceCardKey,\n): Partial<OutreachComplianceSettings> {\n return Object.fromEntries(\n CARDS[card].fields.map((field) => [field, settings[field]]),\n )\n}\n\nfunction cardEqual(\n card: ComplianceCardKey,\n a: OutreachComplianceSettings,\n b: OutreachComplianceSettings,\n): boolean {\n return outreachComplianceSettingsEqual({ ...b, ...pickCard(a, card) }, b)\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244). Each\n * settings card saves its own fields from its header, the console's place\n * for a card's actions (AGL-3333).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState<ComplianceCardKey | null>(null)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n const previousStored = useRef<OutreachComplianceSettings | null>(null)\n useEffect(() => {\n if (!stored) return\n const before = previousStored.current\n previousStored.current = stored\n // A fresh read replaces every card the member has not touched; a card\n // with unsaved edits keeps them, so saving one card never wipes another.\n setForm((current) => {\n if (!before) return { ...stored }\n let next: OutreachComplianceSettings = { ...stored }\n for (const card of Object.keys(CARDS) as ComplianceCardKey[]) {\n if (!cardEqual(card, current, before)) {\n next = { ...next, ...pickCard(current, card) }\n }\n }\n return next\n })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n /** What saving one card would store: its own fields over the stored rest. */\n const candidate = (card: ComplianceCardKey) =>\n validateOutreachComplianceSettings(\n stored ? { ...stored, ...pickCard(form, card) } : form,\n )\n const cardDirty = (card: ComplianceCardKey) =>\n stored\n ? !outreachComplianceSettingsEqual(stored, candidate(card).settings)\n : false\n const cardBlocked = (card: ComplianceCardKey) =>\n candidate(card).issues.some((issue) =>\n (CARDS[card].fields as readonly string[]).includes(issue.field),\n )\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues((previous) => {\n const next = { ...previous }\n delete next[field as OutreachComplianceField]\n return next\n })\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const clearIssues = (card: ComplianceCardKey) =>\n setServerIssues((previous) => {\n const next = { ...previous }\n for (const field of CARDS[card].fields) {\n delete next[field as OutreachComplianceField]\n }\n return next\n })\n\n const discard = (card: ComplianceCardKey) => {\n clearIssues(card)\n if (stored)\n setForm((previous) => ({ ...previous, ...pickCard(stored, card) }))\n }\n\n const save = async (card: ComplianceCardKey) => {\n setSaving(card)\n try {\n const answer = await api.saveSettings(candidate(card).settings)\n clearIssues(card)\n setForm((previous) => ({\n ...previous,\n ...pickCard(answer.settings, card),\n }))\n loaded.reload()\n enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {\n variant: answer.changed ? 'success' : 'info',\n })\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues((previous) => ({\n ...previous,\n ...Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n }))\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(null)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n const cardActions = (card: ComplianceCardKey) => {\n const dirty = cardDirty(card)\n return (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!dirty || saving !== null}\n onClick={() => discard(card)}\n >\n Discard changes\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!dirty || saving !== null || cardBlocked(card)}\n onClick={() => void save(card)}\n >\n {saving === card ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n )\n }\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n HeaderProps={{ action: cardActions('identity') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n HeaderProps={{ action: cardActions('countries') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n clearIssues('countries')\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n {/* Its own list, saved as it is edited (AGL-3244): it has no Save. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useRef","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","CARDS","identity","fields","saved","countries","pickCard","settings","card","Object","fromEntries","map","field","cardEqual","a","b","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","stored","previousStored","before","current","next","keys","normalized","issues","find","issue","message","candidate","cardDirty","cardBlocked","some","includes","footer","set","value","previous","clearIssues","discard","save","answer","saveSettings","reload","changed","variant","error","length","filter","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","cardActions","dirty","direction","spacing","size","disabled","onClick","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","renderValue","getItemProps","index","key","item","renderInput","params","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-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 } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\ntype ComplianceCardKey = 'identity' | 'countries'\n\n/**\n * The page's cards and the settings each one owns. Every card saves only its\n * own fields onto what is stored, so saving the countries never commits a\n * half-edited sender identity, and the other way round.\n */\nconst CARDS: Record<\n ComplianceCardKey,\n { fields: readonly (keyof OutreachComplianceSettings)[]; saved: string }\n> = {\n identity: {\n fields: ['legalName', 'brandName', 'postalAddress'],\n saved: 'Sender identity saved.',\n },\n countries: {\n fields: ['allowedCountries'],\n saved: 'Allowed countries saved.',\n },\n}\n\nfunction pickCard(\n settings: OutreachComplianceSettings,\n card: ComplianceCardKey,\n): Partial<OutreachComplianceSettings> {\n return Object.fromEntries(\n CARDS[card].fields.map((field) => [field, settings[field]]),\n )\n}\n\nfunction cardEqual(\n card: ComplianceCardKey,\n a: OutreachComplianceSettings,\n b: OutreachComplianceSettings,\n): boolean {\n return outreachComplianceSettingsEqual({ ...b, ...pickCard(a, card) }, b)\n}\n\nconst countryName = (option: OutreachCountryOption): string => option.name\nconst sameCountry = (\n option: OutreachCountryOption,\n value: OutreachCountryOption,\n): boolean => option.code === value.code\n\n/**\n * The allowed countries, as chips — MEMOIZED, and every prop it takes is\n * stable while the countries are (AGL-3423).\n *\n * A multiple Autocomplete hands its input a new chip array as\n * `startAdornment` on every render, and MUI's `InputBase` copies that into\n * its `FormControl` from a passive effect: a state update left pending after\n * every commit the field takes part in. React 19 counts each such commit as a\n * nested update, and keystrokes delivered back to back commit one after\n * another with nothing to clear the count, so the fifty-first throws\n * \"Maximum update depth exceeded\" (#185) — the sequence editor's countries\n * field did exactly that. The legal name and address share one form with\n * this list, so each keystroke in them re-rendered it; kept out of those\n * renders, it commits only when it changes.\n */\nconst ComplianceCountriesField = memo(function ComplianceCountriesField(props: {\n options: readonly OutreachCountryOption[]\n value: readonly OutreachCountryOption[]\n issue?: string\n onChange(next: OutreachCountryOption[]): void\n}) {\n const { onChange } = props\n return (\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={props.options}\n value={props.value as OutreachCountryOption[]}\n getOptionLabel={countryName}\n isOptionEqualToValue={sameCountry}\n onChange={(_event, next) => onChange(next)}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return <Chip key={key} size=\"small\" label={option.name} {...item} />\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(props.issue)}\n helperText={props.issue}\n />\n )}\n />\n )\n})\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244). Each\n * settings card saves its own fields from its header, the console's place\n * for a card's actions (AGL-3333).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState<ComplianceCardKey | null>(null)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n const previousStored = useRef<OutreachComplianceSettings | null>(null)\n useEffect(() => {\n if (!stored) return\n const before = previousStored.current\n previousStored.current = stored\n // A fresh read replaces every card the member has not touched; a card\n // with unsaved edits keeps them, so saving one card never wipes another.\n setForm((current) => {\n if (!before) return { ...stored }\n let next: OutreachComplianceSettings = { ...stored }\n for (const card of Object.keys(CARDS) as ComplianceCardKey[]) {\n if (!cardEqual(card, current, before)) {\n next = { ...next, ...pickCard(current, card) }\n }\n }\n return next\n })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n /** What saving one card would store: its own fields over the stored rest. */\n const candidate = (card: ComplianceCardKey) =>\n validateOutreachComplianceSettings(\n stored ? { ...stored, ...pickCard(form, card) } : form,\n )\n const cardDirty = (card: ComplianceCardKey) =>\n stored\n ? !outreachComplianceSettingsEqual(stored, candidate(card).settings)\n : false\n const cardBlocked = (card: ComplianceCardKey) =>\n candidate(card).issues.some((issue) =>\n (CARDS[card].fields as readonly string[]).includes(issue.field),\n )\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues((previous) => {\n const next = { ...previous }\n delete next[field as OutreachComplianceField]\n return next\n })\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const clearIssues = (card: ComplianceCardKey) =>\n setServerIssues((previous) => {\n const next = { ...previous }\n for (const field of CARDS[card].fields) {\n delete next[field as OutreachComplianceField]\n }\n return next\n })\n\n // Stable, for the memoized countries field: an edit clears the card's\n // issues and replaces the list in whatever form it lands on.\n const setCountries = useCallback((next: OutreachCountryOption[]) => {\n setServerIssues((previous) => {\n const cleared = { ...previous }\n for (const field of CARDS.countries.fields) {\n delete cleared[field as OutreachComplianceField]\n }\n return cleared\n })\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }, [])\n const allowedCountries = form.allowedCountries\n const selectedCountries = useMemo(\n () =>\n allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n ),\n [allowedCountries, countries],\n )\n\n const discard = (card: ComplianceCardKey) => {\n clearIssues(card)\n if (stored)\n setForm((previous) => ({ ...previous, ...pickCard(stored, card) }))\n }\n\n const save = async (card: ComplianceCardKey) => {\n setSaving(card)\n try {\n const answer = await api.saveSettings(candidate(card).settings)\n clearIssues(card)\n setForm((previous) => ({\n ...previous,\n ...pickCard(answer.settings, card),\n }))\n loaded.reload()\n enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {\n variant: answer.changed ? 'success' : 'info',\n })\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues((previous) => ({\n ...previous,\n ...Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n }))\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(null)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const cardActions = (card: ComplianceCardKey) => {\n const dirty = cardDirty(card)\n return (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!dirty || saving !== null}\n onClick={() => discard(card)}\n >\n Discard changes\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!dirty || saving !== null || cardBlocked(card)}\n onClick={() => void save(card)}\n >\n {saving === card ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n )\n }\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n HeaderProps={{ action: cardActions('identity') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n HeaderProps={{ action: cardActions('countries') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <ComplianceCountriesField\n options={countries}\n value={selectedCountries}\n issue={fieldIssue('allowedCountries')}\n onChange={setCountries}\n />\n </Stack>\n </CardDisplay>\n\n {/* Its own list, saved as it is edited (AGL-3244): it has no Save. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","memo","useCallback","useEffect","useMemo","useRef","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","CARDS","identity","fields","saved","countries","pickCard","settings","card","Object","fromEntries","map","field","cardEqual","a","b","countryName","option","name","sameCountry","value","code","ComplianceCountriesField","props","onChange","multiple","options","getOptionLabel","isOptionEqualToValue","_event","next","renderValue","getItemProps","index","key","item","size","label","renderInput","params","error","Boolean","issue","helperText","OutreachComplianceSection","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","stored","previousStored","before","current","keys","normalized","issues","find","message","candidate","cardDirty","cardBlocked","some","includes","footer","set","previous","clearIssues","setCountries","cleared","selectedCountries","discard","save","answer","saveSettings","reload","changed","variant","length","filter","allowDuplicate","status","what","onRetry","cardActions","dirty","direction","spacing","disabled","onClick","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","color","event","target","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","displayName"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { type OutreachEnrollGateway, type OutreachEnrollPreviewPerson } from '../model/outreach-api';
2
- import { type OutreachAttestationKind, type OutreachSequence } from '../model/outreach.types';
2
+ import { type OutreachAttestationKind, type OutreachSequence, type OutreachSequenceStep } from '../model/outreach.types';
3
3
  import { type OutreachCuratedDraftState } from './curated-step-editor';
4
4
  import type { OutreachApi } from './use-outreach-api';
5
5
  export interface OutreachEnrollDialogProps {
@@ -38,8 +38,18 @@ interface Supplied {
38
38
  drafts?: Record<number, OutreachCuratedDraftState>;
39
39
  curation?: Curation;
40
40
  }
41
+ /**
42
+ * The steps the dialog offers to begin at (AGL-3228), each by its place in
43
+ * the sequence: "Email 2 · step 3", "Step 2 · LinkedIn".
44
+ */
45
+ export declare function outreachStartStepOptions(steps: readonly OutreachSequenceStep[]): Array<{
46
+ value: number;
47
+ label: string;
48
+ }>;
49
+ /** What beginning at a later step does, as the dialog says it under the picker. */
50
+ export declare function outreachStartStepHelp(startStepIndex: number): string;
41
51
  /** Whether a person may be sent to Confirm as the rep has left them. */
42
- export declare function outreachPersonReady(person: OutreachEnrollPreviewPerson, supplied: Supplied | undefined): boolean;
52
+ export declare function outreachPersonReady(person: OutreachEnrollPreviewPerson, supplied: Supplied | undefined, startStepIndex?: number): boolean;
43
53
  /**
44
54
  * Enroll people in a sequence (AGL-2980).
45
55
  *
@@ -23,6 +23,7 @@ import { useMemo, useState } from "react";
23
23
  import { isOutreachGatewayFronted, outreachGatewayChip } from "../engine/mail-gateway.js";
24
24
  import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
25
25
  import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
26
+ import { outreachStepName } from "../model/enrollment-timeline.js";
26
27
  import { outreachEmailStepLabel } from "./curate-step-dialog.js";
27
28
  import { OutreachCuratedStepEditor, outreachDraftState, outreachDraftToOverride, outreachMemberDraftStates } from "./curated-step-editor.js";
28
29
  import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
@@ -51,21 +52,40 @@ const STATUS_COLORS = {
51
52
  var _ref;
52
53
  return ((_ref = gateway == null ? void 0 : gateway.blocked7) != null ? _ref : 0) > 0;
53
54
  }
54
- /** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied) {
55
+ /**
56
+ * The drafts for the steps that will run: none before the step the
57
+ * enrollment begins at (AGL-3228), which is skipped and never sent.
58
+ */ function liveDrafts(supplied, startStepIndex) {
55
59
  var _ref;
56
- return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).map(outreachDraftToOverride).filter((override)=>override !== null);
60
+ return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).filter((draft)=>draft.stepIndex >= startStepIndex);
57
61
  }
58
- /** Whether a drafted step is still waiting on the rep to use it or keep the template. */ function hasOpenDrafts(supplied) {
59
- var _ref;
60
- return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).some((draft)=>draft.decision === 'pending');
62
+ /** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied, startStepIndex) {
63
+ return liveDrafts(supplied, startStepIndex).map(outreachDraftToOverride).filter((override)=>override !== null);
61
64
  }
62
- /** Whether a person may be sent to Confirm as the rep has left them. */ export function outreachPersonReady(person, supplied) {
65
+ /** Whether a drafted step is still waiting on the rep to use it or keep the template. */ function hasOpenDrafts(supplied, startStepIndex) {
66
+ return liveDrafts(supplied, startStepIndex).some((draft)=>draft.decision === 'pending');
67
+ }
68
+ /**
69
+ * The steps the dialog offers to begin at (AGL-3228), each by its place in
70
+ * the sequence: "Email 2 · step 3", "Step 2 · LinkedIn".
71
+ */ export function outreachStartStepOptions(steps) {
72
+ return steps.map((_step, index)=>({
73
+ value: index,
74
+ label: index === 0 ? `${outreachStepName(steps, 0)} — from the start` : outreachStepName(steps, index)
75
+ }));
76
+ }
77
+ /** What beginning at a later step does, as the dialog says it under the picker. */ export function outreachStartStepHelp(startStepIndex) {
78
+ if (startStepIndex < 1) return 'Everyone starts at step 1, after its delay.';
79
+ const skipped = startStepIndex === 1 ? 'Step 1 is' : `Steps 1–${startStepIndex} are`;
80
+ return `For people who already had the earlier steps by hand. ${skipped} marked skipped and never sent; step ${startStepIndex + 1} goes out after its own delay, counted from now, and starts the email thread.`;
81
+ }
82
+ /** Whether a person may be sent to Confirm as the rep has left them. */ export function outreachPersonReady(person, supplied, startStepIndex = 0) {
63
83
  if (person.status === 'blocked' || !(supplied == null ? void 0 : supplied.include)) return false;
64
84
  const line = supplied.personalLine.trim();
65
85
  if (line.length > OUTREACH_PERSONAL_LINE_MAX) return false;
66
86
  // A draft the rep has read but neither used nor kept holds them back
67
87
  // (AGL-3324): nothing is enrolled with a decision still open.
68
- if (hasOpenDrafts(supplied)) return false;
88
+ if (hasOpenDrafts(supplied, startStepIndex)) return false;
69
89
  if (person.status === 'eligible') return true;
70
90
  if (person.requires.personalLine && !line) return false;
71
91
  return person.requires.attestations.every((kind)=>supplied.attestations.includes(kind));
@@ -98,6 +118,11 @@ const STATUS_COLORS = {
98
118
  const [supplied, setSupplied] = useState({});
99
119
  const [emailPreview, setEmailPreview] = useState({});
100
120
  const [stepTests, setStepTests] = useState({});
121
+ // The step everyone in this enrollment begins at (AGL-3228), zero-based.
122
+ const [startStepIndex, setStartStepIndex] = useState(0);
123
+ const startOptions = useMemo(()=>outreachStartStepOptions(sequence.steps), [
124
+ sequence.steps
125
+ ]);
101
126
  const views = useOutreachSavedViews(props.open ? orgId : null, props.uid);
102
127
  const search = useOutreachContactSearch({
103
128
  orgId: props.open && tab === 'search' ? orgId : null,
@@ -120,6 +145,7 @@ const STATUS_COLORS = {
120
145
  setSupplied({});
121
146
  setEmailPreview({});
122
147
  setStepTests({});
148
+ setStartStepIndex(0);
123
149
  };
124
150
  const close = ()=>{
125
151
  reset();
@@ -159,13 +185,14 @@ const STATUS_COLORS = {
159
185
  }
160
186
  };
161
187
  const answer = stage.kind === 'preview' || stage.kind === 'enrolling' ? stage.answer : null;
162
- const ready = useMemo(()=>answer ? answer.people.filter((person)=>outreachPersonReady(person, supplied[person.personId])) : [], [
188
+ const ready = useMemo(()=>answer ? answer.people.filter((person)=>outreachPersonReady(person, supplied[person.personId], startStepIndex)) : [], [
163
189
  answer,
164
- supplied
190
+ supplied,
191
+ startStepIndex
165
192
  ]);
166
193
  const waiting = answer ? answer.people.filter((person)=>{
167
194
  var _supplied_person_personId;
168
- return person.status !== 'blocked' && ((_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.include) && !outreachPersonReady(person, supplied[person.personId]);
195
+ return person.status !== 'blocked' && ((_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.include) && !outreachPersonReady(person, supplied[person.personId], startStepIndex);
169
196
  }).length : 0;
170
197
  const enroll = async ()=>{
171
198
  if (!answer || !ready.length) return;
@@ -185,10 +212,13 @@ const STATUS_COLORS = {
185
212
  }, {
186
213
  personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : '',
187
214
  attestations: (_ref1 = (_supplied_person_personId1 = supplied[person.personId]) == null ? void 0 : _supplied_person_personId1.attestations) != null ? _ref1 : []
188
- }, usedOverrides(supplied[person.personId]).length ? {
189
- stepOverrides: usedOverrides(supplied[person.personId])
215
+ }, usedOverrides(supplied[person.personId], startStepIndex).length ? {
216
+ stepOverrides: usedOverrides(supplied[person.personId], startStepIndex)
190
217
  } : {});
191
- }));
218
+ }), // Named only when it is not the first step (AGL-3228).
219
+ ...startStepIndex > 0 ? [
220
+ startStepIndex
221
+ ] : []);
192
222
  setStage({
193
223
  kind: 'done',
194
224
  results: result.results,
@@ -214,7 +244,7 @@ const STATUS_COLORS = {
214
244
  try {
215
245
  var _ref;
216
246
  var _supplied_person_personId;
217
- const overrides = usedOverrides(supplied[person.personId]);
247
+ const overrides = usedOverrides(supplied[person.personId], startStepIndex);
218
248
  const result = await api.previewEmail(_extends({
219
249
  sequenceId: sequence.id
220
250
  }, person.target === 'lead' && person.leadId ? {
@@ -225,6 +255,8 @@ const STATUS_COLORS = {
225
255
  personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
226
256
  }, overrides.length ? {
227
257
  stepOverrides: overrides
258
+ } : {}, startStepIndex > 0 ? {
259
+ startStepIndex
228
260
  } : {}));
229
261
  setEmailPreview((previous)=>_extends({}, previous, {
230
262
  [person.personId]: {
@@ -312,7 +344,7 @@ const STATUS_COLORS = {
312
344
  try {
313
345
  var _ref;
314
346
  var _supplied_person_personId;
315
- const overrides = usedOverrides(supplied[person.personId]);
347
+ const overrides = usedOverrides(supplied[person.personId], startStepIndex);
316
348
  const answer = await api.sendStepTest(_extends({
317
349
  sequenceId: sequence.id
318
350
  }, person.target === 'lead' && person.leadId ? {
@@ -323,6 +355,8 @@ const STATUS_COLORS = {
323
355
  personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
324
356
  }, overrides.length ? {
325
357
  stepOverrides: overrides
358
+ } : {}, startStepIndex > 0 ? {
359
+ startStepIndex
326
360
  } : {}));
327
361
  setStepTests((previous)=>_extends({}, previous, {
328
362
  [person.personId]: {
@@ -588,6 +622,25 @@ const STATUS_COLORS = {
588
622
  severity: "info",
589
623
  children: `This shows the first ${answer.people.length} of ${answer.total}. Enroll these, then preview again for the rest.`
590
624
  }) : null,
625
+ startOptions.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
626
+ select: true,
627
+ size: "small",
628
+ label: "Start at",
629
+ value: startStepIndex,
630
+ onChange: (event)=>{
631
+ setStartStepIndex(Number(event.target.value));
632
+ // A preview or a test was written for the old first email.
633
+ setEmailPreview({});
634
+ setStepTests({});
635
+ },
636
+ helperText: outreachStartStepHelp(startStepIndex),
637
+ disabled: stage.kind === 'enrolling',
638
+ fullWidth: true,
639
+ children: startOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
640
+ value: option.value,
641
+ children: option.label
642
+ }, option.value))
643
+ }) : null,
591
644
  /*#__PURE__*/ _jsx(Stack, {
592
645
  spacing: 1,
593
646
  component: "ul",
@@ -600,6 +653,7 @@ const STATUS_COLORS = {
600
653
  children: answer.people.map((person)=>/*#__PURE__*/ _jsx(PersonRow, {
601
654
  person: person,
602
655
  steps: sequence.steps,
656
+ startStepIndex: startStepIndex,
603
657
  supplied: supplied[person.personId],
604
658
  onSupply: (next)=>supply(person.personId, next),
605
659
  emailPreview: emailPreview[person.personId],
@@ -699,7 +753,7 @@ function summary(answer) {
699
753
  });
700
754
  }
701
755
  function PersonRow(props) {
702
- var _ref, _ref1, _ref2, _ref3;
756
+ var _ref, _ref1, _ref2;
703
757
  var _person_gateway, _person_gateway1, _person_gateway2, _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
704
758
  const { person, supplied, disabled } = props;
705
759
  const name = person.name || person.email || person.personId;
@@ -708,7 +762,7 @@ function PersonRow(props) {
708
762
  const curation = (_ref1 = supplied == null ? void 0 : supplied.curation) != null ? _ref1 : {
709
763
  status: 'idle'
710
764
  };
711
- const drafts = Object.values((_ref2 = supplied == null ? void 0 : supplied.drafts) != null ? _ref2 : {}).sort((a, b)=>a.stepIndex - b.stepIndex);
765
+ const drafts = liveDrafts(supplied, props.startStepIndex).sort((a, b)=>a.stepIndex - b.stepIndex);
712
766
  const usedCount = drafts.filter((draft)=>draft.decision === 'use').length;
713
767
  return /*#__PURE__*/ _jsx(Paper, {
714
768
  component: "li",
@@ -797,7 +851,7 @@ function PersonRow(props) {
797
851
  children: [
798
852
  /*#__PURE__*/ _jsx(FormControlLabel, {
799
853
  control: /*#__PURE__*/ _jsx(Checkbox, {
800
- checked: (_ref3 = supplied == null ? void 0 : supplied.include) != null ? _ref3 : true,
854
+ checked: (_ref2 = supplied == null ? void 0 : supplied.include) != null ? _ref2 : true,
801
855
  onChange: (event)=>props.onSupply({
802
856
  include: event.target.checked
803
857
  }),