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

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 (112) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/compliance-section.d.ts +5 -0
  3. package/src/lib/components/compliance-section.js +302 -180
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.d.ts +12 -2
  6. package/src/lib/components/enroll-dialog.js +72 -18
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/enrollment-detail.d.ts +5 -1
  9. package/src/lib/components/enrollment-detail.js +14 -8
  10. package/src/lib/components/enrollment-detail.js.map +1 -1
  11. package/src/lib/components/outreach-console-sections.d.ts +1 -1
  12. package/src/lib/components/outreach-console-sections.js +1 -1
  13. package/src/lib/components/outreach-console-sections.js.map +1 -1
  14. package/src/lib/components/sequence-editor.d.ts +13 -0
  15. package/src/lib/components/sequence-editor.js +586 -335
  16. package/src/lib/components/sequence-editor.js.map +1 -1
  17. package/src/lib/components/sequence-preview.js +25 -10
  18. package/src/lib/components/sequence-preview.js.map +1 -1
  19. package/src/lib/components/sequence-step-card.d.ts +10 -0
  20. package/src/lib/components/sequence-step-card.js +320 -202
  21. package/src/lib/components/sequence-step-card.js.map +1 -1
  22. package/src/lib/components/use-outreach-api.d.ts +2 -1
  23. package/src/lib/components/use-outreach-api.js +5 -3
  24. package/src/lib/components/use-outreach-api.js.map +1 -1
  25. package/src/lib/engine/compose.d.ts +1 -1
  26. package/src/lib/engine/compose.js +10 -6
  27. package/src/lib/engine/compose.js.map +1 -1
  28. package/src/lib/engine/delivery-status.js +1 -1
  29. package/src/lib/engine/delivery-status.js.map +1 -1
  30. package/src/lib/engine/enrollment-activity.d.ts +9 -1
  31. package/src/lib/engine/enrollment-activity.js +15 -2
  32. package/src/lib/engine/enrollment-activity.js.map +1 -1
  33. package/src/lib/engine/enrollment-state.d.ts +33 -2
  34. package/src/lib/engine/enrollment-state.js +68 -9
  35. package/src/lib/engine/enrollment-state.js.map +1 -1
  36. package/src/lib/engine/gates.d.ts +2 -2
  37. package/src/lib/engine/gates.js.map +1 -1
  38. package/src/lib/engine/opt-out-intent.js +1 -1
  39. package/src/lib/engine/opt-out-intent.js.map +1 -1
  40. package/src/lib/engine/sending-capacity.d.ts +1 -1
  41. package/src/lib/engine/sending-capacity.js +1 -1
  42. package/src/lib/engine/sending-capacity.js.map +1 -1
  43. package/src/lib/engine/sequence-validation.d.ts +20 -3
  44. package/src/lib/engine/sequence-validation.js +32 -6
  45. package/src/lib/engine/sequence-validation.js.map +1 -1
  46. package/src/lib/engine/thread-classification.js +3 -3
  47. package/src/lib/engine/thread-classification.js.map +1 -1
  48. package/src/lib/enrollment/enroll-people.js +6 -4
  49. package/src/lib/enrollment/enroll-people.js.map +1 -1
  50. package/src/lib/enrollment/gate-lookups.js +5 -4
  51. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  52. package/src/lib/enrollment/step-overrides.d.ts +4 -2
  53. package/src/lib/enrollment/step-overrides.js +11 -4
  54. package/src/lib/enrollment/step-overrides.js.map +1 -1
  55. package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
  56. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  57. package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
  58. package/src/lib/mailboxes/mailbox-routes.js +10 -7
  59. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  60. package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
  61. package/src/lib/mailboxes/mailbox-settings.js +1 -1
  62. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  63. package/src/lib/model/enrollment-timeline.d.ts +1 -1
  64. package/src/lib/model/enrollment-timeline.js +22 -3
  65. package/src/lib/model/enrollment-timeline.js.map +1 -1
  66. package/src/lib/model/outreach-api.d.ts +19 -1
  67. package/src/lib/model/outreach-api.js.map +1 -1
  68. package/src/lib/model/outreach.types.d.ts +26 -1
  69. package/src/lib/model/outreach.types.js.map +1 -1
  70. package/src/lib/model/sales-topic.d.ts +27 -0
  71. package/src/lib/model/sales-topic.js +27 -0
  72. package/src/lib/model/sales-topic.js.map +1 -0
  73. package/src/lib/model/sequence-draft.d.ts +1 -1
  74. package/src/lib/model/sequence-draft.js +2 -2
  75. package/src/lib/model/sequence-draft.js.map +1 -1
  76. package/src/lib/model/step-preview.d.ts +2 -0
  77. package/src/lib/model/step-preview.js +11 -7
  78. package/src/lib/model/step-preview.js.map +1 -1
  79. package/src/lib/model/stored-records.js +28 -2
  80. package/src/lib/model/stored-records.js.map +1 -1
  81. package/src/lib/plugin.js +1 -1
  82. package/src/lib/plugin.js.map +1 -1
  83. package/src/lib/routes/curate-routes.js +7 -7
  84. package/src/lib/routes/curate-routes.js.map +1 -1
  85. package/src/lib/routes/enroll-routes.d.ts +6 -0
  86. package/src/lib/routes/enroll-routes.js +31 -19
  87. package/src/lib/routes/enroll-routes.js.map +1 -1
  88. package/src/lib/routes/preview-routes.js +14 -5
  89. package/src/lib/routes/preview-routes.js.map +1 -1
  90. package/src/lib/routes/sequence-routes.js +9 -9
  91. package/src/lib/routes/sequence-routes.js.map +1 -1
  92. package/src/lib/routes/step-test-routes.js +3 -3
  93. package/src/lib/routes/step-test-routes.js.map +1 -1
  94. package/src/lib/runtime/campaign-credit.js +2 -2
  95. package/src/lib/runtime/campaign-credit.js.map +1 -1
  96. package/src/lib/runtime/campaign-detach.js +3 -2
  97. package/src/lib/runtime/campaign-detach.js.map +1 -1
  98. package/src/lib/runtime/lead-records.d.ts +6 -1
  99. package/src/lib/runtime/lead-records.js +25 -6
  100. package/src/lib/runtime/lead-records.js.map +1 -1
  101. package/src/lib/runtime/platform-runtime-deps.js +3 -3
  102. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  103. package/src/lib/runtime/send-job.js +20 -4
  104. package/src/lib/runtime/send-job.js.map +1 -1
  105. package/src/lib/runtime/sync-job.js +3 -3
  106. package/src/lib/runtime/sync-job.js.map +1 -1
  107. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  108. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  109. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  110. package/src/lib/transport/gmail-errors.d.ts +27 -0
  111. package/src/lib/transport/gmail-errors.js +56 -0
  112. 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.220",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,26 +25,26 @@
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.220",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.220",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.220",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.220",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.220",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.220",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.220",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.220",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.220",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.220",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.220",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.220",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.220",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@mui/material": "^9.3.1",
45
45
  "firebase": "^12.18.0",
