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

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 (105) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/compliance-section.d.ts +3 -1
  3. package/src/lib/components/compliance-section.js +110 -45
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.d.ts +7 -1
  6. package/src/lib/components/enroll-dialog.js +106 -3
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/mailbox-card.js +11 -2
  9. package/src/lib/components/mailbox-card.js.map +1 -1
  10. package/src/lib/components/outreach-ui.js +1 -0
  11. package/src/lib/components/outreach-ui.js.map +1 -1
  12. package/src/lib/components/sequence-detail.js +1 -0
  13. package/src/lib/components/sequence-detail.js.map +1 -1
  14. package/src/lib/components/sequence-editor.d.ts +2 -0
  15. package/src/lib/components/sequence-editor.js +51 -26
  16. package/src/lib/components/sequence-editor.js.map +1 -1
  17. package/src/lib/components/sequence-step-card.d.ts +7 -0
  18. package/src/lib/components/sequence-step-card.js +37 -14
  19. package/src/lib/components/sequence-step-card.js.map +1 -1
  20. package/src/lib/components/sequences-section.js +12 -3
  21. package/src/lib/components/sequences-section.js.map +1 -1
  22. package/src/lib/components/step-test-dialog.d.ts +31 -0
  23. package/src/lib/components/step-test-dialog.js +247 -0
  24. package/src/lib/components/step-test-dialog.js.map +1 -0
  25. package/src/lib/components/use-outreach-api.d.ts +8 -1
  26. package/src/lib/components/use-outreach-api.js +4 -0
  27. package/src/lib/components/use-outreach-api.js.map +1 -1
  28. package/src/lib/components/use-outreach-data.d.ts +6 -1
  29. package/src/lib/components/use-outreach-data.js +6 -3
  30. package/src/lib/components/use-outreach-data.js.map +1 -1
  31. package/src/lib/constants/api-routes.d.ts +7 -0
  32. package/src/lib/constants/api-routes.js +6 -0
  33. package/src/lib/constants/api-routes.js.map +1 -1
  34. package/src/lib/engine/delivery-status.d.ts +8 -0
  35. package/src/lib/engine/delivery-status.js +10 -5
  36. package/src/lib/engine/delivery-status.js.map +1 -1
  37. package/src/lib/engine/enrollment-state.d.ts +2 -0
  38. package/src/lib/engine/enrollment-state.js +2 -2
  39. package/src/lib/engine/enrollment-state.js.map +1 -1
  40. package/src/lib/engine/gates.d.ts +28 -1
  41. package/src/lib/engine/gates.js +28 -1
  42. package/src/lib/engine/gates.js.map +1 -1
  43. package/src/lib/engine/mail-gateway.d.ts +97 -0
  44. package/src/lib/engine/mail-gateway.js +226 -0
  45. package/src/lib/engine/mail-gateway.js.map +1 -0
  46. package/src/lib/engine/mailbox-health.d.ts +3 -3
  47. package/src/lib/engine/mailbox-health.js +17 -11
  48. package/src/lib/engine/mailbox-health.js.map +1 -1
  49. package/src/lib/engine/thread-classification.d.ts +2 -0
  50. package/src/lib/engine/thread-classification.js +2 -1
  51. package/src/lib/engine/thread-classification.js.map +1 -1
  52. package/src/lib/enrollment/enroll-people.d.ts +6 -1
  53. package/src/lib/enrollment/enroll-people.js +18 -4
  54. package/src/lib/enrollment/enroll-people.js.map +1 -1
  55. package/src/lib/enrollment/gate-lookups.d.ts +7 -0
  56. package/src/lib/enrollment/gate-lookups.js +11 -2
  57. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  58. package/src/lib/model/outreach-api.d.ts +35 -1
  59. package/src/lib/model/outreach-api.js.map +1 -1
  60. package/src/lib/model/outreach.types.d.ts +85 -3
  61. package/src/lib/model/outreach.types.js +21 -2
  62. package/src/lib/model/outreach.types.js.map +1 -1
  63. package/src/lib/model/step-preview.d.ts +7 -1
  64. package/src/lib/model/step-preview.js +3 -2
  65. package/src/lib/model/step-preview.js.map +1 -1
  66. package/src/lib/routes/enroll-routes.js +23 -4
  67. package/src/lib/routes/enroll-routes.js.map +1 -1
  68. package/src/lib/routes/enrollment-routes.js +15 -1
  69. package/src/lib/routes/enrollment-routes.js.map +1 -1
  70. package/src/lib/routes/platform-deps.d.ts +2 -2
  71. package/src/lib/routes/platform-deps.js +2 -2
  72. package/src/lib/routes/platform-deps.js.map +1 -1
  73. package/src/lib/routes/preview-routes.d.ts +24 -0
  74. package/src/lib/routes/preview-routes.js +99 -95
  75. package/src/lib/routes/preview-routes.js.map +1 -1
  76. package/src/lib/routes/register-routes.d.ts +7 -0
  77. package/src/lib/routes/register-routes.js +27 -0
  78. package/src/lib/routes/register-routes.js.map +1 -1
  79. package/src/lib/routes/route-deps.d.ts +6 -0
  80. package/src/lib/routes/route-deps.js.map +1 -1
  81. package/src/lib/routes/step-test-routes.d.ts +88 -0
  82. package/src/lib/routes/step-test-routes.js +275 -0
  83. package/src/lib/routes/step-test-routes.js.map +1 -0
  84. package/src/lib/runtime/click-events.js +10 -0
  85. package/src/lib/runtime/click-events.js.map +1 -1
  86. package/src/lib/runtime/click-link.d.ts +11 -0
  87. package/src/lib/runtime/click-link.js +9 -4
  88. package/src/lib/runtime/click-link.js.map +1 -1
  89. package/src/lib/runtime/click-route.js +3 -0
  90. package/src/lib/runtime/click-route.js.map +1 -1
  91. package/src/lib/runtime/gateway-ledger.d.ts +46 -0
  92. package/src/lib/runtime/gateway-ledger.js +113 -0
  93. package/src/lib/runtime/gateway-ledger.js.map +1 -0
  94. package/src/lib/runtime/platform-runtime-deps.js +3 -0
  95. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  96. package/src/lib/runtime/runtime-deps.d.ts +6 -0
  97. package/src/lib/runtime/runtime-deps.js.map +1 -1
  98. package/src/lib/runtime/send-job.js +48 -0
  99. package/src/lib/runtime/send-job.js.map +1 -1
  100. package/src/lib/runtime/sync-job.d.ts +2 -0
  101. package/src/lib/runtime/sync-job.js +40 -3
  102. package/src/lib/runtime/sync-job.js.map +1 -1
  103. package/src/lib/storage/domain-intel-store.d.ts +107 -0
  104. package/src/lib/storage/domain-intel-store.js +320 -0
  105. package/src/lib/storage/domain-intel-store.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.196",
