@aglyn/plugins-outreach 1.0.0-beta.200 → 1.0.0-beta.202
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 +14 -14
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +21 -0
- package/src/lib/components/enrollments-table.js +249 -210
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +70 -8
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +4 -1
- package/src/lib/components/sequences-section.js +12 -1
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +6 -0
- package/src/lib/model/enrollment-list-query.js +35 -1
- package/src/lib/model/enrollment-list-query.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +78 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +26 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import {
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
19
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
19
20
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
20
21
|
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
@@ -23,13 +24,13 @@ import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-j
|
|
|
23
24
|
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
24
25
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
25
26
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { readOutreachEngagement } from "../model/enrollment-engagement.js";
|
|
31
|
-
import { OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
|
|
27
|
+
import { Card, CardContent, Chip, Link, Stack, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
28
|
+
import { useMemo } from "react";
|
|
29
|
+
import { outreachClickCountLabel, outreachClickSummary } from "../model/enrollment-engagement.js";
|
|
30
|
+
import { OUTREACH_ENROLLMENT_CLICK_FIELDS, OUTREACH_ENROLLMENT_LIST_QUERY } from "../model/enrollment-list-query.js";
|
|
32
31
|
import { OUTREACH_ENROLLMENT_STATUSES, OUTREACH_STOP_REASONS, OUTREACH_TASK_KIND_LABELS } from "../model/outreach.types.js";
|
|
32
|
+
import { useOutreachEnrollmentActions } from "./enrollment-actions.js";
|
|
33
|
+
import { OUTREACH_CLICKED_FILTER_VALUE, OUTREACH_UNCLICKED_FILTER_VALUE } from "./enrollment-filter-params.js";
|
|
33
34
|
import { formatOutreachTime, OUTREACH_ENROLLMENT_STATUS_LABELS, OUTREACH_STOP_REASON_LABELS, OutreachEnrollmentStatusChip, OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
34
35
|
import { useOutreachEnrollmentList } from "./use-outreach-data.js";
|
|
35
36
|
/** The step an enrollment is on, as a person reads it. */ export function outreachCurrentStepLabel(enrollment, steps) {
|
|
@@ -48,43 +49,10 @@ import { useOutreachEnrollmentList } from "./use-outreach-data.js";
|
|
|
48
49
|
var _enrollment_lastSentAtMs, _enrollment_stoppedAtMs, _enrollment_createdAtMs;
|
|
49
50
|
return Math.max((_enrollment_lastSentAtMs = enrollment.lastSentAtMs) != null ? _enrollment_lastSentAtMs : 0, (_enrollment_stoppedAtMs = enrollment.stoppedAtMs) != null ? _enrollment_stoppedAtMs : 0, (_enrollment_createdAtMs = enrollment.createdAtMs) != null ? _enrollment_createdAtMs : 0);
|
|
50
51
|
}
|
|
51
|
-
/** What a member may do to an enrollment in the status it is in. */ function actionsFor(enrollment, steps) {
|
|
52
|
-
const actions = [];
|
|
53
|
-
// Rewrite the next email for this one person, while one is still to go.
|
|
54
|
-
if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate');
|
|
55
|
-
if (enrollment.status === 'active') actions.push('pause', 'stop');
|
|
56
|
-
if (enrollment.status === 'paused') actions.push('resume', 'stop');
|
|
57
|
-
if (enrollment.status !== 'opted_out') actions.push('do_not_contact');
|
|
58
|
-
return actions;
|
|
59
|
-
}
|
|
60
|
-
const ACTION_LABELS = {
|
|
61
|
-
curate: 'Curate next step',
|
|
62
|
-
pause: 'Pause',
|
|
63
|
-
resume: 'Resume',
|
|
64
|
-
stop: 'Stop',
|
|
65
|
-
do_not_contact: 'Mark do-not-contact'
|
|
66
|
-
};
|
|
67
52
|
/** Whether the step the enrollment is on goes out as this person's own copy (AGL-3324). */ export function outreachStepIsCurated(enrollment) {
|
|
68
53
|
var _enrollment_stepOverrides;
|
|
69
54
|
return Boolean((_enrollment_stepOverrides = enrollment.stepOverrides) == null ? void 0 : _enrollment_stepOverrides[String(enrollment.stepIndex)]);
|
|
70
55
|
}
|
|
71
|
-
/** What the snackbar says once an action lands. */ const DONE_LABELS = {
|
|
72
|
-
pause: 'Paused.',
|
|
73
|
-
resume: 'Resumed.',
|
|
74
|
-
stop: 'Stopped.'
|
|
75
|
-
};
|
|
76
|
-
/** The two actions that end something for good ask first. */ const CONFIRM = {
|
|
77
|
-
stop: {
|
|
78
|
-
title: 'Stop this enrollment?',
|
|
79
|
-
body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',
|
|
80
|
-
label: 'Stop'
|
|
81
|
-
},
|
|
82
|
-
do_not_contact: {
|
|
83
|
-
title: 'Mark do-not-contact?',
|
|
84
|
-
body: 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' + 'every send, and they are stopped in every sequence they are in.',
|
|
85
|
-
label: 'Mark do-not-contact'
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
56
|
/*
|
|
89
57
|
* What the enrollments grid's Filters panel offers (AGL-3317), each field on
|
|
90
58
|
* the table's Firestore query (AGL-3321, `OUTREACH_ENROLLMENT_LIST_QUERY`).
|
|
@@ -92,15 +60,23 @@ const ACTION_LABELS = {
|
|
|
92
60
|
* address is typed whole. `target` and `email` are no column of their own,
|
|
93
61
|
* so they are hidden columns the panel can still reach; the search box
|
|
94
62
|
* reads the person's name and address.
|
|
95
|
-
|
|
63
|
+
*
|
|
64
|
+
* A sequence that counts clicks adds two more (AGL-3332), on the same query:
|
|
65
|
+
* whether the person clicked, and a destination they followed — matched
|
|
66
|
+
* whole against the destinations stored on the enrollment, never by a piece
|
|
67
|
+
* of its address, so "followed /pricing" does not also list the people who
|
|
68
|
+
* followed "/pricing/enterprise".
|
|
69
|
+
*/ const BASE_FILTER_FIELDS = OUTREACH_ENROLLMENT_LIST_QUERY.fields.filter((field)=>!OUTREACH_ENROLLMENT_CLICK_FIELDS.includes(field.column));
|
|
96
70
|
const ENROLLMENT_FILTER_HEADERS = {
|
|
97
71
|
status: 'Status',
|
|
98
72
|
target: 'Enrolled as',
|
|
99
73
|
stopReason: 'Stop reason',
|
|
100
74
|
email: 'Email',
|
|
101
|
-
enrolled: 'Enrolled'
|
|
75
|
+
enrolled: 'Enrolled',
|
|
76
|
+
clicked: 'Clicked',
|
|
77
|
+
link: 'Link followed'
|
|
102
78
|
};
|
|
103
|
-
const
|
|
79
|
+
const BASE_FILTER_OPTIONS = {
|
|
104
80
|
status: OUTREACH_ENROLLMENT_STATUSES.map((status)=>({
|
|
105
81
|
value: status,
|
|
106
82
|
label: OUTREACH_ENROLLMENT_STATUS_LABELS[status]
|
|
@@ -120,18 +96,76 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
120
96
|
label: OUTREACH_STOP_REASON_LABELS[reason]
|
|
121
97
|
}))
|
|
122
98
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
99
|
+
const CLICKED_OPTIONS = [
|
|
100
|
+
{
|
|
101
|
+
value: OUTREACH_CLICKED_FILTER_VALUE,
|
|
102
|
+
label: 'Yes'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
value: OUTREACH_UNCLICKED_FILTER_VALUE,
|
|
106
|
+
label: 'No'
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
/** Columns a reader can show from the column chooser, off until they do (AGL-3332). */ const CLICK_DETAIL_COLUMNS = [
|
|
110
|
+
'linksFollowed',
|
|
111
|
+
'lastClick',
|
|
112
|
+
'scannerClicks'
|
|
113
|
+
];
|
|
114
|
+
/** The table's grammar, with the click fields when the sequence counts clicks. */ function filterFieldsFor(trackClicks) {
|
|
115
|
+
return trackClicks ? OUTREACH_ENROLLMENT_LIST_QUERY.fields : BASE_FILTER_FIELDS;
|
|
116
|
+
}
|
|
117
|
+
/** What the grid's cells and its export read off one enrollment. */ function filterRow(enrollment, steps, clicks) {
|
|
118
|
+
return {
|
|
119
|
+
enrollment,
|
|
120
|
+
status: enrollment.status,
|
|
121
|
+
target: enrollment.target === 'lead' ? 'lead' : 'contact',
|
|
122
|
+
contactName: enrollment.contactName,
|
|
123
|
+
email: enrollment.email,
|
|
124
|
+
step: outreachCurrentStepLabel(enrollment, steps),
|
|
125
|
+
stopReason: outreachStopLabel(enrollment),
|
|
126
|
+
clicked: clicks.clicks > 0 ? OUTREACH_CLICKED_FILTER_VALUE : OUTREACH_UNCLICKED_FILTER_VALUE,
|
|
127
|
+
link: clicks.followed
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The stop reason: its short label, with every word of it on hover and on the person's page. */ function StopReasonCell(props) {
|
|
131
|
+
var _OUTREACH_STOP_REASON_LABELS_enrollment_stopReason;
|
|
132
|
+
const { enrollment } = props;
|
|
133
|
+
if (!enrollment.stopReason) {
|
|
134
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
135
|
+
variant: "body2",
|
|
136
|
+
color: "text.secondary",
|
|
137
|
+
children: "—"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const label = (_OUTREACH_STOP_REASON_LABELS_enrollment_stopReason = OUTREACH_STOP_REASON_LABELS[enrollment.stopReason]) != null ? _OUTREACH_STOP_REASON_LABELS_enrollment_stopReason : '';
|
|
141
|
+
const text = /*#__PURE__*/ _jsx(Typography, {
|
|
142
|
+
variant: "body2",
|
|
143
|
+
sx: _extends({
|
|
144
|
+
whiteSpace: 'normal',
|
|
145
|
+
lineHeight: 1.3
|
|
146
|
+
}, enrollment.stopDetail ? {
|
|
147
|
+
textDecoration: 'underline dotted',
|
|
148
|
+
textUnderlineOffset: 3,
|
|
149
|
+
cursor: 'help'
|
|
150
|
+
} : {}),
|
|
151
|
+
children: label
|
|
152
|
+
});
|
|
153
|
+
return enrollment.stopDetail ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
154
|
+
title: outreachStopLabel(enrollment),
|
|
155
|
+
describeChild: true,
|
|
156
|
+
children: text
|
|
157
|
+
}) : text;
|
|
158
|
+
}
|
|
127
159
|
/** The table's columns; the person and the actions are drawn from the row's enrollment. */ function columns(rowActions, trackClicks) {
|
|
128
160
|
return [
|
|
129
161
|
{
|
|
130
162
|
field: 'person',
|
|
131
163
|
headerName: 'Person',
|
|
132
|
-
flex: 1,
|
|
133
|
-
minWidth:
|
|
164
|
+
flex: 1.2,
|
|
165
|
+
minWidth: 190,
|
|
134
166
|
sortable: false,
|
|
167
|
+
// What the export writes for a column that is drawn rather than read.
|
|
168
|
+
valueGetter: (_value, row)=>row.enrollment.contactName ? `${row.enrollment.contactName} <${row.enrollment.email}>` : row.enrollment.email,
|
|
135
169
|
renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
|
|
136
170
|
spacing: 0,
|
|
137
171
|
sx: {
|
|
@@ -172,7 +206,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
172
206
|
{
|
|
173
207
|
field: 'status',
|
|
174
208
|
headerName: 'Status',
|
|
175
|
-
width:
|
|
209
|
+
width: 110,
|
|
176
210
|
renderCell: ({ row })=>/*#__PURE__*/ _jsx(OutreachEnrollmentStatusChip, {
|
|
177
211
|
status: row.status
|
|
178
212
|
})
|
|
@@ -208,132 +242,172 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
208
242
|
{
|
|
209
243
|
field: 'nextSend',
|
|
210
244
|
headerName: 'Next send',
|
|
211
|
-
width:
|
|
245
|
+
width: 170,
|
|
212
246
|
sortable: false
|
|
213
247
|
},
|
|
214
248
|
{
|
|
215
249
|
field: 'lastActivity',
|
|
216
250
|
headerName: 'Last activity',
|
|
217
|
-
width:
|
|
251
|
+
width: 170,
|
|
218
252
|
sortable: false
|
|
219
253
|
},
|
|
220
254
|
...trackClicks ? [
|
|
221
255
|
{
|
|
222
256
|
field: 'clicks',
|
|
223
257
|
headerName: 'Clicks',
|
|
224
|
-
|
|
258
|
+
description: 'A person’s clicks, and how many different links they followed. Scanners are not counted.',
|
|
259
|
+
width: 110,
|
|
225
260
|
sortable: false,
|
|
226
|
-
renderCell: ({ row })
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}) : null
|
|
252
|
-
]
|
|
253
|
-
});
|
|
254
|
-
}
|
|
261
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
262
|
+
variant: "body2",
|
|
263
|
+
color: row.clicks === '—' ? 'text.secondary' : undefined,
|
|
264
|
+
children: row.clicks
|
|
265
|
+
})
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
field: 'linksFollowed',
|
|
269
|
+
headerName: 'Links followed',
|
|
270
|
+
flex: 1,
|
|
271
|
+
minWidth: 220,
|
|
272
|
+
sortable: false
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
field: 'lastClick',
|
|
276
|
+
headerName: 'Last click',
|
|
277
|
+
width: 170,
|
|
278
|
+
sortable: false
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
field: 'scannerClicks',
|
|
282
|
+
headerName: 'Scanner clicks',
|
|
283
|
+
description: 'Clicks a security scanner made, which are not counted as the person’s.',
|
|
284
|
+
width: 130,
|
|
285
|
+
sortable: false
|
|
255
286
|
}
|
|
256
287
|
] : [],
|
|
257
288
|
{
|
|
258
289
|
field: 'stopReason',
|
|
259
290
|
headerName: 'Stop reason',
|
|
260
291
|
flex: 1,
|
|
261
|
-
minWidth: 170
|
|
292
|
+
minWidth: 170,
|
|
293
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(StopReasonCell, {
|
|
294
|
+
enrollment: row.enrollment
|
|
295
|
+
})
|
|
262
296
|
},
|
|
263
297
|
listActionsColumn((row)=>rowActions(row.enrollment), {
|
|
264
298
|
width: 72
|
|
265
299
|
})
|
|
266
300
|
];
|
|
267
301
|
}
|
|
302
|
+
/** A plain click on a link, which the page handles itself; any other is the browser's. */ const plainClick = (event)=>!event.defaultPrevented && event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
268
303
|
/**
|
|
269
304
|
* A sequence's enrollments (AGL-2980): who is in it, where each person
|
|
270
305
|
* stands, when their next step goes out in the mailbox's timezone, and the
|
|
271
306
|
* four things a member can do about it — pause, resume, stop, and mark
|
|
272
307
|
* do-not-contact. A table on wider screens, a list of cards on a phone.
|
|
308
|
+
*
|
|
309
|
+
* Each row opens the person's own page (AGL-3332), which holds everything
|
|
310
|
+
* else known about them: so a cell says one thing, whole, and the detail
|
|
311
|
+
* behind it is a click away rather than cut off at the column's edge.
|
|
273
312
|
*/ export function OutreachEnrollmentsTable(props) {
|
|
274
|
-
|
|
275
|
-
const { steps, timeZone, api } = props;
|
|
313
|
+
const { steps, timeZone, api, onOpen, hrefFor } = props;
|
|
276
314
|
const trackClicks = props.trackClicks === true;
|
|
277
315
|
const theme = useTheme();
|
|
278
316
|
const narrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
317
|
+
const actions = useOutreachEnrollmentActions({
|
|
318
|
+
api,
|
|
319
|
+
steps
|
|
320
|
+
});
|
|
321
|
+
const fields = useMemo(()=>filterFieldsFor(trackClicks), [
|
|
322
|
+
trackClicks
|
|
323
|
+
]);
|
|
324
|
+
const gridFilter = useListGridFilter(_extends({
|
|
285
325
|
selectFields: [
|
|
286
326
|
'status',
|
|
287
327
|
'target',
|
|
288
|
-
'stopReason'
|
|
328
|
+
'stopReason',
|
|
329
|
+
'clicked',
|
|
330
|
+
'link'
|
|
289
331
|
]
|
|
290
|
-
}
|
|
332
|
+
}, props.clauses !== undefined ? {
|
|
333
|
+
clauses: props.clauses,
|
|
334
|
+
onChange: props.onClausesChange
|
|
335
|
+
} : {}));
|
|
291
336
|
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
292
337
|
/*
|
|
293
338
|
* One page of the sequence's enrollments, newest enrolled first, with
|
|
294
339
|
* every clause and the search word on the query (AGL-3321): a filter
|
|
295
340
|
* reaches the enrollment on page forty as surely as the one on page one,
|
|
296
|
-
* and nothing is matched over the rows that happen to be loaded.
|
|
341
|
+
* and nothing is matched over the rows that happen to be loaded. The
|
|
342
|
+
* click filters (AGL-3332) are on it too, answered by the `clicked` flag
|
|
343
|
+
* and the destinations the click route stores on each enrollment.
|
|
297
344
|
*/ const enrollments = useOutreachEnrollmentList(props.orgId, props.sequenceId, {
|
|
298
345
|
clauses: gridFilter.clauses,
|
|
299
346
|
search: gridFilter.searchWords
|
|
300
347
|
});
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
variant: 'error',
|
|
312
|
-
allowDuplicate: true
|
|
313
|
-
});
|
|
314
|
-
} finally{
|
|
315
|
-
setBusy(null);
|
|
316
|
-
}
|
|
348
|
+
const loadedData = enrollments.rows;
|
|
349
|
+
const summaries = useMemo(()=>new Map(loadedData.map((enrollment)=>[
|
|
350
|
+
enrollment.id,
|
|
351
|
+
outreachClickSummary(enrollment.engagement)
|
|
352
|
+
])), [
|
|
353
|
+
loadedData
|
|
354
|
+
]);
|
|
355
|
+
const clicksOf = (enrollment)=>{
|
|
356
|
+
var _summaries_get;
|
|
357
|
+
return (_summaries_get = summaries.get(enrollment.id)) != null ? _summaries_get : outreachClickSummary(enrollment.engagement);
|
|
317
358
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
359
|
+
/*
|
|
360
|
+
* The destinations the "Link followed" filter offers: the sequence's own
|
|
361
|
+
* rollup, and any a loaded person followed that it does not list — the
|
|
362
|
+
* rollup is capped, and a filter that could not name a destination a
|
|
363
|
+
* person followed would hide them from it.
|
|
364
|
+
*/ const linkOptions = useMemo(()=>{
|
|
365
|
+
var _props_linkOptions;
|
|
366
|
+
const options = new Map();
|
|
367
|
+
for (const option of (_props_linkOptions = props.linkOptions) != null ? _props_linkOptions : [])options.set(option.value, option.label);
|
|
368
|
+
for (const summary of summaries.values()){
|
|
369
|
+
for (const url of summary.followed)if (!options.has(url)) options.set(url, url);
|
|
329
370
|
}
|
|
330
|
-
|
|
371
|
+
return [
|
|
372
|
+
...options
|
|
373
|
+
].map(([value, label])=>({
|
|
374
|
+
value,
|
|
375
|
+
label
|
|
376
|
+
}));
|
|
377
|
+
}, [
|
|
378
|
+
props.linkOptions,
|
|
379
|
+
summaries
|
|
380
|
+
]);
|
|
381
|
+
const filterOptions = useMemo(()=>trackClicks ? _extends({}, BASE_FILTER_OPTIONS, {
|
|
382
|
+
clicked: CLICKED_OPTIONS,
|
|
383
|
+
link: linkOptions
|
|
384
|
+
}) : BASE_FILTER_OPTIONS, [
|
|
385
|
+
trackClicks,
|
|
386
|
+
linkOptions
|
|
387
|
+
]);
|
|
388
|
+
const hiddenColumns = useMemo(()=>_extends({}, hiddenFilterVisibility(fields, [
|
|
389
|
+
'status',
|
|
390
|
+
'stopReason'
|
|
391
|
+
]), trackClicks ? Object.fromEntries(CLICK_DETAIL_COLUMNS.map((column)=>[
|
|
392
|
+
column,
|
|
393
|
+
false
|
|
394
|
+
])) : {}), [
|
|
395
|
+
fields,
|
|
396
|
+
trackClicks
|
|
397
|
+
]);
|
|
398
|
+
/*
|
|
399
|
+
* A person who followed this destination before each click was recorded
|
|
400
|
+
* on its own, and another link after it, is known by their LAST link only
|
|
401
|
+
* — so a "Link followed" filter says it may miss them rather than
|
|
402
|
+
* implying the list is whole. Which people those are is exactly what the
|
|
403
|
+
* query cannot see, so the caveat stands whenever the filter does.
|
|
404
|
+
*/ const linkFilterMayMiss = trackClicks && gridFilter.clauses.some((clause)=>clause.field === 'link');
|
|
331
405
|
const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
|
|
332
|
-
fields:
|
|
406
|
+
fields: fields,
|
|
333
407
|
headers: ENROLLMENT_FILTER_HEADERS,
|
|
334
408
|
clauses: gridFilter.clauses,
|
|
335
409
|
onChange: gridFilter.setClauses,
|
|
336
|
-
options:
|
|
410
|
+
options: filterOptions
|
|
337
411
|
});
|
|
338
412
|
if (enrollments.status === 'loading' && !filtering) return /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
339
413
|
label: "Loading enrollments…"
|
|
@@ -362,15 +436,27 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
362
436
|
}
|
|
363
437
|
const rowActions = (enrollment)=>/*#__PURE__*/ _jsx(ListRowActions, {
|
|
364
438
|
label: enrollment.contactName || enrollment.email,
|
|
365
|
-
items:
|
|
366
|
-
key: action,
|
|
367
|
-
label: ACTION_LABELS[action],
|
|
368
|
-
onClick: ()=>choose(enrollment, action),
|
|
369
|
-
destructive: action === 'stop' || action === 'do_not_contact',
|
|
370
|
-
disabled: busy === enrollment.id
|
|
371
|
-
}))
|
|
439
|
+
items: actions.menuItems(enrollment)
|
|
372
440
|
});
|
|
373
441
|
const nextSend = (enrollment)=>enrollment.status === 'active' || enrollment.status === 'paused' ? formatOutreachTime(enrollment.nextDueAtMs, timeZone) : '—';
|
|
442
|
+
const personName = (enrollment)=>{
|
|
443
|
+
const name = enrollment.contactName || enrollment.email;
|
|
444
|
+
if (!hrefFor) return /*#__PURE__*/ _jsx(Typography, {
|
|
445
|
+
variant: "body2",
|
|
446
|
+
children: name
|
|
447
|
+
});
|
|
448
|
+
return /*#__PURE__*/ _jsx(Link, {
|
|
449
|
+
href: hrefFor(enrollment),
|
|
450
|
+
variant: "body2",
|
|
451
|
+
underline: "hover",
|
|
452
|
+
onClick: (event)=>{
|
|
453
|
+
if (!onOpen || !plainClick(event)) return;
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
onOpen(enrollment);
|
|
456
|
+
},
|
|
457
|
+
children: name
|
|
458
|
+
});
|
|
459
|
+
};
|
|
374
460
|
const person = (enrollment)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
375
461
|
spacing: 0,
|
|
376
462
|
children: [
|
|
@@ -381,10 +467,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
381
467
|
alignItems: 'center'
|
|
382
468
|
},
|
|
383
469
|
children: [
|
|
384
|
-
|
|
385
|
-
variant: "body2",
|
|
386
|
-
children: enrollment.contactName || enrollment.email
|
|
387
|
-
}),
|
|
470
|
+
personName(enrollment),
|
|
388
471
|
enrollment.target === 'lead' ? /*#__PURE__*/ _jsx(Chip, {
|
|
389
472
|
size: "small",
|
|
390
473
|
variant: "outlined",
|
|
@@ -406,12 +489,17 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
406
489
|
chips,
|
|
407
490
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
408
491
|
refused: listQueryRefusals(enrollments.plan.refused, {
|
|
409
|
-
fields
|
|
492
|
+
fields,
|
|
410
493
|
headers: ENROLLMENT_FILTER_HEADERS,
|
|
411
|
-
options:
|
|
494
|
+
options: filterOptions
|
|
412
495
|
}),
|
|
413
496
|
notices: enrollments.plan.notices
|
|
414
497
|
}),
|
|
498
|
+
linkFilterMayMiss ? /*#__PURE__*/ _jsx(Typography, {
|
|
499
|
+
variant: "caption",
|
|
500
|
+
color: "text.secondary",
|
|
501
|
+
children: "Clicks made before each one was recorded on its own kept only the person’s last link, so someone who followed this link and then another back then is not listed here."
|
|
502
|
+
}) : null,
|
|
415
503
|
narrow ? /*#__PURE__*/ _jsx(Stack, {
|
|
416
504
|
spacing: 1,
|
|
417
505
|
component: "ul",
|
|
@@ -421,7 +509,9 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
421
509
|
m: 0
|
|
422
510
|
},
|
|
423
511
|
"aria-label": "Enrollments",
|
|
424
|
-
children: pageRows.map((enrollment)
|
|
512
|
+
children: pageRows.map((enrollment)=>{
|
|
513
|
+
const clicks = clicksOf(enrollment);
|
|
514
|
+
return /*#__PURE__*/ _jsx(Card, {
|
|
425
515
|
variant: "outlined",
|
|
426
516
|
component: "li",
|
|
427
517
|
children: /*#__PURE__*/ _jsx(CardContent, {
|
|
@@ -473,10 +563,10 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
473
563
|
color: "text.secondary",
|
|
474
564
|
children: `Next send: ${nextSend(enrollment)} · Last activity: ${formatOutreachTime(lastActivityMs(enrollment), timeZone)}`
|
|
475
565
|
}),
|
|
476
|
-
trackClicks &&
|
|
566
|
+
trackClicks && clicks.clicks ? /*#__PURE__*/ _jsx(Typography, {
|
|
477
567
|
variant: "body2",
|
|
478
568
|
color: "text.secondary",
|
|
479
|
-
children: `
|
|
569
|
+
children: `Clicks: ${outreachClickCountLabel(clicks)}`
|
|
480
570
|
}) : null,
|
|
481
571
|
outreachStopLabel(enrollment) ? /*#__PURE__*/ _jsx(Typography, {
|
|
482
572
|
variant: "body2",
|
|
@@ -486,28 +576,34 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
486
576
|
]
|
|
487
577
|
})
|
|
488
578
|
})
|
|
489
|
-
}, enrollment.id)
|
|
579
|
+
}, enrollment.id);
|
|
580
|
+
})
|
|
490
581
|
}) : /*#__PURE__*/ _jsx(ListTable, {
|
|
491
582
|
"aria-label": "Enrollments",
|
|
492
|
-
columns: listFilterGridColumns(columns(rowActions, trackClicks),
|
|
583
|
+
columns: listFilterGridColumns(columns(rowActions, trackClicks), fields, filterOptions, ENROLLMENT_FILTER_HEADERS),
|
|
493
584
|
initialState: {
|
|
494
585
|
columns: {
|
|
495
|
-
columnVisibilityModel:
|
|
586
|
+
columnVisibilityModel: hiddenColumns
|
|
496
587
|
}
|
|
497
588
|
},
|
|
498
589
|
loading: enrollments.status === 'loading',
|
|
499
|
-
rows: pageRows.map((enrollment)=>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
email: enrollment.email,
|
|
506
|
-
step: outreachCurrentStepLabel(enrollment, steps),
|
|
590
|
+
rows: pageRows.map((enrollment)=>{
|
|
591
|
+
var _enrollment_engagement;
|
|
592
|
+
const clicks = clicksOf(enrollment);
|
|
593
|
+
return _extends({
|
|
594
|
+
$id: enrollment.id
|
|
595
|
+
}, filterRow(enrollment, steps, clicks), {
|
|
507
596
|
nextSend: nextSend(enrollment),
|
|
508
597
|
lastActivity: formatOutreachTime(lastActivityMs(enrollment), timeZone),
|
|
598
|
+
// What the export writes; the cells draw these their own way.
|
|
599
|
+
clicks: outreachClickCountLabel(clicks),
|
|
600
|
+
linksFollowed: clicks.followed.join(' ') || '—',
|
|
601
|
+
lastClick: clicks.clicks ? formatOutreachTime((_enrollment_engagement = enrollment.engagement) == null ? void 0 : _enrollment_engagement.lastClickAtMs, timeZone) : '—',
|
|
602
|
+
scannerClicks: clicks.machineClicks ? String(clicks.machineClicks) : '—',
|
|
509
603
|
stopReason: outreachStopLabel(enrollment) || '—'
|
|
510
|
-
})
|
|
604
|
+
});
|
|
605
|
+
}),
|
|
606
|
+
onOpen: onOpen ? (_id, row)=>onOpen(row.enrollment) : undefined,
|
|
511
607
|
/*
|
|
512
608
|
* The panel and the search are the grid's; every clause and the
|
|
513
609
|
* search word are on the table's query (AGL-3321). The grid
|
|
@@ -529,64 +625,7 @@ const ENROLLMENT_FILTER_OPTIONS = {
|
|
|
529
625
|
onPageChange: enrollments.setPage,
|
|
530
626
|
onPageSizeChange: enrollments.setPageSize
|
|
531
627
|
}),
|
|
532
|
-
|
|
533
|
-
enrollment: curating,
|
|
534
|
-
steps: steps,
|
|
535
|
-
api: api,
|
|
536
|
-
onClose: ()=>setCurating(null)
|
|
537
|
-
}),
|
|
538
|
-
/*#__PURE__*/ _jsx(Dialog, {
|
|
539
|
-
open: Boolean(confirming),
|
|
540
|
-
onClose: ()=>setConfirming(null),
|
|
541
|
-
fullWidth: true,
|
|
542
|
-
maxWidth: "xs",
|
|
543
|
-
children: confirming ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
544
|
-
children: [
|
|
545
|
-
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
546
|
-
children: (_CONFIRM_confirming_action = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action.title
|
|
547
|
-
}),
|
|
548
|
-
/*#__PURE__*/ _jsx(DialogContent, {
|
|
549
|
-
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
550
|
-
spacing: 2,
|
|
551
|
-
children: [
|
|
552
|
-
/*#__PURE__*/ _jsx(DialogContentText, {
|
|
553
|
-
children: (_CONFIRM_confirming_action1 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action1.body
|
|
554
|
-
}),
|
|
555
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
556
|
-
label: "Why (optional)",
|
|
557
|
-
value: detail,
|
|
558
|
-
onChange: (event)=>setDetail(event.target.value),
|
|
559
|
-
fullWidth: true,
|
|
560
|
-
slotProps: {
|
|
561
|
-
htmlInput: {
|
|
562
|
-
maxLength: 200
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
})
|
|
566
|
-
]
|
|
567
|
-
})
|
|
568
|
-
}),
|
|
569
|
-
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
570
|
-
children: [
|
|
571
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
572
|
-
onClick: ()=>setConfirming(null),
|
|
573
|
-
children: "Cancel"
|
|
574
|
-
}),
|
|
575
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
576
|
-
color: "error",
|
|
577
|
-
variant: "contained",
|
|
578
|
-
onClick: ()=>{
|
|
579
|
-
const { enrollment, action } = confirming;
|
|
580
|
-
setConfirming(null);
|
|
581
|
-
void act(enrollment, action, detail.trim() || undefined);
|
|
582
|
-
},
|
|
583
|
-
children: (_CONFIRM_confirming_action2 = CONFIRM[confirming.action]) == null ? void 0 : _CONFIRM_confirming_action2.label
|
|
584
|
-
})
|
|
585
|
-
]
|
|
586
|
-
})
|
|
587
|
-
]
|
|
588
|
-
}) : null
|
|
589
|
-
})
|
|
628
|
+
actions.dialogs
|
|
590
629
|
]
|
|
591
630
|
});
|
|
592
631
|
}
|