@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.
- package/package.json +15 -15
- package/src/lib/components/compliance-section.d.ts +5 -0
- package/src/lib/components/compliance-section.js +302 -180
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.d.ts +12 -2
- package/src/lib/components/enroll-dialog.js +72 -18
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollment-detail.d.ts +5 -1
- package/src/lib/components/enrollment-detail.js +14 -8
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/outreach-console-sections.d.ts +1 -1
- package/src/lib/components/outreach-console-sections.js +1 -1
- package/src/lib/components/outreach-console-sections.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +13 -0
- package/src/lib/components/sequence-editor.js +586 -335
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-preview.js +25 -10
- package/src/lib/components/sequence-preview.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +10 -0
- package/src/lib/components/sequence-step-card.js +320 -202
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +2 -1
- package/src/lib/components/use-outreach-api.js +5 -3
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/engine/compose.d.ts +1 -1
- package/src/lib/engine/compose.js +10 -6
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/delivery-status.js +1 -1
- package/src/lib/engine/delivery-status.js.map +1 -1
- package/src/lib/engine/enrollment-activity.d.ts +9 -1
- package/src/lib/engine/enrollment-activity.js +15 -2
- package/src/lib/engine/enrollment-activity.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +33 -2
- package/src/lib/engine/enrollment-state.js +68 -9
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +2 -2
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/opt-out-intent.js +1 -1
- package/src/lib/engine/opt-out-intent.js.map +1 -1
- package/src/lib/engine/sending-capacity.d.ts +1 -1
- package/src/lib/engine/sending-capacity.js +1 -1
- package/src/lib/engine/sending-capacity.js.map +1 -1
- package/src/lib/engine/sequence-validation.d.ts +20 -3
- package/src/lib/engine/sequence-validation.js +32 -6
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/engine/thread-classification.js +3 -3
- package/src/lib/engine/thread-classification.js.map +1 -1
- package/src/lib/enrollment/enroll-people.js +6 -4
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +5 -4
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/step-overrides.d.ts +4 -2
- package/src/lib/enrollment/step-overrides.js +11 -4
- package/src/lib/enrollment/step-overrides.js.map +1 -1
- package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
- package/src/lib/mailboxes/mailbox-routes.js +10 -7
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
- package/src/lib/mailboxes/mailbox-settings.js +1 -1
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +1 -1
- package/src/lib/model/enrollment-timeline.js +22 -3
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +19 -1
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +26 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sales-topic.d.ts +27 -0
- package/src/lib/model/sales-topic.js +27 -0
- package/src/lib/model/sales-topic.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +1 -1
- package/src/lib/model/sequence-draft.js +2 -2
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +2 -0
- package/src/lib/model/step-preview.js +11 -7
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.js +28 -2
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +7 -7
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +6 -0
- package/src/lib/routes/enroll-routes.js +31 -19
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +14 -5
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +9 -9
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/routes/step-test-routes.js +3 -3
- package/src/lib/routes/step-test-routes.js.map +1 -1
- package/src/lib/runtime/campaign-credit.js +2 -2
- package/src/lib/runtime/campaign-credit.js.map +1 -1
- package/src/lib/runtime/campaign-detach.js +3 -2
- package/src/lib/runtime/campaign-detach.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +6 -1
- package/src/lib/runtime/lead-records.js +25 -6
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +3 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -4
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +3 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
- package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
- package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +27 -0
- package/src/lib/transport/gmail-errors.js +56 -0
- 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
|
|
24
|
+
import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
|
|
25
25
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
26
|
-
import {
|
|
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,
|
|
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
|
|
73
|
-
|
|
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 =
|
|
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 = (
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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)
|
|
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:
|
|
348
|
-
inThread:
|
|
349
|
-
|
|
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
|
|
780
|
+
issues: stepIssues[index],
|
|
352
781
|
disabled: archived,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
/*#__PURE__*/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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: (
|
|
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
|
-
|
|
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:
|
|
584
|
-
name: mailbox.displayName,
|
|
585
|
-
email: mailbox.sendAs || mailbox.email
|
|
586
|
-
} : null,
|
|
837
|
+
sender: sender,
|
|
587
838
|
siteName: siteName
|
|
588
839
|
})
|
|
589
840
|
})
|