3
+ "version": "1.0.0-beta.198",
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.196",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.196",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.196",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.196",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.196",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.196",
34
- "@aglyn/shared-util-email": "1.0.0-beta.196",
35
- "@aglyn/shared-util-http": "1.0.0-beta.196",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.196",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.196",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.196",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.196",
28
+ "@aglyn/aglyn": "1.0.0-beta.198",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.198",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.198",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.198",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.198",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.198",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.198",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.198",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.198",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.198",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.198",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.198",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -7,7 +7,9 @@ export declare const OUTREACH_POSTAL_ADDRESS_HELP: string;
7
7
  /**
8
8
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
9
9
  * countries a sequence may send to at all, and — below the settings, with
10
- * no Save of its own — the domains no sequence emails (AGL-3244).
10
+ * no Save of its own — the domains no sequence emails (AGL-3244). Each
11
+ * settings card saves its own fields from its header, the console's place
12
+ * for a card's actions (AGL-3333).
11
13
  *
12
14
  * The footer is previewed from what is typed, by the engine's own
13
15
  * `composeOutreachFooter`, so the page shows the lines every email will end
@@ -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, useState } from "react";
24
+ import { 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";
@@ -37,10 +37,41 @@ const EMPTY = {
37
37
  'US'
38
38
  ]
39
39
  };
40
+ /**
41
+ * The page's cards and the settings each one owns. Every card saves only its
42
+ * own fields onto what is stored, so saving the countries never commits a
43
+ * half-edited sender identity, and the other way round.
44
+ */ const CARDS = {
45
+ identity: {
46
+ fields: [
47
+ 'legalName',
48
+ 'brandName',
49
+ 'postalAddress'
50
+ ],
51
+ saved: 'Sender identity saved.'
52
+ },
53
+ countries: {
54
+ fields: [
55
+ 'allowedCountries'
56
+ ],
57
+ saved: 'Allowed countries saved.'
58
+ }
59
+ };
60
+ function pickCard(settings, card) {
61
+ return Object.fromEntries(CARDS[card].fields.map((field)=>[
62
+ field,
63
+ settings[field]
64
+ ]));
65
+ }
66
+ function cardEqual(card, a, b) {
67
+ return outreachComplianceSettingsEqual(_extends({}, b, pickCard(a, card)), b);
68
+ }
40
69
  /**
41
70
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
42
71
  * countries a sequence may send to at all, and — below the settings, with
43
- * no Save of its own — the domains no sequence emails (AGL-3244).
72
+ * no Save of its own — the domains no sequence emails (AGL-3244). Each
73
+ * settings card saves its own fields from its header, the console's place
74
+ * for a card's actions (AGL-3333).
44
75
  *
45
76
  * The footer is previewed from what is typed, by the engine's own
46
77
  * `composeOutreachFooter`, so the page shows the lines every email will end
@@ -53,12 +84,27 @@ const EMPTY = {
53
84
  const loaded = useOutreachComplianceSettings(api, orgId);
54
85
  const { enqueueSnackbar } = useSnackbar();
55
86
  const [form, setForm] = useState(EMPTY);
56
- const [saving, setSaving] = useState(false);
87
+ const [saving, setSaving] = useState(null);
57
88
  const [serverIssues, setServerIssues] = useState({});
58
89
  const countries = useMemo(()=>outreachCountryOptions(), []);
59
90
  const stored = loaded.settings;
91
+ const previousStored = useRef(null);
60
92
  useEffect(()=>{
61
- if (stored) setForm(_extends({}, stored));
93
+ if (!stored) return;
94
+ const before = previousStored.current;
95
+ previousStored.current = stored;
96
+ // A fresh read replaces every card the member has not touched; a card
97
+ // with unsaved edits keeps them, so saving one card never wipes another.
98
+ setForm((current)=>{
99
+ if (!before) return _extends({}, stored);
100
+ let next = _extends({}, stored);
101
+ for (const card of Object.keys(CARDS)){
102
+ if (!cardEqual(card, current, before)) {
103
+ next = _extends({}, next, pickCard(current, card));
104
+ }
105
+ }
106
+ return next;
107
+ });
62
108
  }, [
63
109
  stored
64
110
  ]);
@@ -68,37 +114,54 @@ const EMPTY = {
68
114
  var _issues_find;
69
115
  return (_serverIssues_field = serverIssues[field]) != null ? _serverIssues_field : (_issues_find = issues.find((issue)=>issue.field === field)) == null ? void 0 : _issues_find.message;
70
116
  };
71
- const dirty = stored ? !outreachComplianceSettingsEqual(stored, normalized) : false;
117
+ /** What saving one card would store: its own fields over the stored rest. */ const candidate = (card)=>validateOutreachComplianceSettings(stored ? _extends({}, stored, pickCard(form, card)) : form);
118
+ const cardDirty = (card)=>stored ? !outreachComplianceSettingsEqual(stored, candidate(card).settings) : false;
119
+ const cardBlocked = (card)=>candidate(card).issues.some((issue)=>CARDS[card].fields.includes(issue.field));
72
120
  const footer = composeOutreachFooter(normalized);