46
46
  "firebase-admin": "^14.3.0",
47
- "next": "16.3.3",
47
+ "next": "^16.3.3",
48
48
  "react": "^19.2.8"
49
49
  },
50
50
  "sideEffects": false,
@@ -15,6 +15,11 @@ export declare const OUTREACH_POSTAL_ADDRESS_HELP: string;
15
15
  * `composeOutreachFooter`, so the page shows the lines every email will end
16
16
  * with — or, while the legal name or the address is empty, the sentence
17
17
  * that explains why nothing can be sent and no sequence activated.
18
+ *
19
+ * Both settings cards are drawn from the one form, and each is memoized and
20
+ * handed only what it shows, through handlers that keep their identity
21
+ * (AGL-3423): a letter typed into the sender identity redraws its field and
22
+ * the footer, not the other fields, the countries or the domain list.
18
23
  */
19
24
  export declare function OutreachComplianceSection(props: OutreachComplianceSectionProps): import("react").JSX.Element;
20
25
  export declare namespace OutreachComplianceSection {
@@ -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,219 @@ 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
+ });
112
+ /** A text field that redraws only when what it shows changes (AGL-3423). */ const MemoTextField = /*#__PURE__*/ memo(TextField);
113
+ /** Its own list and listener: nothing typed in the cards above it reaches it. */ const MemoDoNotContactDomainsCard = /*#__PURE__*/ memo(OutreachDoNotContactDomainsCard);
114
+ const LEGAL_NAME_SLOT_PROPS = {
115
+ htmlInput: {
116
+ maxLength: OUTREACH_LEGAL_NAME_MAX + 20
117
+ }
118
+ };
119
+ const BRAND_NAME_SLOT_PROPS = {
120
+ htmlInput: {
121
+ maxLength: OUTREACH_BRAND_NAME_MAX + 20
122
+ }
123
+ };
124
+ const POSTAL_ADDRESS_SLOT_PROPS = {
125
+ htmlInput: {
126
+ maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40
127
+ }
128
+ };
129
+ const FOOTER_PAPER = {
130
+ p: 1.5
131
+ };
132
+ const FOOTER_TEXT = {
133
+ whiteSpace: 'pre-wrap',
134
+ wordBreak: 'break-word'
135
+ };
136
+ /** A settings card's header actions: discard its edits, or save its own fields. */ const ComplianceCardActions = /*#__PURE__*/ memo(function ComplianceCardActions(props) {
137
+ const { card, dirty, saving, onDiscard, onSave } = props;
138
+ return /*#__PURE__*/ _jsxs(Stack, {
139
+ direction: "row",
140
+ spacing: 1,
141
+ children: [
142
+ /*#__PURE__*/ _jsx(Button, {
143
+ size: "small",
144
+ variant: "text",
145
+ disabled: !dirty || saving !== null,
146
+ onClick: ()=>onDiscard(card),
147
+ children: "Discard changes"
148
+ }),
149
+ /*#__PURE__*/ _jsx(Button, {
150
+ size: "small",
151
+ variant: "contained",
152
+ disabled: !dirty || saving !== null || props.blocked,
153
+ onClick: ()=>onSave(card),
154
+ children: saving === card ? 'Saving…' : 'Save'
155
+ })
156
+ ]
157
+ });
158
+ });
159
+ /**
160
+ * Who every email says sent it, and the footer that says it. Each field is
161
+ * memoized with a handler that keeps its identity, so a letter typed into one
162
+ * redraws that field and the footer it changes, not the other two.
163
+ */ const ComplianceIdentityCard = /*#__PURE__*/ memo(function ComplianceIdentityCard(props) {
164
+ var _props_legalNameIssue, _props_brandNameIssue, _props_postalAddressIssue;
165
+ const { footer } = props;
166
+ return /*#__PURE__*/ _jsx(CardDisplay, {
167
+ header: "Sender identity",
168
+ help: pluginDocsHelp('sequences', {
169
+ anchor: '#compliance-settings'
170
+ }),
171
+ HeaderProps: {
172
+ action: /*#__PURE__*/ _jsx(ComplianceCardActions, {
173
+ card: "identity",
174
+ dirty: props.dirty,
175
+ blocked: props.blocked,
176
+ saving: props.saving,
177
+ onDiscard: props.onDiscard,
178
+ onSave: props.onSave
179
+ })
180
+ },
181
+ contentGutterX: true,
182
+ contentGutterY: true,
183
+ children: /*#__PURE__*/ _jsxs(Stack, {
184
+ spacing: 2,
185
+ children: [
186
+ /*#__PURE__*/ _jsx(Typography, {
187
+ variant: "body2",
188
+ color: "text.secondary",
189
+ children: 'Every email a sequence sends ends with a footer naming your organization, its postal ' + 'address, that the email is a sales email, and how to stop more of them. The law ' + 'requires these on commercial email, so every email gets the footer automatically.'
190
+ }),
191
+ /*#__PURE__*/ _jsx(MemoTextField, {
192
+ label: "Legal name",
193
+ value: props.legalName,
194
+ onChange: props.onLegalName,
195
+ error: Boolean(props.legalNameIssue),
196
+ helperText: (_props_legalNameIssue = props.legalNameIssue) != null ? _props_legalNameIssue : 'Your organization’s legal name, as the footer prints it.',
197
+ slotProps: LEGAL_NAME_SLOT_PROPS,
198
+ fullWidth: true
199
+ }),
200
+ /*#__PURE__*/ _jsx(MemoTextField, {
201
+ label: "Brand name",
202
+ value: props.brandName,
203
+ onChange: props.onBrandName,
204
+ error: Boolean(props.brandNameIssue),
205
+ helperText: (_props_brandNameIssue = props.brandNameIssue) != null ? _props_brandNameIssue : 'The name the solicitation sentence uses. Leave empty to use the legal name.',
206
+ slotProps: BRAND_NAME_SLOT_PROPS,
207
+ fullWidth: true
208
+ }),
209
+ /*#__PURE__*/ _jsx(MemoTextField, {
210
+ label: "Postal address",
211
+ value: props.postalAddress,
212
+ onChange: props.onPostalAddress,
213
+ error: Boolean(props.postalAddressIssue),
214
+ helperText: (_props_postalAddressIssue = props.postalAddressIssue) != null ? _props_postalAddressIssue : OUTREACH_POSTAL_ADDRESS_HELP,
215
+ slotProps: POSTAL_ADDRESS_SLOT_PROPS,
216
+ multiline: true,
217
+ minRows: 3,
218
+ fullWidth: true
219
+ }),
220
+ footer ? /*#__PURE__*/ _jsxs(Stack, {
221
+ spacing: 0.5,
222
+ children: [
223
+ /*#__PURE__*/ _jsx(Typography, {
224
+ variant: "subtitle2",
225
+ children: "Every email ends with"
226
+ }),
227
+ /*#__PURE__*/ _jsx(Paper, {
228
+ variant: "outlined",
229
+ sx: FOOTER_PAPER,
230
+ children: /*#__PURE__*/ _jsx(Typography, {
231
+ variant: "body2",
232
+ sx: FOOTER_TEXT,
233
+ "data-testid": "outreach-footer-preview",
234
+ children: footer
235
+ })
236
+ })
237
+ ]
238
+ }) : /*#__PURE__*/ _jsx(Alert, {
239
+ severity: "warning",
240
+ children: 'No sequence can be activated, and no email sent, until you add your organization’s legal ' + 'name and postal address.'
241
+ })
242
+ ]
243
+ })
244
+ });
245
+ });
246
+ /** The countries a sequence may send to at all. */ const ComplianceCountriesCard = /*#__PURE__*/ memo(function ComplianceCountriesCard(props) {
247
+ return /*#__PURE__*/ _jsx(CardDisplay, {
248
+ header: "Allowed countries",
249
+ help: pluginDocsHelp('sequences', {
250
+ anchor: '#allowed-countries'
251
+ }),
252
+ HeaderProps: {
253
+ action: /*#__PURE__*/ _jsx(ComplianceCardActions, {
254
+ card: "countries",
255
+ dirty: props.dirty,
256
+ blocked: props.blocked,
257
+ saving: props.saving,
258
+ onDiscard: props.onDiscard,
259
+ onSave: props.onSave
260
+ })
261
+ },
262
+ contentGutterX: true,
263
+ contentGutterY: true,
264
+ children: /*#__PURE__*/ _jsxs(Stack, {
265
+ spacing: 2,
266
+ children: [
267
+ /*#__PURE__*/ _jsx(Typography, {
268
+ variant: "body2",
269
+ color: "text.secondary",
270
+ 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.'
271
+ }),
272
+ /*#__PURE__*/ _jsx(ComplianceCountriesField, {
273
+ options: props.options,
274
+ value: props.value,
275
+ issue: props.issue,
276
+ onChange: props.onChange
277
+ })
278
+ ]
279
+ })
280
+ });
281
+ });
69
282
  /**
70
283
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
71
284
  * countries a sequence may send to at all, and — below the settings, with
@@ -77,8 +290,12 @@ function cardEqual(card, a, b) {
77
290
  * `composeOutreachFooter`, so the page shows the lines every email will end
78
291
  * with — or, while the legal name or the address is empty, the sentence
79
292
  * that explains why nothing can be sent and no sequence activated.
293
+ *
294
+ * Both settings cards are drawn from the one form, and each is memoized and
295
+ * handed only what it shows, through handlers that keep their identity
296
+ * (AGL-3423): a letter typed into the sender identity redraws its field and
297
+ * the footer, not the other fields, the countries or the domain list.
80
298
  */ export function OutreachComplianceSection(props) {
81
- var _fieldIssue, _fieldIssue1, _fieldIssue2;
82
299
  const { orgId } = props;
83
300
  const api = useOutreachApi(orgId);
84
301
  const loaded = useOutreachComplianceSettings(api, orgId);
@@ -118,16 +335,25 @@ function cardEqual(card, a, b) {
118
335
  const cardDirty = (card)=>stored ? !outreachComplianceSettingsEqual(stored, candidate(card).settings) : false;
119
336
  const cardBlocked = (card)=>candidate(card).issues.some((issue)=>CARDS[card].fields.includes(issue.field));
120
337
  const footer = composeOutreachFooter(normalized);
121
- const set = (field)=>(value)=>{
122
- setServerIssues((previous)=>{
123
- const next = _extends({}, previous);
124
- delete next[field];
125
- return next;
126
- });
127
- setForm((previous)=>_extends({}, previous, {
128
- [field]: value
129
- }));
130
- };
338
+ const set = useCallback((field, value)=>{
339
+ setServerIssues((previous)=>{
340
+ const next = _extends({}, previous);
341
+ delete next[field];
342
+ return next;
343
+ });
344
+ setForm((previous)=>_extends({}, previous, {
345
+ [field]: value
346
+ }));
347
+ }, []);
348
+ const setLegalName = useCallback((event)=>set('legalName', event.target.value), [
349
+ set
350
+ ]);
351
+ const setBrandName = useCallback((event)=>set('brandName', event.target.value), [
352
+ set
353
+ ]);
354
+ const setPostalAddress = useCallback((event)=>set('postalAddress', event.target.value), [
355
+ set
356
+ ]);
131
357
  const clearIssues = (card)=>setServerIssues((previous)=>{
132
358
  const next = _extends({}, previous);
133
359
  for (const field of CARDS[card].fields){
@@ -135,6 +361,31 @@ function cardEqual(card, a, b) {
135
361
  }
136
362
  return next;
137
363
  });
364
+ // Stable, for the memoized countries field: an edit clears the card's
365
+ // issues and replaces the list in whatever form it lands on.
366
+ const setCountries = useCallback((next)=>{
367
+ setServerIssues((previous)=>{
368
+ const cleared = _extends({}, previous);
369
+ for (const field of CARDS.countries.fields){
370
+ delete cleared[field];
371
+ }
372
+ return cleared;
373
+ });
374
+ setForm((previous)=>_extends({}, previous, {
375
+ allowedCountries: next.map((option)=>option.code)
376
+ }));
377
+ }, []);
378
+ const allowedCountries = form.allowedCountries;
379
+ const selectedCountries = useMemo(()=>allowedCountries.map((code)=>{
380
+ var _countries_find;
381
+ return (_countries_find = countries.find((option)=>option.code === code)) != null ? _countries_find : {
382
+ code,
383
+ name: outreachCountryLabel(code)
384
+ };
385
+ }), [
386
+ allowedCountries,
387
+ countries
388
+ ]);
138
389
  const discard = (card)=>{
139
390
  clearIssues(card);
140
391
  if (stored) setForm((previous)=>_extends({}, previous, pickCard(stored, card)));
@@ -164,6 +415,18 @@ function cardEqual(card, a, b) {
164
415
  setSaving(null);
165
416
  }
166
417
  };
418
+ // The discard and save as last drawn, behind handlers that keep their
419
+ // identity: both read the form and the stored settings of that render.
420
+ const drawn = useRef({
421
+ discard,
422
+ save
423
+ });
424
+ drawn.current = {
425
+ discard,
426
+ save
427
+ };
428
+ const onDiscard = useCallback((card)=>drawn.current.discard(card), []);
429
+ const onSave = useCallback((card)=>void drawn.current.save(card), []);
167
430
  if (loaded.status === 'loading' || !orgId && !stored) {
168
431
  return /*#__PURE__*/ _jsx(OutreachLoading, {
169
432
  label: "Loading compliance settings…"
@@ -177,179 +440,38 @@ function cardEqual(card, a, b) {
177
440
  onRetry: loaded.reload
178
441
  });
179
442
  }
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
- 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
- };
210
443
  return /*#__PURE__*/ _jsxs(Stack, {
211
444
  spacing: 2,
212
445
  children: [
213
- /*#__PURE__*/ _jsx(CardDisplay, {
214
- header: "Sender identity",
215
- help: pluginDocsHelp('sequences', {
216
- anchor: '#compliance-settings'
217
- }),
218
- HeaderProps: {
219
- action: cardActions('identity')
220
- },
221
- contentGutterX: true,
222
- contentGutterY: true,
223
- children: /*#__PURE__*/ _jsxs(Stack, {
224
- spacing: 2,
225
- children: [
226
- /*#__PURE__*/ _jsx(Typography, {
227
- variant: "body2",
228
- color: "text.secondary",
229
- children: 'Every email a sequence sends ends with a footer naming your organization, its postal ' + 'address, that the email is a sales email, and how to stop more of them. The law ' + 'requires these on commercial email, so every email gets the footer automatically.'
230
- }),
231
- /*#__PURE__*/ _jsx(TextField, {
232
- label: "Legal name",
233
- value: form.legalName,
234
- onChange: (event)=>set('legalName')(event.target.value),
235
- error: Boolean(fieldIssue('legalName')),
236
- helperText: (_fieldIssue = fieldIssue('legalName')) != null ? _fieldIssue : 'Your organization’s legal name, as the footer prints it.',
237
- slotProps: {
238
- htmlInput: {
239
- maxLength: OUTREACH_LEGAL_NAME_MAX + 20
240
- }
241
- },
242
- fullWidth: true
243
- }),
244
- /*#__PURE__*/ _jsx(TextField, {
245
- label: "Brand name",
246
- value: form.brandName,
247
- onChange: (event)=>set('brandName')(event.target.value),
248
- error: Boolean(fieldIssue('brandName')),
249
- helperText: (_fieldIssue1 = fieldIssue('brandName')) != null ? _fieldIssue1 : 'The name the solicitation sentence uses. Leave empty to use the legal name.',
250
- slotProps: {
251
- htmlInput: {
252
- maxLength: OUTREACH_BRAND_NAME_MAX + 20
253
- }
254
- },
255
- fullWidth: true
256
- }),
257
- /*#__PURE__*/ _jsx(TextField, {
258
- label: "Postal address",
259
- value: form.postalAddress,
260
- onChange: (event)=>set('postalAddress')(event.target.value),
261
- error: Boolean(fieldIssue('postalAddress')),
262
- helperText: (_fieldIssue2 = fieldIssue('postalAddress')) != null ? _fieldIssue2 : OUTREACH_POSTAL_ADDRESS_HELP,
263
- slotProps: {
264
- htmlInput: {
265
- maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40
266
- }
267
- },
268
- multiline: true,
269
- minRows: 3,
270
- fullWidth: true
271
- }),
272
- footer.footer ? /*#__PURE__*/ _jsxs(Stack, {
273
- spacing: 0.5,
274
- children: [
275
- /*#__PURE__*/ _jsx(Typography, {
276
- variant: "subtitle2",
277
- children: "Every email ends with"
278
- }),
279
- /*#__PURE__*/ _jsx(Paper, {
280
- variant: "outlined",
281
- sx: {
282
- p: 1.5
283
- },
284
- children: /*#__PURE__*/ _jsx(Typography, {
285
- variant: "body2",
286
- sx: {
287
- whiteSpace: 'pre-wrap',
288
- wordBreak: 'break-word'
289
- },
290
- "data-testid": "outreach-footer-preview",
291
- children: footer.footer
292
- })
293
- })
294
- ]
295
- }) : /*#__PURE__*/ _jsx(Alert, {
296
- severity: "warning",
297
- children: 'No sequence can be activated, and no email sent, until you add your organization’s legal ' + 'name and postal address.'
298
- })
299
- ]
300
- })
446
+ /*#__PURE__*/ _jsx(ComplianceIdentityCard, {
447
+ legalName: form.legalName,
448
+ brandName: form.brandName,
449
+ postalAddress: form.postalAddress,
450
+ legalNameIssue: fieldIssue('legalName'),
451
+ brandNameIssue: fieldIssue('brandName'),
452
+ postalAddressIssue: fieldIssue('postalAddress'),
453
+ footer: footer.footer,
454
+ onLegalName: setLegalName,
455
+ onBrandName: setBrandName,
456
+ onPostalAddress: setPostalAddress,
457
+ dirty: cardDirty('identity'),
458
+ blocked: cardBlocked('identity'),
459
+ saving: saving,
460
+ onDiscard: onDiscard,
461
+ onSave: onSave
301
462
  }),
302
- /*#__PURE__*/ _jsx(CardDisplay, {
303
- header: "Allowed countries",
304
- help: pluginDocsHelp('sequences', {
305
- anchor: '#allowed-countries'
306
- }),
307
- HeaderProps: {
308
- action: cardActions('countries')
309
- },
310
- contentGutterX: true,
311
- contentGutterY: true,
312
- children: /*#__PURE__*/ _jsxs(Stack, {
313
- spacing: 2,
314
- children: [
315
- /*#__PURE__*/ _jsx(Typography, {
316
- variant: "body2",
317
- color: "text.secondary",
318
- 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
- }),
320
- /*#__PURE__*/ _jsx(Autocomplete, {
321
- multiple: true,
322
- options: countries,
323
- 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
- }))
348
- })
349
- ]
350
- })
463
+ /*#__PURE__*/ _jsx(ComplianceCountriesCard, {
464
+ options: countries,
465
+ value: selectedCountries,
466
+ issue: fieldIssue('allowedCountries'),
467
+ onChange: setCountries,
468
+ dirty: cardDirty('countries'),
469
+ blocked: cardBlocked('countries'),
470
+ saving: saving,
471
+ onDiscard: onDiscard,
472
+ onSave: onSave
351
473
  }),
352
- /*#__PURE__*/ _jsx(OutreachDoNotContactDomainsCard, {
474
+ /*#__PURE__*/ _jsx(MemoDoNotContactDomainsCard, {
353
475
  orgId: orgId
354
476
  })
355
477
  ]