@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
@@ -21,11 +21,11 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
21
21
  import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
22
22
  import { mdiPlus } from "@aglyn/shared-data-mdi";
23
23
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
24
- import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
24
+ import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
25
25
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
- import { useOrgCampaigns, useUser } from "@aglyn/tenant-feature-instance";
26
+ import { useOrgContainerOptions, useUser } from "@aglyn/tenant-feature-instance";
27
27
  import { Alert, Autocomplete, Box, Button, Chip, FormControlLabel, Grid, Menu, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
28
- import { useEffect, useMemo, useState } from "react";
28
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
29
29
  import { isInThreadEmailStep, OUTREACH_SEQUENCE_NAME_MAX, validateOutreachSequence } from "../engine/sequence-validation.js";
30
30
  import { outreachCountryLabel } from "../model/compliance-settings.js";
31
31
  import { OUTREACH_MAX_EMAIL_STEPS, OUTREACH_MAX_STEPS, OUTREACH_TASK_KIND_LABELS, OUTREACH_TASK_KINDS } from "../model/outreach.types.js";
@@ -47,6 +47,406 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
47
47
  field: field === 'id' ? 'step' : field
48
48
  };
49
49
  }
50
+ /**
51
+ * The countries a sequence sends to, as chips in an Autocomplete.
52
+ *
53
+ * MEMOIZED, AND EVERY PROP IT TAKES IS STABLE WHILE THE COUNTRIES ARE
54
+ * (AGL-3423). A multiple Autocomplete hands its input a new chip array as
55
+ * `startAdornment` on every render, and MUI's `InputBase` copies that into
56
+ * its `FormControl` from a passive effect — a state update after every
57
+ * commit this field takes part in. React 19 counts a commit that leaves such
58
+ * an update pending as a nested update, and keystrokes the browser delivers
59
+ * back to back (a held key, fast typing on a busy page) commit one after
60
+ * another with nothing in between to clear the count: fifty-one of them and
61
+ * the next keystroke's own `setState` throws "Maximum update depth exceeded"
62
+ * (minified error #185), which is what typing a step's body reported. Kept
63
+ * out of the editor's other renders, the field commits only when the
64
+ * countries, their options or their issue change.
65
+ */ const OutreachCountriesField = /*#__PURE__*/ memo(function OutreachCountriesField(props) {
66
+ return /*#__PURE__*/ _jsx(Autocomplete, {
67
+ multiple: true,
68
+ options: props.options,
69
+ value: props.value,
70
+ getOptionLabel: outreachCountryLabel,
71
+ onChange: (_event, next)=>props.onChange(next),
72
+ disabled: props.disabled,
73
+ renderValue: (value, getItemProps)=>value.map((code, index)=>{
74
+ const _getItemProps = getItemProps({
75
+ index
76
+ }), { key } = _getItemProps, item = _object_without_properties_loose(_getItemProps, [
77
+ "key"
78
+ ]);
79
+ return /*#__PURE__*/ _jsx(Chip, _extends({
80
+ size: "small",
81
+ label: outreachCountryLabel(code)
82
+ }, item), key);
83
+ }),
84
+ renderInput: (params)=>{
85
+ var _props_issue;
86
+ return /*#__PURE__*/ _jsx(TextField, _extends({}, params, {
87
+ label: "Countries",
88
+ error: Boolean(props.issue),
89
+ helperText: (_props_issue = props.issue) != null ? _props_issue : 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'
90
+ }));
91
+ }
92
+ });
93
+ });
94
+ /**
95
+ * Each step's issues, the SAME record for a step while what it holds reads
96
+ * the same (AGL-3423), so a step a keystroke did not touch keeps its props.
97
+ */ function useStepIssues(issues, count) {
98
+ const held = useRef([]);
99
+ return useMemo(()=>{
100
+ const found = Array.from({
101
+ length: count
102
+ }, ()=>({}));
103
+ for (const issue of issues){
104
+ const key = stepIssueKey(issue.path);
105
+ const record = key ? found[key.index] : undefined;
106
+ if (key && record && !record[key.field]) record[key.field] = issue.message;
107
+ }
108
+ held.current = found.map((value, index)=>{
109
+ const key = JSON.stringify(value);
110
+ const before = held.current[index];
111
+ return (before == null ? void 0 : before.key) === key ? before : {
112
+ key,
113
+ value
114
+ };
115
+ });
116
+ return held.current.map((entry)=>entry.value);
117
+ }, [
118
+ issues,
119
+ count
120
+ ]);
121
+ }
122
+ /** A text field that redraws only when what it shows changes (AGL-3423). */ const MemoTextField = /*#__PURE__*/ memo(TextField);
123
+ /*
124
+ * The sections below are drawn from the one draft, and each redraws only
125
+ * when what it shows changes (AGL-3423).
126
+ */ const MemoMailboxPicker = /*#__PURE__*/ memo(OutreachSequenceMailboxPicker);
127
+ const MemoContainerPicker = /*#__PURE__*/ memo(ContainerPicker);
128
+ const MemoSendWindowFields = /*#__PURE__*/ memo(OutreachSendWindowFields);
129
+ const MemoSequencePreview = /*#__PURE__*/ memo(OutreachSequencePreview);
130
+ const NAME_SLOT_PROPS = {
131
+ htmlInput: {
132
+ maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20
133
+ }
134
+ };
135
+ const ADD_STEP_ROW = {
136
+ flexWrap: 'wrap',
137
+ rowGap: 1
138
+ };
139
+ const PREVIEW_COLUMN = {
140
+ position: {
141
+ md: 'sticky'
142
+ },
143
+ top: {
144
+ md: 16
145
+ }
146
+ };
147
+ const SequenceSiteField = /*#__PURE__*/ memo(function SequenceSiteField(props) {
148
+ const { hosts, value, issue } = props;
149
+ return /*#__PURE__*/ _jsx(TextField, {
150
+ select: true,
151
+ label: "Site",
152
+ value: hosts.some((host)=>host.id === value) ? value : '',
153
+ onChange: props.onChange,
154
+ disabled: props.disabled || !hosts.length,
155
+ error: Boolean(issue),
156
+ helperText: issue != null ? issue : 'The site whose contacts this sequence emails.',
157
+ fullWidth: true,
158
+ children: hosts.map((host)=>/*#__PURE__*/ _jsx(MenuItem, {
159
+ value: host.id,
160
+ children: host.name
161
+ }, host.id))
162
+ });
163
+ });
164
+ /** The sequence card's header actions: Cancel, and the save of the whole sequence. */ const SequenceSaveActions = /*#__PURE__*/ memo(function SequenceSaveActions(props) {
165
+ return /*#__PURE__*/ _jsxs(Stack, {
166
+ direction: "row",
167
+ spacing: 1,
168
+ children: [
169
+ props.onCancel ? /*#__PURE__*/ _jsx(Button, {
170
+ size: "small",
171
+ variant: "text",
172
+ onClick: props.onCancel,
173
+ disabled: props.saving,
174
+ children: "Cancel"
175
+ }) : null,
176
+ /*#__PURE__*/ _jsx(Button, {
177
+ size: "small",
178
+ variant: "contained",
179
+ onClick: props.onSave,
180
+ disabled: props.saving,
181
+ children: props.saving ? 'Saving…' : props.saveLabel
182
+ })
183
+ ]
184
+ });
185
+ });
186
+ /** The sequence's own card: its name, site, mailbox and campaigns. */ const SequenceCard = /*#__PURE__*/ memo(function SequenceCard(props) {
187
+ var _props_campaignsIssue;
188
+ const { archived } = props;
189
+ return /*#__PURE__*/ _jsx(CardDisplay, {
190
+ header: "Sequence",
191
+ help: pluginDocsHelp('sequences', {
192
+ anchor: '#build-a-sequence'
193
+ }),
194
+ // The whole sequence (steps and sending rules included) saves
195
+ // from its first card's header, where the console puts a card's
196
+ // actions (AGL-3333).
197
+ HeaderProps: {
198
+ action: archived ? undefined : /*#__PURE__*/ _jsx(SequenceSaveActions, {
199
+ saving: props.saving,
200
+ saveLabel: props.saveLabel,
201
+ onCancel: props.onCancel,
202
+ onSave: props.onSave
203
+ })
204
+ },
205
+ contentGutterX: true,
206
+ contentGutterY: true,
207
+ children: /*#__PURE__*/ _jsxs(Stack, {
208
+ spacing: 2,
209
+ children: [
210
+ /*#__PURE__*/ _jsx(MemoTextField, {
211
+ label: "Name",
212
+ value: props.name,
213
+ onChange: props.onName,
214
+ disabled: archived,
215
+ error: Boolean(props.nameIssue),
216
+ helperText: props.nameIssue,
217
+ slotProps: NAME_SLOT_PROPS,
218
+ fullWidth: true
219
+ }),
220
+ /*#__PURE__*/ _jsx(SequenceSiteField, {
221
+ hosts: props.hosts,
222
+ value: props.hostId,
223
+ issue: props.hostIssue,
224
+ disabled: archived,
225
+ onChange: props.onHost
226
+ }),
227
+ /*#__PURE__*/ _jsx(MemoMailboxPicker, {
228
+ orgId: props.orgId,
229
+ uid: props.uid,
230
+ mailboxes: props.mailboxes,
231
+ mailboxesPath: props.mailboxesPath,
232
+ value: props.mailboxId,
233
+ onChange: props.onMailbox,
234
+ error: props.mailboxIssue,
235
+ disabled: archived
236
+ }),
237
+ /*#__PURE__*/ _jsx(MemoContainerPicker, {
238
+ kind: "campaign",
239
+ options: props.campaignOptions,
240
+ value: props.campaignIds,
241
+ onChange: props.onCampaigns,
242
+ label: "Campaigns",
243
+ helperText: (_props_campaignsIssue = props.campaignsIssue) != null ? _props_campaignsIssue : 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.',
244
+ disabled: archived,
245
+ empty: props.noCampaigns,
246
+ emptyText: "There are no campaigns yet. Create one from Marketing to file this sequence under it."
247
+ })
248
+ ]
249
+ })
250
+ });
251
+ });
252
+ /**
253
+ * One step's card, bound to its position. Memoized, with handlers that keep
254
+ * their identity, so a keystroke in one step leaves the others undrawn.
255
+ */ const SequenceStepItem = /*#__PURE__*/ memo(function SequenceStepItem(props) {
256
+ const { index, canSendTest, onStepChange, onStepMove, onStepRemove, onStepTest } = props, card = _object_without_properties_loose(props, [
257
+ "index",
258
+ "canSendTest",
259
+ "onStepChange",
260
+ "onStepMove",
261
+ "onStepRemove",
262
+ "onStepTest"
263
+ ]);
264
+ const onChange = useCallback((step)=>onStepChange(index, step), [
265
+ index,
266
+ onStepChange
267
+ ]);
268
+ const onMove = useCallback((direction)=>onStepMove(index, direction), [
269
+ index,
270
+ onStepMove
271
+ ]);
272
+ const onRemove = useCallback(()=>onStepRemove(index), [
273
+ index,
274
+ onStepRemove
275
+ ]);
276
+ const onSendTest = useCallback(()=>onStepTest(index), [
277
+ index,
278
+ onStepTest
279
+ ]);
280
+ return /*#__PURE__*/ _jsx(OutreachStepCard, _extends({}, card, {
281
+ index: index,
282
+ onChange: onChange,
283
+ onMove: onMove,
284
+ onRemove: onRemove,
285
+ onSendTest: canSendTest ? onSendTest : undefined
286
+ }));
287
+ });
288
+ /** Adds an email, or a task of a kind picked from its menu. */ const SequenceAddStep = /*#__PURE__*/ memo(function SequenceAddStep(props) {
289
+ const { onAddTask } = props;
290
+ const [taskAnchor, setTaskAnchor] = useState(null);
291
+ return /*#__PURE__*/ _jsxs(Stack, {
292
+ direction: "row",
293
+ spacing: 1,
294
+ sx: ADD_STEP_ROW,
295
+ children: [
296
+ /*#__PURE__*/ _jsx(Button, {
297
+ variant: "outlined",
298
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
299
+ path: mdiPlus.path,
300
+ fontSize: "small"
301
+ }),
302
+ disabled: props.emailDisabled,
303
+ onClick: props.onAddEmail,
304
+ children: "Add email"
305
+ }),
306
+ /*#__PURE__*/ _jsx(Button, {
307
+ variant: "outlined",
308
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
309
+ path: mdiPlus.path,
310
+ fontSize: "small"
311
+ }),
312
+ disabled: props.taskDisabled,
313
+ onClick: (event)=>setTaskAnchor(event.currentTarget),
314
+ "aria-haspopup": "menu",
315
+ children: "Add task"
316
+ }),
317
+ /*#__PURE__*/ _jsx(Menu, {
318
+ anchorEl: taskAnchor,
319
+ open: Boolean(taskAnchor),
320
+ onClose: ()=>setTaskAnchor(null),
321
+ children: OUTREACH_TASK_KINDS.map((kind)=>/*#__PURE__*/ _jsx(MenuItem, {
322
+ onClick: ()=>{
323
+ setTaskAnchor(null);
324
+ onAddTask(kind);
325
+ },
326
+ children: OUTREACH_TASK_KIND_LABELS[kind]
327
+ }, kind))
328
+ })
329
+ ]
330
+ });
331
+ });
332
+ /** Who the sequence sends to, and when: its countries, switches and hours. */ const SequenceSendingCard = /*#__PURE__*/ memo(function SequenceSendingCard(props) {
333
+ const { settings, disabled, onSettings } = props;
334
+ const setAllowCustomers = useCallback((event)=>onSettings({
335
+ allowCustomers: event.target.checked
336
+ }), [
337
+ onSettings
338
+ ]);
339
+ const setTrackClicks = useCallback((event)=>onSettings({
340
+ trackClicks: event.target.checked
341
+ }), [
342
+ onSettings
343
+ ]);
344
+ const setCountOpens = useCallback((event)=>onSettings({
345
+ countOpens: event.target.checked
346
+ }), [
347
+ onSettings
348
+ ]);
349
+ const setListUnsubscribe = useCallback((event)=>onSettings({
350
+ listUnsubscribe: event.target.checked
351
+ }), [
352
+ onSettings
353
+ ]);
354
+ const setWindow = useCallback((window)=>onSettings({
355
+ window
356
+ }), [
357
+ onSettings
358
+ ]);
359
+ return /*#__PURE__*/ _jsx(CardDisplay, {
360
+ header: "Who it sends to, and when",
361
+ help: pluginDocsHelp('sequences', {
362
+ anchor: '#build-a-sequence',
363
+ excerpt: 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.'
364
+ }),
365
+ contentGutterX: true,
366
+ contentGutterY: true,
367
+ children: /*#__PURE__*/ _jsxs(Stack, {
368
+ spacing: 2,
369
+ children: [
370
+ /*#__PURE__*/ _jsx(OutreachCountriesField, {
371
+ options: props.countryOptions,
372
+ value: settings.allowedCountries,
373
+ onChange: props.onCountries,
374
+ issue: props.countriesIssue,
375
+ disabled: disabled
376
+ }),
377
+ /*#__PURE__*/ _jsx(FormControlLabel, {
378
+ control: /*#__PURE__*/ _jsx(Switch, {
379
+ checked: settings.allowCustomers,
380
+ onChange: setAllowCustomers,
381
+ disabled: disabled
382
+ }),
383
+ label: "Include customers"
384
+ }),
385
+ /*#__PURE__*/ _jsxs(Stack, {
386
+ spacing: 0,
387
+ children: [
388
+ /*#__PURE__*/ _jsx(FormControlLabel, {
389
+ control: /*#__PURE__*/ _jsx(Switch, {
390
+ checked: settings.trackClicks,
391
+ onChange: setTrackClicks,
392
+ disabled: disabled
393
+ }),
394
+ label: "Count link clicks"
395
+ }),
396
+ /*#__PURE__*/ _jsx(Typography, {
397
+ variant: "caption",
398
+ color: "text.secondary",
399
+ children: "Links in these emails are replaced with links of ours that forward to the same page, so clicks can be counted. The recipient sees the replacement, not your address. The emails stay plain text."
400
+ })
401
+ ]
402
+ }),
403
+ /*#__PURE__*/ _jsxs(Stack, {
404
+ spacing: 0,
405
+ children: [
406
+ /*#__PURE__*/ _jsx(FormControlLabel, {
407
+ control: /*#__PURE__*/ _jsx(Switch, {
408
+ checked: settings.countOpens,
409
+ onChange: setCountOpens,
410
+ disabled: disabled
411
+ }),
412
+ label: "Count opens"
413
+ }),
414
+ /*#__PURE__*/ _jsx(Typography, {
415
+ variant: "caption",
416
+ color: "text.secondary",
417
+ children: "Each email also goes out as HTML — the same words — with a tiny tracking image, so opens can be counted. The plain-text version is unchanged. It makes the email look less like one written by hand, and some spam filters score HTML with a remote image against you, so turn it on to measure a slice of your sends rather than for every sequence. Gmail opens count; Apple Mail loads every image on arrival, so its loads are counted separately."
418
+ })
419
+ ]
420
+ }),
421
+ /*#__PURE__*/ _jsxs(Stack, {
422
+ spacing: 0,
423
+ children: [
424
+ /*#__PURE__*/ _jsx(FormControlLabel, {
425
+ control: /*#__PURE__*/ _jsx(Switch, {
426
+ checked: settings.listUnsubscribe,
427
+ onChange: setListUnsubscribe,
428
+ disabled: disabled
429
+ }),
430
+ label: "Add a mail-client unsubscribe button"
431
+ }),
432
+ /*#__PURE__*/ _jsx(Typography, {
433
+ variant: "caption",
434
+ color: "text.secondary",
435
+ children: "Adds the List-Unsubscribe header, so the recipient’s mail app shows its own Unsubscribe button. It’s the easiest way out for them, and fewer people mark you as spam when they have it, but mail apps then label the email as coming from a mailing list. Off, the footer’s “reply ‘no’” line is the way out, and it’s always there."
436
+ })
437
+ ]
438
+ }),
439
+ /*#__PURE__*/ _jsx(MemoSendWindowFields, {
440
+ window: settings.window,
441
+ onChange: setWindow,
442
+ daysIssue: props.daysIssue,
443
+ hoursIssue: props.hoursIssue,
444
+ disabled: disabled
445
+ })
446
+ ]
447
+ })
448
+ });
449
+ });
50
450
  /**
51
451
  * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,
52
452
  * who it sends to and when, beside a live preview of each email.
@@ -57,8 +457,21 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
57
457
  * judge (whose mailbox, which countries the organization allows, what may
58
458
  * no longer change once people are enrolled) comes back from the save and
59
459
  * is shown in the same places.
460
+ *
461
+ * ## A keystroke redraws its own field (AGL-3423)
462
+ *
463
+ * The whole sequence is one draft, so every letter typed anywhere is a new
464
+ * draft, and drawing the editor from it inline redrew every field of every
465
+ * step, the settings and the preview with each one: 17 inputs and some 1,150
466
+ * components for one letter in a three-step sequence. That cost is what lets
467
+ * typed input queue, and queued keystrokes processed back to back are what
468
+ * drove the countries chips into React's #185. The sequence card, each step,
469
+ * the sending card and the preview are memoized and handed only what they
470
+ * show, through handlers that keep their identity; the handlers that wrote
471
+ * from the draft as drawn still do, through `drawn`. A letter now redraws the
472
+ * field it lands in, and the preview when it shows that step's email.
60
473
  */ export function OutreachSequenceEditor(props) {
61
- var _ref, _ref1, _props_mailboxes_mailboxes_find, _ref2, _issueAt, _issueAt1, _props_org, _ref3;
474
+ var _ref, _props_mailboxes_mailboxes_find, _ref1, _props_org, _ref2;
62
475
  var _settings_settings, _settings_settings1, _issues_find, _hosts_find;
63
476
  const { orgId, orgMount, sequence, settings } = props;
64
477
  const api = useOutreachApi(orgId);
@@ -69,9 +482,12 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
69
482
  const hosts = useMemo(()=>mountedHosts != null ? mountedHosts : [], [
70
483
  mountedHosts
71
484
  ]);
72
- const orgCountries = (_ref1 = (_settings_settings = settings.settings) == null ? void 0 : _settings_settings.allowedCountries) != null ? _ref1 : [
73
- 'US'
74
- ];
485
+ const allowedCountries = (_settings_settings = settings.settings) == null ? void 0 : _settings_settings.allowedCountries;
486
+ const orgCountries = useMemo(()=>allowedCountries != null ? allowedCountries : [
487
+ 'US'
488
+ ], [
489
+ allowedCountries
490
+ ]);
75
491
  const [draft, setDraft] = useState(()=>sequence ? outreachSequenceDraftOf(sequence) : emptyOutreachSequenceDraft({
76
492
  hostId: hosts.length === 1 ? hosts[0].id : '',
77
493
  allowedCountries: orgCountries
@@ -79,9 +495,11 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
79
495
  const [attempted, setAttempted] = useState(false);
80
496
  const [serverIssues, setServerIssues] = useState([]);
81
497
  const [saving, setSaving] = useState(false);
82
- const [taskAnchor, setTaskAnchor] = useState(null);
83
498
  // The step whose test-send dialog is open (AGL-3325), by index.
84
499
  const [testing, setTesting] = useState(null);
500
+ // The draft as last drawn, which the handlers that build on it read.
501
+ const drawn = useRef(draft);
502
+ drawn.current = draft;
85
503
  // A stored sequence the listener updated (a save, a status change) is the
86
504
  // new starting point.
87
505
  useEffect(()=>{
@@ -127,13 +545,19 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
127
545
  ]);
128
546
  const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid);
129
547
  const mailbox = (_props_mailboxes_mailboxes_find = props.mailboxes.mailboxes.find((entry)=>entry.id === draft.mailboxId)) != null ? _props_mailboxes_mailboxes_find : null;
548
+ const sender = useMemo(()=>mailbox ? {
549
+ name: mailbox.displayName,
550
+ email: mailbox.sendAs || mailbox.email
551
+ } : null, [
552
+ mailbox
553
+ ]);
130
554
  const archived = (sequence == null ? void 0 : sequence.status) === 'archived';
131
555
  /*
132
556
  * The org's campaigns, for the picker (AGL-3254): read while the editor
133
557
  * is open. A sequence is the organization's record and so is a campaign,
134
558
  * so the choice is every campaign in the org, whichever site the
135
559
  * sequence sends as.
136
- */ const campaigns = useOrgCampaigns(orgId, {
560
+ */ const campaigns = useOrgContainerOptions('campaign', orgId, {
137
561
  enabled: true
138
562
  });
139
563
  const issues = useMemo(()=>{
@@ -151,35 +575,96 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
151
575
  var _issues_find;
152
576
  return (_issues_find = issues.find((issue)=>issue.path === path)) == null ? void 0 : _issues_find.message;
153
577
  };
154
- const stepIssues = (index)=>{
155
- const found = {};
156
- for (const issue of issues){
157
- const key = stepIssueKey(issue.path);
158
- if ((key == null ? void 0 : key.index) === index && !found[key.field]) found[key.field] = issue.message;
159
- }
160
- return found;
161
- };
578
+ const stepIssues = useStepIssues(issues, draft.steps.length);
162
579
  const listIssue = issueAt('steps');
163
580
  const countriesIssue = (_issues_find = issues.find((issue)=>issue.path.startsWith('settings.allowedCountries'))) == null ? void 0 : _issues_find.message;
164
- const update = (next)=>{
165
- setServerIssues([]);
581
+ // An edit clears what the last save refused. The empty list stays the same
582
+ // list, so an edit with nothing to clear does not change that state.
583
+ const clearServerIssues = useCallback(()=>setServerIssues((previous)=>previous.length ? [] : previous), []);
584
+ const update = useCallback((next)=>{
585
+ clearServerIssues();
166
586
  setDraft((previous)=>_extends({}, previous, next));
167
- };
168
- const setStep = (index, step)=>update({
169
- steps: draft.steps.map((entry, position)=>position === index ? step : entry)
170
- });
171
- const moveStep = (index, direction)=>{
587
+ }, [
588
+ clearServerIssues
589
+ ]);
590
+ // Stable, for the memoized countries field: it reads the settings it
591
+ // replaces from the draft it is given rather than from this render's.
592
+ const setCountries = useCallback((next)=>{
593
+ clearServerIssues();
594
+ setDraft((previous)=>_extends({}, previous, {
595
+ settings: _extends({}, previous.settings, {
596
+ allowedCountries: next
597
+ })
598
+ }));
599
+ }, [
600
+ clearServerIssues
601
+ ]);
602
+ const setName = useCallback((event)=>update({
603
+ name: event.target.value
604
+ }), [
605
+ update
606
+ ]);
607
+ const setHost = useCallback((event)=>update({
608
+ hostId: event.target.value
609
+ }), [
610
+ update
611
+ ]);
612
+ const setMailbox = useCallback((mailboxId)=>update({
613
+ mailboxId
614
+ }), [
615
+ update
616
+ ]);
617
+ const setCampaigns = useCallback((campaignIds)=>update({
618
+ campaignIds
619
+ }), [
620
+ update
621
+ ]);
622
+ const setSettings = useCallback((patch)=>update({
623
+ settings: _extends({}, drawn.current.settings, patch)
624
+ }), [
625
+ update
626
+ ]);
627
+ const setStep = useCallback((index, step)=>update({
628
+ steps: drawn.current.steps.map((entry, position)=>position === index ? step : entry)
629
+ }), [
630
+ update
631
+ ]);
632
+ const moveStep = useCallback((index, direction)=>{
172
633
  const steps = [
173
- ...draft.steps
634
+ ...drawn.current.steps
174
635
  ];
175
636
  const [moved] = steps.splice(index, 1);
176
637
  steps.splice(index + direction, 0, moved);
177
638
  update({
178
639
  steps
179
640
  });
180
- };
641
+ }, [
642
+ update
643
+ ]);
644
+ const removeStep = useCallback((index)=>update({
645
+ steps: drawn.current.steps.filter((_entry, position)=>position !== index)
646
+ }), [
647
+ update
648
+ ]);
649
+ const addEmail = useCallback(()=>update({
650
+ steps: [
651
+ ...drawn.current.steps,
652
+ newOutreachEmailStep(drawn.current.steps)
653
+ ]
654
+ }), [
655
+ update
656
+ ]);
657
+ const addTask = useCallback((kind)=>update({
658
+ steps: [
659
+ ...drawn.current.steps,
660
+ newOutreachTaskStep(drawn.current.steps, kind)
661
+ ]
662
+ }), [
663
+ update
664
+ ]);
181
665
  const emailCount = draft.steps.filter((step)=>step.kind === 'email').length;
182
666
  const full = draft.steps.length >= OUTREACH_MAX_STEPS;
667
+ const firstEmail = draft.steps.findIndex((entry)=>entry.kind === 'email');
183
668
  const threadSubject = (index)=>{
184
669
  for(let position = index - 1; position >= 0; position -= 1){
185
670
  const step = draft.steps[position];
@@ -212,13 +697,21 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
212
697
  setSaving(false);
213
698
  }
214
699
  };
215
- const siteName = (_ref2 = (_hosts_find = hosts.find((host)=>host.id === draft.hostId)) == null ? void 0 : _hosts_find.name) != null ? _ref2 : '';
216
- const countryOptions = [
217
- ...new Set([
218
- ...orgCountries,
219
- ...draft.settings.allowedCountries
220
- ])
221
- ];
700
+ // The save as last drawn, behind a handler that keeps its identity.
701
+ const drawnSave = useRef(save);
702
+ drawnSave.current = save;
703
+ const onSave = useCallback(()=>void drawnSave.current(), []);
704
+ const siteName = (_ref1 = (_hosts_find = hosts.find((host)=>host.id === draft.hostId)) == null ? void 0 : _hosts_find.name) != null ? _ref1 : '';
705
+ const draftCountries = draft.settings.allowedCountries;
706
+ const countryOptions = useMemo(()=>[
707
+ ...new Set([
708
+ ...orgCountries,
709
+ ...draftCountries
710
+ ])
711
+ ], [
712
+ orgCountries,
713
+ draftCountries
714
+ ]);
222
715
  /*
223
716
  * A test sends the step as STORED (AGL-3325): the route reads the saved
224
717
  * sequence, so a new one has nothing to send yet and an edited one sends
@@ -242,310 +735,78 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
242
735
  severity: "info",
243
736
  children: "This sequence is archived, so it can’t be edited."
244
737
  }) : null,
245
- /*#__PURE__*/ _jsx(CardDisplay, {
246
- header: "Sequence",
247
- help: pluginDocsHelp('sequences', {
248
- anchor: '#build-a-sequence'
249
- }),
250
- // The whole sequence (steps and sending rules included) saves
251
- // from its first card's header, where the console puts a card's
252
- // actions (AGL-3333).
253
- HeaderProps: {
254
- action: archived ? undefined : /*#__PURE__*/ _jsxs(Stack, {
255
- direction: "row",
256
- spacing: 1,
257
- children: [
258
- props.onCancel ? /*#__PURE__*/ _jsx(Button, {
259
- size: "small",
260
- variant: "text",
261
- onClick: props.onCancel,
262
- disabled: saving,
263
- children: "Cancel"
264
- }) : null,
265
- /*#__PURE__*/ _jsx(Button, {
266
- size: "small",
267
- variant: "contained",
268
- onClick: ()=>void save(),
269
- disabled: saving,
270
- children: saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'
271
- })
272
- ]
273
- })
274
- },
275
- contentGutterX: true,
276
- contentGutterY: true,
277
- children: /*#__PURE__*/ _jsxs(Stack, {
278
- spacing: 2,
279
- children: [
280
- /*#__PURE__*/ _jsx(TextField, {
281
- label: "Name",
282
- value: draft.name,
283
- onChange: (event)=>update({
284
- name: event.target.value
285
- }),
286
- disabled: archived,
287
- error: Boolean(issueAt('name')),
288
- helperText: issueAt('name'),
289
- slotProps: {
290
- htmlInput: {
291
- maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20
292
- }
293
- },
294
- fullWidth: true
295
- }),
296
- /*#__PURE__*/ _jsx(TextField, {
297
- select: true,
298
- label: "Site",
299
- value: hosts.some((host)=>host.id === draft.hostId) ? draft.hostId : '',
300
- onChange: (event)=>update({
301
- hostId: event.target.value
302
- }),
303
- disabled: archived || !hosts.length,
304
- error: Boolean(issueAt('hostId')),
305
- helperText: (_issueAt = issueAt('hostId')) != null ? _issueAt : 'The site whose contacts this sequence emails.',
306
- fullWidth: true,
307
- children: hosts.map((host)=>/*#__PURE__*/ _jsx(MenuItem, {
308
- value: host.id,
309
- children: host.name
310
- }, host.id))
311
- }),
312
- /*#__PURE__*/ _jsx(OutreachSequenceMailboxPicker, {
313
- orgId: orgId,
314
- uid: uid,
315
- mailboxes: props.mailboxes,
316
- mailboxesPath: props.mailboxesPath,
317
- value: draft.mailboxId,
318
- onChange: (mailboxId)=>update({
319
- mailboxId
320
- }),
321
- error: issueAt('mailboxId'),
322
- disabled: archived
323
- }),
324
- /*#__PURE__*/ _jsx(CampaignPicker, {
325
- options: campaigns.options,
326
- value: draft.campaignIds,
327
- onChange: (campaignIds)=>update({
328
- campaignIds
329
- }),
330
- label: "Campaigns",
331
- helperText: (_issueAt1 = issueAt('campaignIds')) != null ? _issueAt1 : 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.',
332
- disabled: archived,
333
- empty: campaigns.ready && !campaigns.options.length,
334
- emptyText: "There are no campaigns yet. Create one from Marketing to file this sequence under it."
335
- })
336
- ]
337
- })
738
+ /*#__PURE__*/ _jsx(SequenceCard, {
739
+ orgId: orgId,
740
+ uid: uid,
741
+ name: draft.name,
742
+ hostId: draft.hostId,
743
+ mailboxId: draft.mailboxId,
744
+ campaignIds: draft.campaignIds,
745
+ hosts: hosts,
746
+ mailboxes: props.mailboxes,
747
+ mailboxesPath: props.mailboxesPath,
748
+ campaignOptions: campaigns.options,
749
+ noCampaigns: campaigns.ready && !campaigns.options.length,
750
+ nameIssue: issueAt('name'),
751
+ hostIssue: issueAt('hostId'),
752
+ mailboxIssue: issueAt('mailboxId'),
753
+ campaignsIssue: issueAt('campaignIds'),
754
+ archived: archived,
755
+ saving: saving,
756
+ saveLabel: sequence ? 'Save' : 'Create sequence',
757
+ onCancel: props.onCancel,
758
+ onSave: onSave,
759
+ onName: setName,
760
+ onHost: setHost,
761
+ onMailbox: setMailbox,
762
+ onCampaigns: setCampaigns
338
763
  }),
339
764
  listIssue ? /*#__PURE__*/ _jsx(Alert, {
340
765
  severity: "error",
341
766
  children: listIssue
342
767
  }) : null,
343
- draft.steps.map((step, index)=>/*#__PURE__*/ _jsx(OutreachStepCard, {
768
+ draft.steps.map((step, index)=>{
769
+ const inThread = isInThreadEmailStep(draft.steps, index);
770
+ return /*#__PURE__*/ _jsx(SequenceStepItem, {
344
771
  step: step,
345
772
  index: index,
346
773
  count: draft.steps.length,
347
- firstEmail: draft.steps.findIndex((entry)=>entry.kind === 'email') === index,
348
- inThread: isInThreadEmailStep(draft.steps, index),
349
- threadSubject: threadSubject(index),
774
+ firstEmail: firstEmail === index,
775
+ inThread: inThread,
776
+ // Read only by an email sent in the thread, so no other
777
+ // step is redrawn as the subject it replies to is typed.
778
+ threadSubject: inThread ? threadSubject(index) : '',
350
779
  templates: templates.data,
351
- issues: stepIssues(index),
780
+ issues: stepIssues[index],
352
781
  disabled: archived,
353
- onChange: (next)=>setStep(index, next),
354
- onMove: (direction)=>moveStep(index, direction),
355
- onRemove: ()=>update({
356
- steps: draft.steps.filter((_entry, position)=>position !== index)
357
- }),
358
- onSendTest: step.kind === 'email' && !archived ? ()=>setTesting(index) : undefined,
359
- sendTestDisabledReason: sendTestDisabledReason
360
- }, step.id || `step-${index}`)),
361
- /*#__PURE__*/ _jsxs(Stack, {
362
- direction: "row",
363
- spacing: 1,
364
- sx: {
365
- flexWrap: 'wrap',
366
- rowGap: 1
367
- },
368
- children: [
369
- /*#__PURE__*/ _jsx(Button, {
370
- variant: "outlined",
371
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
372
- path: mdiPlus.path,
373
- fontSize: "small"
374
- }),
375
- disabled: archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS,
376
- onClick: ()=>update({
377
- steps: [
378
- ...draft.steps,
379
- newOutreachEmailStep(draft.steps)
380
- ]
381
- }),
382
- children: "Add email"
383
- }),
384
- /*#__PURE__*/ _jsx(Button, {
385
- variant: "outlined",
386
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
387
- path: mdiPlus.path,
388
- fontSize: "small"
389
- }),
390
- disabled: archived || full,
391
- onClick: (event)=>setTaskAnchor(event.currentTarget),
392
- "aria-haspopup": "menu",
393
- children: "Add task"
394
- }),
395
- /*#__PURE__*/ _jsx(Menu, {
396
- anchorEl: taskAnchor,
397
- open: Boolean(taskAnchor),
398
- onClose: ()=>setTaskAnchor(null),
399
- children: OUTREACH_TASK_KINDS.map((kind)=>/*#__PURE__*/ _jsx(MenuItem, {
400
- onClick: ()=>{
401
- setTaskAnchor(null);
402
- update({
403
- steps: [
404
- ...draft.steps,
405
- newOutreachTaskStep(draft.steps, kind)
406
- ]
407
- });
408
- },
409
- children: OUTREACH_TASK_KIND_LABELS[kind]
410
- }, kind))
411
- })
412
- ]
782
+ canSendTest: step.kind === 'email' && !archived,
783
+ sendTestDisabledReason: sendTestDisabledReason,
784
+ onStepChange: setStep,
785
+ onStepMove: moveStep,
786
+ onStepRemove: removeStep,
787
+ onStepTest: setTesting
788
+ }, step.id || `step-${index}`);
789
+ }),
790
+ /*#__PURE__*/ _jsx(SequenceAddStep, {
791
+ emailDisabled: archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS,
792
+ taskDisabled: archived || full,
793
+ onAddEmail: addEmail,
794
+ onAddTask: addTask
413
795
  }),
414
796
  /*#__PURE__*/ _jsx(Typography, {
415
797
  variant: "caption",
416
798
  color: "text.secondary",
417
799
  children: `Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`
418
800
  }),
419
- /*#__PURE__*/ _jsx(CardDisplay, {
420
- header: "Who it sends to, and when",
421
- help: pluginDocsHelp('sequences', {
422
- anchor: '#build-a-sequence',
423
- excerpt: 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.'
424
- }),
425
- contentGutterX: true,
426
- contentGutterY: true,
427
- children: /*#__PURE__*/ _jsxs(Stack, {
428
- spacing: 2,
429
- children: [
430
- /*#__PURE__*/ _jsx(Autocomplete, {
431
- multiple: true,
432
- options: countryOptions,
433
- value: draft.settings.allowedCountries,
434
- getOptionLabel: outreachCountryLabel,
435
- onChange: (_event, next)=>update({
436
- settings: _extends({}, draft.settings, {
437
- allowedCountries: next
438
- })
439
- }),
440
- disabled: archived,
441
- renderValue: (value, getItemProps)=>value.map((code, index)=>{
442
- const _getItemProps = getItemProps({
443
- index
444
- }), { key } = _getItemProps, item = _object_without_properties_loose(_getItemProps, [
445
- "key"
446
- ]);
447
- return /*#__PURE__*/ _jsx(Chip, _extends({
448
- size: "small",
449
- label: outreachCountryLabel(code)
450
- }, item), key);
451
- }),
452
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
453
- label: "Countries",
454
- error: Boolean(countriesIssue),
455
- helperText: countriesIssue != null ? countriesIssue : 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'
456
- }))
457
- }),
458
- /*#__PURE__*/ _jsx(FormControlLabel, {
459
- control: /*#__PURE__*/ _jsx(Switch, {
460
- checked: draft.settings.allowCustomers,
461
- onChange: (event)=>update({
462
- settings: _extends({}, draft.settings, {
463
- allowCustomers: event.target.checked
464
- })
465
- }),
466
- disabled: archived
467
- }),
468
- label: "Include customers"
469
- }),
470
- /*#__PURE__*/ _jsxs(Stack, {
471
- spacing: 0,
472
- children: [
473
- /*#__PURE__*/ _jsx(FormControlLabel, {
474
- control: /*#__PURE__*/ _jsx(Switch, {
475
- checked: draft.settings.trackClicks,
476
- onChange: (event)=>update({
477
- settings: _extends({}, draft.settings, {
478
- trackClicks: event.target.checked
479
- })
480
- }),
481
- disabled: archived
482
- }),
483
- label: "Count link clicks"
484
- }),
485
- /*#__PURE__*/ _jsx(Typography, {
486
- variant: "caption",
487
- color: "text.secondary",
488
- children: "Links in these emails are replaced with links of ours that forward to the same page, so clicks can be counted. The recipient sees the replacement, not your address. The emails stay plain text."
489
- })
490
- ]
491
- }),
492
- /*#__PURE__*/ _jsxs(Stack, {
493
- spacing: 0,
494
- children: [
495
- /*#__PURE__*/ _jsx(FormControlLabel, {
496
- control: /*#__PURE__*/ _jsx(Switch, {
497
- checked: draft.settings.countOpens,
498
- onChange: (event)=>update({
499
- settings: _extends({}, draft.settings, {
500
- countOpens: event.target.checked
501
- })
502
- }),
503
- disabled: archived
504
- }),
505
- label: "Count opens"
506
- }),
507
- /*#__PURE__*/ _jsx(Typography, {
508
- variant: "caption",
509
- color: "text.secondary",
510
- children: "Each email also goes out as HTML — the same words — with a tiny tracking image, so opens can be counted. The plain-text version is unchanged. It makes the email look less like one written by hand, and some spam filters score HTML with a remote image against you, so turn it on to measure a slice of your sends rather than for every sequence. Gmail opens count; Apple Mail loads every image on arrival, so its loads are counted separately."
511
- })
512
- ]
513
- }),
514
- /*#__PURE__*/ _jsxs(Stack, {
515
- spacing: 0,
516
- children: [
517
- /*#__PURE__*/ _jsx(FormControlLabel, {
518
- control: /*#__PURE__*/ _jsx(Switch, {
519
- checked: draft.settings.listUnsubscribe,
520
- onChange: (event)=>update({
521
- settings: _extends({}, draft.settings, {
522
- listUnsubscribe: event.target.checked
523
- })
524
- }),
525
- disabled: archived
526
- }),
527
- label: "Add a mail-client unsubscribe button"
528
- }),
529
- /*#__PURE__*/ _jsx(Typography, {
530
- variant: "caption",
531
- color: "text.secondary",
532
- children: "Adds the List-Unsubscribe header, so the recipient’s mail app shows its own Unsubscribe button. It’s the easiest way out for them, and fewer people mark you as spam when they have it, but mail apps then label the email as coming from a mailing list. Off, the footer’s “reply ‘no’” line is the way out, and it’s always there."
533
- })
534
- ]
535
- }),
536
- /*#__PURE__*/ _jsx(OutreachSendWindowFields, {
537
- window: draft.settings.window,
538
- onChange: (window)=>update({
539
- settings: _extends({}, draft.settings, {
540
- window
541
- })
542
- }),
543
- daysIssue: issueAt('settings.window.days'),
544
- hoursIssue: issueAt('settings.window'),
545
- disabled: archived
546
- })
547
- ]
548
- })
801
+ /*#__PURE__*/ _jsx(SequenceSendingCard, {
802
+ settings: draft.settings,
803
+ countryOptions: countryOptions,
804
+ countriesIssue: countriesIssue,
805
+ daysIssue: issueAt('settings.window.days'),
806
+ hoursIssue: issueAt('settings.window'),
807
+ disabled: archived,
808
+ onCountries: setCountries,
809
+ onSettings: setSettings
549
810
  })
550
811
  ]
551
812
  })
@@ -557,7 +818,7 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
557
818
  sequence: sequence,
558
819
  stepIndex: testing,
559
820
  contactGroupId: sequence.hostId ? consentGroupForHost((_props_org = props.org) != null ? _props_org : null, sequence.hostId).groupId : '',
560
- selfEmail: (_ref3 = user == null ? void 0 : user.email) != null ? _ref3 : null,
821
+ selfEmail: (_ref2 = user == null ? void 0 : user.email) != null ? _ref2 : null,
561
822
  unsaved: unsaved,
562
823
  api: api
563
824
  }) : null,
@@ -567,23 +828,13 @@ import { useOutreachEmailTemplates } from "./use-outreach-crm.js";
567
828
  md: 5
568
829
  },
569
830
  children: /*#__PURE__*/ _jsx(Box, {
570
- sx: {
571
- position: {
572
- md: 'sticky'
573
- },
574
- top: {
575
- md: 16
576
- }
577
- },
578
- children: /*#__PURE__*/ _jsx(OutreachSequencePreview, {
831
+ sx: PREVIEW_COLUMN,
832
+ children: /*#__PURE__*/ _jsx(MemoSequencePreview, {
579
833
  steps: draft.steps,
580
834
  orgSettings: settings.settings,
581
835
  orgSettingsStatus: settings.status,
582
836
  templates: templates.data,
583
- sender: mailbox ? {
584
- name: mailbox.displayName,
585
- email: mailbox.sendAs || mailbox.email
586
- } : null,
837
+ sender: sender,
587
838
  siteName: siteName
588
839
  })
589
840
  })