73
121
  const set = (field)=>(value)=>{
74
- setServerIssues({});
122
+ setServerIssues((previous)=>{
123
+ const next = _extends({}, previous);
124
+ delete next[field];
125
+ return next;
126
+ });
75
127
  setForm((previous)=>_extends({}, previous, {
76
128
  [field]: value
77
129
  }));
78
130
  };
79
- const save = async ()=>{
80
- setSaving(true);
131
+ const clearIssues = (card)=>setServerIssues((previous)=>{
132
+ const next = _extends({}, previous);
133
+ for (const field of CARDS[card].fields){
134
+ delete next[field];
135
+ }
136
+ return next;
137
+ });
138
+ const discard = (card)=>{
139
+ clearIssues(card);
140
+ if (stored) setForm((previous)=>_extends({}, previous, pickCard(stored, card)));
141
+ };
142
+ const save = async (card)=>{
143
+ setSaving(card);
81
144
  try {
82
- const answer = await api.saveSettings(normalized);
83
- setServerIssues({});
84
- setForm(_extends({}, answer.settings));
145
+ const answer = await api.saveSettings(candidate(card).settings);
146
+ clearIssues(card);
147
+ setForm((previous)=>_extends({}, previous, pickCard(answer.settings, card)));
85
148
  loaded.reload();
86
- enqueueSnackbar(answer.changed ? 'Compliance settings saved.' : 'Nothing changed.', {
149
+ enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {
87
150
  variant: answer.changed ? 'success' : 'info'
88
151
  });
89
152
  } catch (error) {
90
153
  if (error instanceof OutreachRouteError && error.issues.length) {
91
- setServerIssues(Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
92
- issue.field,
93
- issue.message
94
- ])));
154
+ setServerIssues((previous)=>_extends({}, previous, Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
155
+ issue.field,
156
+ issue.message
157
+ ]))));
95
158
  }
