@aglyn/plugins-outreach 1.0.0-beta.274 → 1.0.0-beta.275
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/README.md +7 -2
- package/package.json +14 -14
- package/src/lib/components/enrollment-actions.d.ts +11 -4
- package/src/lib/components/enrollment-actions.js +16 -0
- package/src/lib/components/enrollment-actions.js.map +1 -1
- package/src/lib/components/enrollment-detail.js +40 -13
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/mailbox-card.js +13 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/record-reply-button.d.ts +52 -0
- package/src/lib/components/record-reply-button.js +92 -0
- package/src/lib/components/record-reply-button.js.map +1 -0
- package/src/lib/components/reply-dialog.d.ts +66 -0
- package/src/lib/components/reply-dialog.js +353 -0
- package/src/lib/components/reply-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +10 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/manual-reply.d.ts +108 -0
- package/src/lib/engine/manual-reply.js +301 -0
- package/src/lib/engine/manual-reply.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +3 -1
- package/src/lib/model/enrollment-timeline.js +83 -7
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +66 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +85 -3
- package/src/lib/model/outreach.types.js +6 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +7 -1
- package/src/lib/model/stored-records.js +37 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +6 -0
- package/src/lib/routes/register-routes.js +18 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/reply-routes.d.ts +76 -0
- package/src/lib/routes/reply-routes.js +563 -0
- package/src/lib/routes/reply-routes.js.map +1 -0
- package/src/lib/runtime/sync-job.js +14 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/transport/send-message.d.ts +11 -0
- package/src/lib/transport/send-message.js +22 -0
- package/src/lib/transport/send-message.js.map +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,10 @@ Two halves, like the CRM plugin:
|
|
|
27
27
|
- **Console** (`registerOutreachConsole`, client barrel): the organization-level
|
|
28
28
|
Sequences hub at `/[orgSlug]/outreach/<section>`, declared through
|
|
29
29
|
`ConsoleExtension.orgNavItems`, behind `release_outreach`, the
|
|
30
|
-
`features.outreach` entitlement and the `outreach.use` permission.
|
|
30
|
+
`features.outreach` entitlement and the `outreach.use` permission. Behind
|
|
31
|
+
the same gates, one widget in a zone the CRM hosts: **Reply in thread from**
|
|
32
|
+
*the mailbox* in a contact's or a lead's header, before Send email
|
|
33
|
+
(`crmRecordEmailActions`, AGL-4020).
|
|
31
34
|
- **Console API** (`registerOutreachConsoleApi`, `@aglyn/plugins-outreach/server`):
|
|
32
35
|
handlers under the `outreach/` prefix, served by the console's
|
|
33
36
|
`/api/[...pluginApi]` dispatcher.
|
|
@@ -90,6 +93,7 @@ pure functions with no I/O, exported from the package root:
|
|
|
90
93
|
| `mailbox-notice` | the email a mailbox's owner gets when it pauses itself or needs reconnecting |
|
|
91
94
|
| `gateway-block` | whether a hard bounce is the recipient domain's gateway refusing the sender, rather than one unknown address |
|
|
92
95
|
| `do-not-contact-domain` | how a domain on the do-not-contact list is spelled |
|
|
96
|
+
| `manual-reply` | answering a person who wrote back (AGL-4020): what the enrollment keeps of their messages, the in-thread answer (`Re:`, `In-Reply-To`, `References`, their message quoted) and what still refuses it |
|
|
93
97
|
|
|
94
98
|
`engine/do-not-contact` derives the do-not-contact document id with
|
|
95
99
|
`node:crypto`, so it is not in the barrel: import
|
|
@@ -103,7 +107,8 @@ console alone — each run opens a rep's sealed grant:
|
|
|
103
107
|
| module | does |
|
|
104
108
|
| -- | -- |
|
|
105
109
|
| `runtime/send-job` | `outreach-send`: due enrollments claimed in a transaction that reserves the mailbox's day, gates asked again, the email composed — with the `List-Unsubscribe` header when the sequence turns it on (AGL-3296), and its links' `outreachLinks` documents written first when it tracks clicks (AGL-3297) — and sent, the step recorded and filed on the contact's timeline; task steps file the rep's task |
|
|
106
|
-
| `runtime/sync-job` | `outreach-sync`: replies, out-of-office answers, opt-outs and bounces read with `format=full` and classified by the engine; the mailbox's health evidence and its automatic pause |
|
|
110
|
+
| `runtime/sync-job` | `outreach-sync`: replies, out-of-office answers, opt-outs and bounces read with `format=full` and classified by the engine; what the person wrote kept on the enrollment (AGL-4020); the mailbox's health evidence and its automatic pause |
|
|
111
|
+
| `routes/reply-routes`, `components/reply-dialog` | **Reply in thread** (AGL-4020): `outreach/enrollments/thread` reads what the person wrote — from the mailbox, for a reply synced before replies were kept — and `outreach/enrollments/reply` sends a member's answer from the enrollment's mailbox in the same thread, kept as a `manual_reply` step record and on the CRM timeline, counted on `health.daily.<day>.manualReplies` and never against the cap |
|
|
107
112
|
| `runtime/unsubscribe-link`, `runtime/unsubscribe-route` | the signed one-click link (`GET` and RFC 8058 `POST /api/outreach/unsubscribe`), a recipient link that outlives a paused rollout, and the `+unsubscribe` mailto |
|
|
108
113
|
| `runtime/click-link`, `runtime/click-route` | click tracking: the short link every send carries (`GET /api/outreach/l/<id>`, resolved through the top-level `outreachLinks` document, AGL-3297) and the signed `GET /api/outreach/click?t=…` link older emails carry; both recipient links, redirect first, then the click counted once per person with scanners kept apart; on the mailbox domain's verified `links.` host as `https://links.<domain>/<id>` (AGL-3306), which the console middleware rewrites to the same route |
|
|
109
114
|
| `routes/link-domain-routes`, `components/link-domains-card` | Sequences → Mailboxes → **Link domains** (AGL-3306): `outreach/link-domains` lists each mailbox domain's `links.` host and sets one up, checks it or removes it through `@aglyn/tenant-data-admin/server/tracking-hosts` — the domain driver attaches it, an HTTPS probe verifies it; set-up and removal are an owner's or admin's |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.275",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.275",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.275",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.275",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.275",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.275",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.275",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.275",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.275",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.275",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.275",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.275",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.275",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.275",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -9,9 +9,14 @@ import type { OutreachApi } from './use-outreach-api';
|
|
|
9
9
|
* the header of the person's own page (AGL-3332). The same actions, the same
|
|
10
10
|
* words, the same two confirmations — a Stop that asked first in one place
|
|
11
11
|
* and not in the other would be two products.
|
|
12
|
+
*
|
|
13
|
+
* REPLY IN THREAD (AGL-4020) is offered once the person wrote back — a
|
|
14
|
+
* reply, or an automatic answer — and opens the composer that answers them
|
|
15
|
+
* from the enrollment's own mailbox. Not to a person who opted out or whose
|
|
16
|
+
* address bounced: nothing would reach them, or they asked for nothing to.
|
|
12
17
|
*/
|
|
13
|
-
/** The four the action route takes,
|
|
14
|
-
export type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate';
|
|
18
|
+
/** The four the action route takes, curating the next step (AGL-3324), and answering a reply (AGL-4020). */
|
|
19
|
+
export type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate' | 'reply';
|
|
15
20
|
/** What a member may do to an enrollment in the status it is in. */
|
|
16
21
|
export declare function outreachEnrollmentActionsFor(enrollment: OutreachEnrollment, steps: readonly OutreachSequenceStep[]): OutreachEnrollmentRowAction[];
|
|
17
22
|
export declare const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string>;
|
|
@@ -20,14 +25,16 @@ export declare const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachE
|
|
|
20
25
|
export interface OutreachEnrollmentActions {
|
|
21
26
|
/** The enrollment an action is running on, if any. */
|
|
22
27
|
busy: string | null;
|
|
23
|
-
/** Runs one action: straight away, after its confirmation, or in the curate dialog. */
|
|
28
|
+
/** Runs one action: straight away, after its confirmation, or in the curate or reply dialog. */
|
|
24
29
|
choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void;
|
|
25
30
|
/** The actions as menu items, the destructive ones marked. */
|
|
26
31
|
menuItems(enrollment: OutreachEnrollment, only?: (action: OutreachEnrollmentRowAction) => boolean): RowActionsMenuItem[];
|
|
27
|
-
/** The confirmation and the
|
|
32
|
+
/** The confirmation, the curate dialog and the reply composer, rendered once wherever the actions are offered. */
|
|
28
33
|
dialogs: ReactNode;
|
|
29
34
|
}
|
|
30
35
|
export declare function useOutreachEnrollmentActions(input: {
|
|
31
36
|
api: OutreachApi;
|
|
32
37
|
steps: readonly OutreachSequenceStep[];
|
|
38
|
+
/** The mailbox's zone, for the times the reply composer shows. */
|
|
39
|
+
timeZone?: string | null;
|
|
33
40
|
}): OutreachEnrollmentActions;
|
|
@@ -18,9 +18,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
19
19
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField } from "@mui/material";
|
|
20
20
|
import { useState } from "react";
|
|
21
|
+
import { outreachEnrollmentHasInbound } from "../engine/manual-reply.js";
|
|
21
22
|
import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-step-dialog.js";
|
|
23
|
+
import { OutreachReplyDialog } from "./reply-dialog.js";
|
|
22
24
|
/** What a member may do to an enrollment in the status it is in. */ export function outreachEnrollmentActionsFor(enrollment, steps) {
|
|
23
25
|
const actions = [];
|
|
26
|
+
// Answer them in the thread, once they wrote back (AGL-4020).
|
|
27
|
+
if (outreachEnrollmentHasInbound(enrollment) && enrollment.status !== 'opted_out' && enrollment.status !== 'bounced') {
|
|
28
|
+
actions.push('reply');
|
|
29
|
+
}
|
|
24
30
|
// Rewrite the next email for this one person, while one is still to go.
|
|
25
31
|
if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate');
|
|
26
32
|
if (enrollment.status === 'active') actions.push('pause', 'stop');
|
|
@@ -29,6 +35,7 @@ import { OutreachCurateStepDialog, outreachNextEmailStepIndex } from "./curate-s
|
|
|
29
35
|
return actions;
|
|
30
36
|
}
|
|
31
37
|
export const OUTREACH_ENROLLMENT_ACTION_LABELS = {
|
|
38
|
+
reply: 'Reply in thread',
|
|
32
39
|
curate: 'Curate next step',
|
|
33
40
|
pause: 'Pause',
|
|
34
41
|
resume: 'Resume',
|
|
@@ -64,6 +71,7 @@ export function useOutreachEnrollmentActions(input) {
|
|
|
64
71
|
const [detail, setDetail] = useState('');
|
|
65
72
|
const [busy, setBusy] = useState(null);
|
|
66
73
|
const [curating, setCurating] = useState(null);
|
|
74
|
+
const [replying, setReplying] = useState(null);
|
|
67
75
|
const act = async (enrollment, action, why)=>{
|
|
68
76
|
setBusy(enrollment.id);
|
|
69
77
|
try {
|
|
@@ -84,6 +92,8 @@ export function useOutreachEnrollmentActions(input) {
|
|
|
84
92
|
const choose = (enrollment, action)=>{
|
|
85
93
|
if (action === 'curate') {
|
|
86
94
|
setCurating(enrollment);
|
|
95
|
+
} else if (action === 'reply') {
|
|
96
|
+
setReplying(enrollment);
|
|
87
97
|
} else if (CONFIRM[action]) {
|
|
88
98
|
setDetail('');
|
|
89
99
|
setConfirming({
|
|
@@ -109,6 +119,12 @@ export function useOutreachEnrollmentActions(input) {
|
|
|
109
119
|
api: api,
|
|
110
120
|
onClose: ()=>setCurating(null)
|
|
111
121
|
}),
|
|
122
|
+
/*#__PURE__*/ _jsx(OutreachReplyDialog, {
|
|
123
|
+
enrollment: replying,
|
|
124
|
+
api: api,
|
|
125
|
+
timeZone: input.timeZone,
|
|
126
|
+
onClose: ()=>setReplying(null)
|
|
127
|
+
}),
|
|
112
128
|
/*#__PURE__*/ _jsx(Dialog, {
|
|
113
129
|
open: Boolean(confirming),
|
|
114
130
|
onClose: ()=>setConfirming(null),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollment-actions.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types'\nimport { OutreachCurateStepDialog, outreachNextEmailStepIndex } from './curate-step-dialog'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place\n * for the two places it is offered: a row's menu on the Enrollments tab, and\n * the header of the person's own page (AGL-3332). The same actions, the same\n * words, the same two confirmations — a Stop that asked first in one place\n * and not in the other would be two products.\n */\n\n/** The four the action route takes, and curating the next step (AGL-3324). */\nexport type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate'\n\n/** What a member may do to an enrollment in the status it is in. */\nexport function outreachEnrollmentActionsFor(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n): OutreachEnrollmentRowAction[] {\n const actions: OutreachEnrollmentRowAction[] = []\n // Rewrite the next email for this one person, while one is still to go.\n if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate')\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nexport const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string> = {\n curate: 'Curate next step',\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** The two actions that end something for good. */\nexport const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[] = [\n 'stop',\n 'do_not_contact',\n]\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<Exclude<OutreachEnrollmentAction, 'do_not_contact'>, string> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<Record<OutreachEnrollmentAction, { title: string; body: string; label: string }>> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\nexport interface OutreachEnrollmentActions {\n /** The enrollment an action is running on, if any. */\n busy: string | null\n /** Runs one action: straight away, after its confirmation, or in the curate dialog. */\n choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void\n /** The actions as menu items, the destructive ones marked. */\n menuItems(\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[]\n /** The confirmation and the curate dialog, rendered once wherever the actions are offered. */\n dialogs: ReactNode\n}\n\nexport function useOutreachEnrollmentActions(input: {\n api: OutreachApi\n steps: readonly OutreachSequenceStep[]\n}): OutreachEnrollmentActions {\n const { api, steps } = input\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [curating, setCurating] = useState<OutreachEnrollment | null>(null)\n\n const act = async (enrollment: OutreachEnrollment, action: OutreachEnrollmentAction, why?: string) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction) => {\n if (action === 'curate') {\n setCurating(enrollment)\n } else if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n const menuItems = (\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[] =>\n outreachEnrollmentActionsFor(enrollment, steps)\n .filter((action) => !only || only(action))\n .map((action) => ({\n key: action,\n label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),\n disabled: busy === enrollment.id,\n }))\n\n const dialogs = (\n <>\n <OutreachCurateStepDialog enrollment={curating} steps={steps} api={api} onClose={() => setCurating(null)} />\n <Dialog open={Boolean(confirming)} onClose={() => setConfirming(null)} fullWidth maxWidth=\"xs\">\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{CONFIRM[confirming.action]?.body}</DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </>\n )\n\n return { busy, choose, menuItems, dialogs }\n}\n"],"names":["useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","useState","OutreachCurateStepDialog","outreachNextEmailStepIndex","outreachEnrollmentActionsFor","enrollment","steps","actions","push","status","OUTREACH_ENROLLMENT_ACTION_LABELS","curate","pause","resume","stop","do_not_contact","OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS","DONE_LABELS","CONFIRM","title","body","label","useOutreachEnrollmentActions","input","api","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","curating","setCurating","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","variant","error","message","allowDuplicate","choose","menuItems","only","filter","map","key","onClick","destructive","includes","disabled","dialogs","onClose","open","Boolean","fullWidth","maxWidth","spacing","value","onChange","event","target","slotProps","htmlInput","maxLength","color","trim","undefined"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/enrollment-actions.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n} from '@mui/material'\nimport { type ReactNode, useState } from 'react'\nimport { outreachEnrollmentHasInbound } from '../engine/manual-reply'\nimport type { OutreachEnrollmentAction } from '../model/outreach-api'\nimport type { OutreachEnrollment, OutreachSequenceStep } from '../model/outreach.types'\nimport { OutreachCurateStepDialog, outreachNextEmailStepIndex } from './curate-step-dialog'\nimport { OutreachReplyDialog } from './reply-dialog'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * WHAT A MEMBER CAN DO TO ONE ENROLLMENT (AGL-2980, AGL-3324), in one place\n * for the two places it is offered: a row's menu on the Enrollments tab, and\n * the header of the person's own page (AGL-3332). The same actions, the same\n * words, the same two confirmations — a Stop that asked first in one place\n * and not in the other would be two products.\n *\n * REPLY IN THREAD (AGL-4020) is offered once the person wrote back — a\n * reply, or an automatic answer — and opens the composer that answers them\n * from the enrollment's own mailbox. Not to a person who opted out or whose\n * address bounced: nothing would reach them, or they asked for nothing to.\n */\n\n/** The four the action route takes, curating the next step (AGL-3324), and answering a reply (AGL-4020). */\nexport type OutreachEnrollmentRowAction = OutreachEnrollmentAction | 'curate' | 'reply'\n\n/** What a member may do to an enrollment in the status it is in. */\nexport function outreachEnrollmentActionsFor(\n enrollment: OutreachEnrollment,\n steps: readonly OutreachSequenceStep[],\n): OutreachEnrollmentRowAction[] {\n const actions: OutreachEnrollmentRowAction[] = []\n // Answer them in the thread, once they wrote back (AGL-4020).\n if (\n outreachEnrollmentHasInbound(enrollment) &&\n enrollment.status !== 'opted_out' &&\n enrollment.status !== 'bounced'\n ) {\n actions.push('reply')\n }\n // Rewrite the next email for this one person, while one is still to go.\n if (outreachNextEmailStepIndex(enrollment, steps) !== null) actions.push('curate')\n if (enrollment.status === 'active') actions.push('pause', 'stop')\n if (enrollment.status === 'paused') actions.push('resume', 'stop')\n if (enrollment.status !== 'opted_out') actions.push('do_not_contact')\n return actions\n}\n\nexport const OUTREACH_ENROLLMENT_ACTION_LABELS: Record<OutreachEnrollmentRowAction, string> = {\n reply: 'Reply in thread',\n curate: 'Curate next step',\n pause: 'Pause',\n resume: 'Resume',\n stop: 'Stop',\n do_not_contact: 'Mark do-not-contact',\n}\n\n/** The two actions that end something for good. */\nexport const OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS: readonly OutreachEnrollmentRowAction[] = [\n 'stop',\n 'do_not_contact',\n]\n\n/** What the snackbar says once an action lands. */\nconst DONE_LABELS: Record<Exclude<OutreachEnrollmentAction, 'do_not_contact'>, string> = {\n pause: 'Paused.',\n resume: 'Resumed.',\n stop: 'Stopped.',\n}\n\n/** The two actions that end something for good ask first. */\nconst CONFIRM: Partial<Record<OutreachEnrollmentAction, { title: string; body: string; label: string }>> = {\n stop: {\n title: 'Stop this enrollment?',\n body: 'They won’t get any more steps from this sequence, and they can’t be enrolled in it again.',\n label: 'Stop',\n },\n do_not_contact: {\n title: 'Mark do-not-contact?',\n body:\n 'The address goes on your organization’s do-not-contact list, which every sequence checks before ' +\n 'every send, and they are stopped in every sequence they are in.',\n label: 'Mark do-not-contact',\n },\n}\n\nexport interface OutreachEnrollmentActions {\n /** The enrollment an action is running on, if any. */\n busy: string | null\n /** Runs one action: straight away, after its confirmation, or in the curate or reply dialog. */\n choose(enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction): void\n /** The actions as menu items, the destructive ones marked. */\n menuItems(\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[]\n /** The confirmation, the curate dialog and the reply composer, rendered once wherever the actions are offered. */\n dialogs: ReactNode\n}\n\nexport function useOutreachEnrollmentActions(input: {\n api: OutreachApi\n steps: readonly OutreachSequenceStep[]\n /** The mailbox's zone, for the times the reply composer shows. */\n timeZone?: string | null\n}): OutreachEnrollmentActions {\n const { api, steps } = input\n const { enqueueSnackbar } = useSnackbar()\n const [confirming, setConfirming] = useState<{\n enrollment: OutreachEnrollment\n action: OutreachEnrollmentAction\n } | null>(null)\n const [detail, setDetail] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const [curating, setCurating] = useState<OutreachEnrollment | null>(null)\n const [replying, setReplying] = useState<OutreachEnrollment | null>(null)\n\n const act = async (enrollment: OutreachEnrollment, action: OutreachEnrollmentAction, why?: string) => {\n setBusy(enrollment.id)\n try {\n const answer = await api.actOnEnrollment(enrollment.id, action, why)\n const others = answer.stoppedOthers\n enqueueSnackbar(\n action === 'do_not_contact'\n ? `Marked do-not-contact${others ? `, and stopped in ${others} other ${others === 1 ? 'sequence' : 'sequences'}` : ''}.`\n : answer.changed\n ? DONE_LABELS[action]\n : 'Nothing to change.',\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n const choose = (enrollment: OutreachEnrollment, action: OutreachEnrollmentRowAction) => {\n if (action === 'curate') {\n setCurating(enrollment)\n } else if (action === 'reply') {\n setReplying(enrollment)\n } else if (CONFIRM[action]) {\n setDetail('')\n setConfirming({ enrollment, action })\n } else {\n void act(enrollment, action)\n }\n }\n\n const menuItems = (\n enrollment: OutreachEnrollment,\n only?: (action: OutreachEnrollmentRowAction) => boolean,\n ): RowActionsMenuItem[] =>\n outreachEnrollmentActionsFor(enrollment, steps)\n .filter((action) => !only || only(action))\n .map((action) => ({\n key: action,\n label: OUTREACH_ENROLLMENT_ACTION_LABELS[action],\n onClick: () => choose(enrollment, action),\n destructive: OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS.includes(action),\n disabled: busy === enrollment.id,\n }))\n\n const dialogs = (\n <>\n <OutreachCurateStepDialog enrollment={curating} steps={steps} api={api} onClose={() => setCurating(null)} />\n <OutreachReplyDialog\n enrollment={replying}\n api={api}\n timeZone={input.timeZone}\n onClose={() => setReplying(null)}\n />\n <Dialog open={Boolean(confirming)} onClose={() => setConfirming(null)} fullWidth maxWidth=\"xs\">\n {confirming ? (\n <>\n <DialogTitle>{CONFIRM[confirming.action]?.title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{CONFIRM[confirming.action]?.body}</DialogContentText>\n <TextField\n label=\"Why (optional)\"\n value={detail}\n onChange={(event) => setDetail(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirming(null)}>Cancel</Button>\n <Button\n color=\"error\"\n variant=\"contained\"\n onClick={() => {\n const { enrollment, action } = confirming\n setConfirming(null)\n void act(enrollment, action, detail.trim() || undefined)\n }}\n >\n {CONFIRM[confirming.action]?.label}\n </Button>\n </DialogActions>\n </>\n ) : null}\n </Dialog>\n </>\n )\n\n return { busy, choose, menuItems, dialogs }\n}\n"],"names":["useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","useState","outreachEnrollmentHasInbound","OutreachCurateStepDialog","outreachNextEmailStepIndex","OutreachReplyDialog","outreachEnrollmentActionsFor","enrollment","steps","actions","status","push","OUTREACH_ENROLLMENT_ACTION_LABELS","reply","curate","pause","resume","stop","do_not_contact","OUTREACH_DESTRUCTIVE_ENROLLMENT_ACTIONS","DONE_LABELS","CONFIRM","title","body","label","useOutreachEnrollmentActions","input","api","enqueueSnackbar","confirming","setConfirming","detail","setDetail","busy","setBusy","curating","setCurating","replying","setReplying","act","action","why","id","answer","actOnEnrollment","others","stoppedOthers","changed","variant","error","message","allowDuplicate","choose","menuItems","only","filter","map","key","onClick","destructive","includes","disabled","dialogs","onClose","timeZone","open","Boolean","fullWidth","maxWidth","spacing","value","onChange","event","target","slotProps","htmlInput","maxLength","color","trim","undefined"],"mappings":"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"}
|
|
@@ -18,15 +18,16 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
20
|
import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
21
|
-
import { mdiAccountCancelOutline, mdiAccountOutline, mdiAccountPlusOutline, mdiAlertCircleOutline, mdiCheckCircleOutline, mdiChevronDown, mdiChevronUp, mdiClipboardCheckOutline, mdiContentCopy, mdiCursorDefaultClickOutline, mdiEmailAlertOutline, mdiEmailOffOutline, mdiEmailOpenOutline, mdiEmailOutline, mdiHandBackLeftOutline, mdiPauseCircleOutline, mdiPencilOutline, mdiPlayCircleOutline, mdiReply, mdiShieldSearch, mdiSkipNext, mdiStopCircleOutline } from "@aglyn/shared-data-mdi";
|
|
21
|
+
import { mdiAccountCancelOutline, mdiAccountOutline, mdiAccountPlusOutline, mdiAlertCircleOutline, mdiCheckCircleOutline, mdiChevronDown, mdiChevronUp, mdiClipboardCheckOutline, mdiContentCopy, mdiCursorDefaultClickOutline, mdiEmailAlertOutline, mdiEmailOffOutline, mdiEmailOpenOutline, mdiEmailOutline, mdiHandBackLeftOutline, mdiPauseCircleOutline, mdiPencilOutline, mdiMessageReplyTextOutline, mdiPlayCircleOutline, mdiReply, mdiReplyOutline, mdiShieldSearch, mdiSkipNext, mdiStopCircleOutline } from "@aglyn/shared-data-mdi";
|
|
22
22
|
import { AppLink, CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
23
23
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
24
24
|
import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
|
|
25
25
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
26
26
|
import { useOrgContainerOptions, useOrgMemberOptions } from "@aglyn/tenant-feature-instance";
|
|
27
27
|
import { Alert, Box, Button, Card, Chip, Collapse, Divider, IconButton, Link, List, ListItem, ListItemIcon, ListItemText, Stack, Tooltip, Typography } from "@mui/material";
|
|
28
|
-
import { useCallback, useMemo, useState } from "react";
|
|
28
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
29
29
|
import { OUTREACH_MAIL_GATEWAY_LABELS } from "../engine/mail-gateway.js";
|
|
30
|
+
import { outreachEnrollmentHasInbound } from "../engine/manual-reply.js";
|
|
30
31
|
import { outreachNextSendState } from "../engine/next-send.js";
|
|
31
32
|
import { outreachClickSummary } from "../model/enrollment-engagement.js";
|
|
32
33
|
import { outreachEnrollmentFigures, outreachEnrollmentTimeline, outreachStepName } from "../model/enrollment-timeline.js";
|
|
@@ -78,6 +79,8 @@ const KIND_ICONS = {
|
|
|
78
79
|
stopped: mdiStopCircleOutline,
|
|
79
80
|
'do-not-contact': mdiAccountCancelOutline,
|
|
80
81
|
replied: mdiReply,
|
|
82
|
+
'auto-reply': mdiReplyOutline,
|
|
83
|
+
answered: mdiMessageReplyTextOutline,
|
|
81
84
|
bounced: mdiEmailAlertOutline,
|
|
82
85
|
'opted-out': mdiEmailOffOutline,
|
|
83
86
|
held: mdiHandBackLeftOutline,
|
|
@@ -526,7 +529,7 @@ const TONE_COLORS = {
|
|
|
526
529
|
}
|
|
527
530
|
export function OutreachEnrollmentDetail(props) {
|
|
528
531
|
var _props_mailboxes_mailboxes_find, _ref, _ref1, _enrollment_gmailThreadId;
|
|
529
|
-
var _enrollment_campaignIds, _sequence_stats, _sequence_stats1;
|
|
532
|
+
var _enrollment_campaignIds, _enrollment_inboundMessages, _sequence_stats, _sequence_stats1;
|
|
530
533
|
const { orgId, sectionPath, sequenceId, enrollmentId, orgMount } = props;
|
|
531
534
|
const api = useOutreachApi(orgId);
|
|
532
535
|
const loadedSequence = useOutreachSequence(orgId, sequenceId);
|
|
@@ -549,10 +552,6 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
549
552
|
}, [
|
|
550
553
|
sequence
|
|
551
554
|
]);
|
|
552
|
-
const actions = useOutreachEnrollmentActions({
|
|
553
|
-
api,
|
|
554
|
-
steps
|
|
555
|
-
});
|
|
556
555
|
const memberName = useCallback((uid)=>{
|
|
557
556
|
var _ref;
|
|
558
557
|
var _roster_options_find;
|
|
@@ -570,6 +569,11 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
570
569
|
const enrollmentsPath = `${sectionPath}/${sequenceId}/enrollments`;
|
|
571
570
|
const mailbox = (_props_mailboxes_mailboxes_find = props.mailboxes.mailboxes.find((entry)=>entry.id === ((enrollment == null ? void 0 : enrollment.mailboxId) || (sequence == null ? void 0 : sequence.mailboxId)))) != null ? _props_mailboxes_mailboxes_find : null;
|
|
572
571
|
const timeZone = (_ref = mailbox == null ? void 0 : mailbox.timezone) != null ? _ref : null;
|
|
572
|
+
const actions = useOutreachEnrollmentActions({
|
|
573
|
+
api,
|
|
574
|
+
steps,
|
|
575
|
+
timeZone
|
|
576
|
+
});
|
|
573
577
|
const time = (ms)=>formatOutreachTime(ms, timeZone);
|
|
574
578
|
const timeline = useMemo(()=>{
|
|
575
579
|
var _ref;
|
|
@@ -590,6 +594,24 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
590
594
|
timeZone,
|
|
591
595
|
gateway.data
|
|
592
596
|
]);
|
|
597
|
+
/*
|
|
598
|
+
* A reply synced before replies were kept is on the enrollment only as an
|
|
599
|
+
* id (AGL-4020). Once per page, the thread route reads it from the mailbox
|
|
600
|
+
* and keeps it; the enrollment's listener then shows it in the timeline.
|
|
601
|
+
* A member who may not read that mailbox is answered with nothing new.
|
|
602
|
+
*/ const backfilled = useRef(null);
|
|
603
|
+
const unreadReply = enrollment !== null && outreachEnrollmentHasInbound(enrollment) && !((_enrollment_inboundMessages = enrollment.inboundMessages) == null ? void 0 : _enrollment_inboundMessages.length);
|
|
604
|
+
useEffect(()=>{
|
|
605
|
+
if (!unreadReply || !enrollmentId || backfilled.current === enrollmentId) return;
|
|
606
|
+
backfilled.current = enrollmentId;
|
|
607
|
+
api.readEnrollmentThread(enrollmentId).catch((error)=>{
|
|
608
|
+
console.warn('[outreach] their reply could not be read from the mailbox', error);
|
|
609
|
+
});
|
|
610
|
+
}, [
|
|
611
|
+
api,
|
|
612
|
+
enrollmentId,
|
|
613
|
+
unreadReply
|
|
614
|
+
]);
|
|
593
615
|
const back = /*#__PURE__*/ _jsx(Button, _extends({}, {
|
|
594
616
|
component: AppLink,
|
|
595
617
|
componentVariant: 'naked',
|
|
@@ -639,11 +661,12 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
639
661
|
const open = enrollment.status === 'active' || enrollment.status === 'paused';
|
|
640
662
|
const held = enrollment.status === 'paused' && enrollment.stopReason === 'gateway_blocked_here';
|
|
641
663
|
/*
|
|
642
|
-
* The one act that moves the person forward is the button:
|
|
643
|
-
*
|
|
644
|
-
*
|
|
664
|
+
* The one act that moves the person forward is the button: answering a
|
|
665
|
+
* person who wrote back (AGL-4020), else resuming a paused one, else
|
|
666
|
+
* curating the next email. Everything else is in the menu, the
|
|
667
|
+
* destructive acts included, as on every record page.
|
|
645
668
|
*/ const available = outreachEnrollmentActionsFor(enrollment, steps);
|
|
646
|
-
const primary = available.includes('resume') ? 'resume' : available.includes('curate') ? 'curate' : null;
|
|
669
|
+
const primary = available.includes('reply') ? 'reply' : available.includes('resume') ? 'resume' : available.includes('curate') ? 'curate' : null;
|
|
647
670
|
const menu = actions.menuItems(enrollment, (action)=>action !== primary);
|
|
648
671
|
const dash = (value)=>value ? value.toLocaleString() : '—';
|
|
649
672
|
const notTracked = !trackClicks && !figures.clicks && !figures.scannerClicks ? 'Not tracked' : undefined;
|
|
@@ -691,7 +714,7 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
691
714
|
size: "small",
|
|
692
715
|
disabled: actions.busy === enrollment.id,
|
|
693
716
|
onClick: ()=>actions.choose(enrollment, primary),
|
|
694
|
-
children: held ? 'Resume and send' : OUTREACH_ENROLLMENT_ACTION_LABELS[primary]
|
|
717
|
+
children: held && primary === 'resume' ? 'Resume and send' : OUTREACH_ENROLLMENT_ACTION_LABELS[primary]
|
|
695
718
|
}) : null,
|
|
696
719
|
menu.length ? /*#__PURE__*/ _jsx(RowActionsMenu, {
|
|
697
720
|
label: name,
|
|
@@ -804,7 +827,11 @@ export function OutreachEnrollmentDetail(props) {
|
|
|
804
827
|
/*#__PURE__*/ _jsx(OutreachFigure, {
|
|
805
828
|
label: "Replies",
|
|
806
829
|
value: dash(figures.replies)
|
|
807
|
-
})
|
|
830
|
+
}),
|
|
831
|
+
figures.answers ? /*#__PURE__*/ _jsx(OutreachFigure, {
|
|
832
|
+
label: "Answers sent",
|
|
833
|
+
value: dash(figures.answers)
|
|
834
|
+
}) : null
|
|
808
835
|
]
|
|
809
836
|
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
810
837
|
variant: "body2",
|