96
159
  enqueueSnackbar(error.message, {
97
160
  variant: 'error',
98
161
  allowDuplicate: true
99
162
  });
100
163
  } finally{
101
- setSaving(false);
164
+ setSaving(null);
102
165
  }
103
166
  };
104
167
  if (loaded.status === 'loading' || !orgId && !stored) {
@@ -121,6 +184,29 @@ const EMPTY = {
121
184
  name: outreachCountryLabel(code)
122
185
  };
123
186
  });
187
+ const cardActions = (card)=>{
188
+ const dirty = cardDirty(card);
189
+ return /*#__PURE__*/ _jsxs(Stack, {
190
+ direction: "row",
191
+ spacing: 1,
192
+ children: [
193
+ /*#__PURE__*/ _jsx(Button, {
194
+ size: "small",
195
+ variant: "text",
196
+ disabled: !dirty || saving !== null,
197
+ onClick: ()=>discard(card),
198
+ children: "Discard changes"
199
+ }),
200
+ /*#__PURE__*/ _jsx(Button, {
201
+ size: "small",
202
+ variant: "contained",
203
+ disabled: !dirty || saving !== null || cardBlocked(card),
204
+ onClick: ()=>void save(card),
205
+ children: saving === card ? 'Saving…' : 'Save'
206
+ })
207
+ ]
208
+ });
209
+ };
124
210
  return /*#__PURE__*/ _jsxs(Stack, {
125
211
  spacing: 2,
126
212
  children: [
@@ -129,6 +215,9 @@ const EMPTY = {
129
215
  help: pluginDocsHelp('sequences', {
130
216
  anchor: '#compliance-settings'
131
217
  }),
218
+ HeaderProps: {
219
+ action: cardActions('identity')
220
+ },
132
221
  contentGutterX: true,
133
222
  contentGutterY: true,
134
223
  children: /*#__PURE__*/ _jsxs(Stack, {
@@ -215,6 +304,9 @@ const EMPTY = {
215
304
  help: pluginDocsHelp('sequences', {
216
305
  anchor: '#allowed-countries'
217
306
  }),
307
+ HeaderProps: {
308
+ action: cardActions('countries')
309
+ },
218
310
  contentGutterX: true,
219
311
  contentGutterY: true,
220
312
  children: /*#__PURE__*/ _jsxs(Stack, {
@@ -232,7 +324,7 @@ const EMPTY = {
232
324
  getOptionLabel: (option)=>option.name,
233
325
  isOptionEqualToValue: (option, value)=>option.code === value.code,
234
326
  onChange: (_event, next)=>{
235
- setServerIssues({});
327
+ clearIssues('countries');
236
328
  setForm((previous)=>_extends({}, previous, {
237
329
  allowedCountries: next.map((option)=>option.code)
238
330
  }));
@@ -257,33 +349,6 @@ const EMPTY = {
257
349
  ]
258
350
  })
259
351
  }),
260
- /*#__PURE__*/ _jsxs(Stack, {
261
- direction: {
262
- xs: 'column',
263
- sm: 'row'
264
- },
265
- spacing: 1,
266
- sx: {
267
- justifyContent: 'flex-end'
268
- },
269
- children: [
270
- /*#__PURE__*/ _jsx(Button, {
271
- variant: "text",
272
- disabled: !dirty || saving,
273
- onClick: ()=>{
274
- setServerIssues({});
275
- if (stored) setForm(_extends({}, stored));
276
- },
277
- children: "Discard changes"
278
- }),
279
- /*#__PURE__*/ _jsx(Button, {
280
- variant: "contained",
281
- disabled: !dirty || saving || issues.length > 0,
282
- onClick: ()=>void save(),
283
- children: saving ? 'Saving…' : 'Save'
284
- })
285
- ]
286
- }),
287
352
  /*#__PURE__*/ _jsx(OutreachDoNotContactDomainsCard, {
288
353
  orgId: orgId
289
354
  })
@@ -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, 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\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).\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(false)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n useEffect(() => {\n if (stored) setForm({ ...stored })\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 const dirty = stored\n ? !outreachComplianceSettingsEqual(stored, normalized)\n : false\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues({})\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const save = async () => {\n setSaving(true)\n try {\n const answer = await api.saveSettings(normalized)\n setServerIssues({})\n setForm({ ...answer.settings })\n loaded.reload()\n enqueueSnackbar(\n answer.changed ? 'Compliance settings saved.' : 'Nothing changed.',\n {\n variant: answer.changed ? 'success' : 'info',\n },\n )\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\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(false)\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 return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\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 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 setServerIssues({})\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 <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n variant=\"text\"\n disabled={!dirty || saving}\n onClick={() => {\n setServerIssues({})\n if (stored) setForm({ ...stored })\n }}\n >\n Discard changes\n </Button>\n <Button\n variant=\"contained\"\n disabled={!dirty || saving || issues.length > 0}\n onClick={() => void save()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n\n {/* Its own list, saved as it is edited (AGL-3244): no Save above it. */}\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","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","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","countries","stored","settings","normalized","issues","field","find","issue","message","dirty","footer","set","value","previous","save","answer","saveSettings","reload","changed","variant","error","length","Object","fromEntries","filter","map","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","spacing","header","help","anchor","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","next","renderValue","getItemProps","index","key","item","size","renderInput","params","direction","xs","sm","justifyContent","disabled","onClick","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 { 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,4 +1,4 @@
1
- import { type OutreachEnrollPreviewPerson } from '../model/outreach-api';
1
+ import { type OutreachEnrollGateway, type OutreachEnrollPreviewPerson } from '../model/outreach-api';
2
2
  import { type OutreachAttestationKind, type OutreachSequence } from '../model/outreach.types';
3
3
  import { type OutreachCuratedDraftState } from './curated-step-editor';
4
4
  import type { OutreachApi } from './use-outreach-api';
@@ -12,6 +12,12 @@ export interface OutreachEnrollDialogProps {
12
12
  uid: string | null;
13
13
  api: OutreachApi;
14
14
  }
15
+ /**
16
+ * Whether the gateway in front of this person refused the sender this week
17
+ * (AGL-3326): the person is shown, and left un-ticked, rather than blocked
18
+ * — the founder decides, and ticking them is the decision.
19
+ */
20
+ export declare function outreachGatewayRefusedThisWeek(gateway: OutreachEnrollGateway | null | undefined): boolean;
15
21
  /** Where one person's curated drafts stand (AGL-3324). */
16
22
  type Curation = {
17
23
  status: 'idle';
@@ -20,6 +20,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { HelpTip } from "@aglyn/shared-ui-jsx";
21
21
  import { Alert, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Paper, Stack, Tab, Tabs, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
22
22
  import { useMemo, useState } from "react";
23
+ import { isOutreachGatewayFronted, outreachGatewayChip } from "../engine/mail-gateway.js";
23
24
  import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
24
25
  import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
25
26
  import { outreachEmailStepLabel } from "./curate-step-dialog.js";
@@ -36,6 +37,20 @@ const STATUS_COLORS = {
36
37
  needs_confirmation: 'warning',
37
38
  blocked: 'default'
38
39
  };
40
+ /** The gateway chip's color by its tone (AGL-3326). */ const GATEWAY_TONE_COLORS = {
41
+ refused: 'error',
42
+ delivered: 'success',
43
+ neutral: 'default',
44
+ blocked: 'default'
45
+ };
46
+ /**
47
+ * Whether the gateway in front of this person refused the sender this week
48
+ * (AGL-3326): the person is shown, and left un-ticked, rather than blocked
49
+ * — the founder decides, and ticking them is the decision.
50
+ */ export function outreachGatewayRefusedThisWeek(gateway) {
51
+ var _ref;
52
+ return ((_ref = gateway == null ? void 0 : gateway.blocked7) != null ? _ref : 0) > 0;
53
+ }
39
54
  /** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied) {
40
55
  var _ref;
41
56
  return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).map(outreachDraftToOverride).filter((override)=>override !== null);
@@ -82,6 +97,7 @@ const STATUS_COLORS = {
82
97
  const [problem, setProblem] = useState(null);
83
98
  const [supplied, setSupplied] = useState({});
84
99
  const [emailPreview, setEmailPreview] = useState({});
100
+ const [stepTests, setStepTests] = useState({});
85
101
  const views = useOutreachSavedViews(props.open ? orgId : null, props.uid);
86
102
  const search = useOutreachContactSearch({
87
103
  orgId: props.open && tab === 'search' ? orgId : null,
@@ -103,6 +119,7 @@ const STATUS_COLORS = {
103
119
  setProblem(null);
104
120
  setSupplied({});
105
121
  setEmailPreview({});
122
+ setStepTests({});
106
123
  };
107
124
  const close = ()=>{
108
125
  reset();
@@ -123,7 +140,9 @@ const STATUS_COLORS = {
123
140
  setSupplied(Object.fromEntries(answer.people.map((person)=>[
124
141
  person.personId,
125
142
  {
126
- include: person.status !== 'blocked',
143
+ // A person whose gateway refused the sender this week starts
144
+ // un-ticked (AGL-3326); the chip beside them says why.
145
+ include: person.status !== 'blocked' && !outreachGatewayRefusedThisWeek(person.gateway),
127
146
  personalLine: '',
128
147
  attestations: []
129
148
  }
@@ -279,6 +298,47 @@ const STATUS_COLORS = {
279
298
  ]))
280
299
  });
281
300
  };
301
+ /**
302
+ * This person's first email, sent to the member as a test (AGL-3325):
303
+ * their record, the personal line as written so far and the curated copy
304
+ * they used (AGL-3324) fill it, and nothing is enrolled or stored for
305
+ * them.
306
+ */ const sendTest = async (person)=>{
307
+ setStepTests((previous)=>_extends({}, previous, {
308
+ [person.personId]: {
309
+ status: 'sending'
310
+ }
311
+ }));
312
+ try {
313
+ var _ref;
314
+ var _supplied_person_personId;
315
+ const overrides = usedOverrides(supplied[person.personId]);
316
+ const answer = await api.sendStepTest(_extends({
317
+ sequenceId: sequence.id
318
+ }, person.target === 'lead' && person.leadId ? {
319
+ leadId: person.leadId
320
+ } : {
321
+ contactId: person.contactId
322
+ }, {
323
+ personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
324
+ }, overrides.length ? {
325
+ stepOverrides: overrides
326
+ } : {}));
327
+ setStepTests((previous)=>_extends({}, previous, {
328
+ [person.personId]: {
329
+ status: 'sent',
330
+ answer
331
+ }
332
+ }));
333
+ } catch (error) {
334
+ setStepTests((previous)=>_extends({}, previous, {
335
+ [person.personId]: {
336
+ status: 'failed',
337
+ message: error.message
338
+ }
339
+ }));
340
+ }
341
+ };
282
342
  const title = `Enroll people in ${sequence.name}`;
283
343
  return /*#__PURE__*/ _jsxs(Dialog, {
284
344
  open: props.open,
@@ -544,6 +604,8 @@ const STATUS_COLORS = {
544
604
  onSupply: (next)=>supply(person.personId, next),
545
605
  emailPreview: emailPreview[person.personId],
546
606
  onPreviewEmail: ()=>void previewEmail(person),
607
+ stepTest: stepTests[person.personId],
608
+ onSendTest: ()=>void sendTest(person),
547
609
  onCurate: ()=>void curate(person),
548
610
  onWriteThemselves: ()=>writeThemselves(person),
549
611
  disabled: stage.kind === 'enrolling'
@@ -616,11 +678,29 @@ const STATUS_COLORS = {
616
678
  OutreachEnrollDialog.displayName = 'OutreachEnrollDialog';
617
679
  function summary(answer) {
618
680
  const count = (status)=>answer.people.filter((person)=>person.status === status).length;
619
- return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked`;
681
+ // The people behind a security gateway (AGL-3326): a Barracuda, a
682
+ // Proofpoint or a Mimecast refuses a sender for a whole company, and a
683
+ // LinkedIn note first is the cheaper first touch there.
684
+ const fronted = answer.people.filter((person)=>{
685
+ var _person_gateway;
686
+ return person.status !== 'blocked' && isOutreachGatewayFronted((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.gateway);
687
+ }).length;
688
+ return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked` + (fronted ? ` · ${fronted} gateway-fronted — LinkedIn-first?` : '');
689
+ }
690
+ /** The Check-people chip for one person's mail gateway (AGL-3326), or nothing. */ function GatewayChip(props) {
691
+ const chip = outreachGatewayChip(props.gateway);
692
+ if (!chip) return null;
693
+ return /*#__PURE__*/ _jsx(Chip, {
694
+ size: "small",
695
+ variant: chip.tone === 'neutral' ? 'outlined' : 'filled',
696
+ color: GATEWAY_TONE_COLORS[chip.tone],
697
+ label: chip.label,
698
+ "aria-label": `Mail gateway for ${props.name}: ${chip.label}`
699
+ });
620
700
  }
621
701
  function PersonRow(props) {
622
702
  var _ref, _ref1, _ref2, _ref3;
623
- var _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
703
+ var _person_gateway, _person_gateway1, _person_gateway2, _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
624
704
  const { person, supplied, disabled } = props;
625
705
  const name = person.name || person.email || person.personId;
626
706
  const line = (_ref = supplied == null ? void 0 : supplied.personalLine) != null ? _ref : '';
@@ -686,6 +766,10 @@ function PersonRow(props) {
686
766
  variant: "outlined",
687
767
  label: "Cold"
688
768
  }) : null,
769
+ /*#__PURE__*/ _jsx(GatewayChip, {
770
+ gateway: person.gateway,
771
+ name: name
772
+ }),
689
773
  /*#__PURE__*/ _jsx(Chip, {
690
774
  size: "small",
691
775
  label: STATUS_LABELS[person.status],
@@ -721,6 +805,11 @@ function PersonRow(props) {
721
805
  }),
722
806
  label: "Enroll this person"
723
807
  }),
808
+ (supplied == null ? void 0 : supplied.include) === false && outreachGatewayRefusedThisWeek(person.gateway) ? /*#__PURE__*/ _jsx(Typography, {
809
+ variant: "caption",
810
+ color: "text.secondary",
811
+ children: `Left un-ticked: their mail gateway refused ${((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.blocked7) === 1 ? 'an email' : `${(_person_gateway1 = person.gateway) == null ? void 0 : _person_gateway1.blocked7} emails`} from this organization this week. Tick them to enroll anyway${((_person_gateway2 = person.gateway) == null ? void 0 : _person_gateway2.hold) ? ' — the email will go, and it is your call' : ''}.`
812
+ }) : null,
724
813
  (supplied == null ? void 0 : supplied.include) === false ? null : /*#__PURE__*/ _jsxs(_Fragment, {
725
814
  children: [
726
815
  /*#__PURE__*/ _jsx(TextField, {
@@ -773,6 +862,12 @@ function PersonRow(props) {
773
862
  disabled: disabled,
774
863
  children: "Preview the first email"
775
864
  }),
865
+ /*#__PURE__*/ _jsx(Button, {
866
+ size: "small",
867
+ onClick: props.onSendTest,
868
+ disabled: disabled || ((_props_stepTest = props.stepTest) == null ? void 0 : _props_stepTest.status) === 'sending',
869
+ children: ((_props_stepTest1 = props.stepTest) == null ? void 0 : _props_stepTest1.status) === 'sending' ? 'Sending…' : 'Send me this as a test'
870
+ }),
776
871
  /*#__PURE__*/ _jsx(Button, {
777
872
  size: "small",
778
873
  onClick: props.onCurate,
@@ -781,6 +876,14 @@ function PersonRow(props) {
781
876
  })
782
877
  ]
783
878
  }),
879
+ ((_props_stepTest2 = props.stepTest) == null ? void 0 : _props_stepTest2.status) === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
880
+ severity: "error",
881
+ children: props.stepTest.message
882
+ }) : null,
883
+ ((_props_stepTest3 = props.stepTest) == null ? void 0 : _props_stepTest3.status) === 'sent' ? /*#__PURE__*/ _jsx(Alert, {
884
+ severity: "success",
885
+ children: `Sent to ${props.stepTest.answer.sentTo} as “${props.stepTest.answer.subject}”. ${name} was not enrolled.`
886
+ }) : null,
784
887
  curation.status === 'drafting' ? /*#__PURE__*/ _jsx(OutreachLoading, {
785
888
  label: "Drafting their emails…"
786
889
  }) : null,