@aglyn/plugins-outreach 1.0.0-beta.196 → 1.0.0-beta.198
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/compliance-section.d.ts +3 -1
- package/src/lib/components/compliance-section.js +110 -45
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/enroll-dialog.d.ts +7 -1
- package/src/lib/components/enroll-dialog.js +106 -3
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/mailbox-card.js +11 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/outreach-ui.js +1 -0
- package/src/lib/components/outreach-ui.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +2 -0
- package/src/lib/components/sequence-editor.js +51 -26
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +7 -0
- package/src/lib/components/sequence-step-card.js +37 -14
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +12 -3
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/step-test-dialog.d.ts +31 -0
- package/src/lib/components/step-test-dialog.js +247 -0
- package/src/lib/components/step-test-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +8 -1
- package/src/lib/components/use-outreach-api.js +4 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +6 -1
- package/src/lib/components/use-outreach-data.js +6 -3
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +7 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/delivery-status.d.ts +8 -0
- package/src/lib/engine/delivery-status.js +10 -5
- package/src/lib/engine/delivery-status.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +2 -0
- package/src/lib/engine/enrollment-state.js +2 -2
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/gates.d.ts +28 -1
- package/src/lib/engine/gates.js +28 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +97 -0
- package/src/lib/engine/mail-gateway.js +226 -0
- package/src/lib/engine/mail-gateway.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +3 -3
- package/src/lib/engine/mailbox-health.js +17 -11
- package/src/lib/engine/mailbox-health.js.map +1 -1
- package/src/lib/engine/thread-classification.d.ts +2 -0
- package/src/lib/engine/thread-classification.js +2 -1
- package/src/lib/engine/thread-classification.js.map +1 -1
- package/src/lib/enrollment/enroll-people.d.ts +6 -1
- package/src/lib/enrollment/enroll-people.js +18 -4
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.d.ts +7 -0
- package/src/lib/enrollment/gate-lookups.js +11 -2
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +35 -1
- 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 +21 -2
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +7 -1
- package/src/lib/model/step-preview.js +3 -2
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +23 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +15 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -2
- package/src/lib/routes/platform-deps.js +2 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.d.ts +24 -0
- package/src/lib/routes/preview-routes.js +99 -95
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +27 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +6 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/step-test-routes.d.ts +88 -0
- package/src/lib/routes/step-test-routes.js +275 -0
- package/src/lib/routes/step-test-routes.js.map +1 -0
- package/src/lib/runtime/click-events.js +10 -0
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +11 -0
- package/src/lib/runtime/click-link.js +9 -4
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.js +3 -0
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +46 -0
- package/src/lib/runtime/gateway-ledger.js +113 -0
- package/src/lib/runtime/gateway-ledger.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.js +3 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +6 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +48 -0
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +40 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +107 -0
- package/src/lib/storage/domain-intel-store.js +320 -0
- package/src/lib/storage/domain-intel-store.js.map +1 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { createElement as _createElement } from "react";
|
|
20
|
+
import { crmMergeUnresolvedMessage } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
21
|
+
import { Alert, Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
|
|
22
|
+
import { useEffect, useState } from "react";
|
|
23
|
+
import { isInThreadEmailStep } from "../engine/sequence-validation.js";
|
|
24
|
+
import { OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
|
|
25
|
+
import { OUTREACH_SAMPLE_PERSON } from "../model/step-preview.js";
|
|
26
|
+
import { useOutreachContactSearch, useOutreachLeadSearch } from "./use-outreach-crm.js";
|
|
27
|
+
const SAMPLE = {
|
|
28
|
+
kind: 'sample',
|
|
29
|
+
id: '',
|
|
30
|
+
name: `${OUTREACH_SAMPLE_PERSON.name} (sample)`,
|
|
31
|
+
email: OUTREACH_SAMPLE_PERSON.email
|
|
32
|
+
};
|
|
33
|
+
const GROUP_LABELS = {
|
|
34
|
+
sample: 'Sample',
|
|
35
|
+
lead: 'Leads',
|
|
36
|
+
contact: 'Contacts'
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Send a test of one step (AGL-3325): the step as the sequence stores it,
|
|
40
|
+
* rendered for a chosen person — the sample person the preview uses, or an
|
|
41
|
+
* open lead or contact of the sequence's site — with the personal line the
|
|
42
|
+
* member types, sent through the sequence's mailbox to the member's own
|
|
43
|
+
* address or one they name. The route prefixes the subject with `[Test]`,
|
|
44
|
+
* counts it on nothing but the mailbox's test tally, and never enrolls the
|
|
45
|
+
* person whose record filled the tokens.
|
|
46
|
+
*/ export function OutreachStepTestDialog(props) {
|
|
47
|
+
var _outcome_answer_unresolvedFields;
|
|
48
|
+
const { open, orgId, sequence, stepIndex, api } = props;
|
|
49
|
+
const [person, setPerson] = useState(SAMPLE);
|
|
50
|
+
const [text, setText] = useState('');
|
|
51
|
+
const [personalLine, setPersonalLine] = useState(OUTREACH_SAMPLE_PERSON.personalLine);
|
|
52
|
+
const [to, setTo] = useState('');
|
|
53
|
+
const [outcome, setOutcome] = useState({
|
|
54
|
+
status: 'idle'
|
|
55
|
+
});
|
|
56
|
+
// A fresh dialog for each step: the last test's answer belongs to it.
|
|
57
|
+
useEffect(()=>{
|
|
58
|
+
if (open) setOutcome({
|
|
59
|
+
status: 'idle'
|
|
60
|
+
});
|
|
61
|
+
}, [
|
|
62
|
+
open,
|
|
63
|
+
stepIndex
|
|
64
|
+
]);
|
|
65
|
+
const leads = useOutreachLeadSearch({
|
|
66
|
+
orgId,
|
|
67
|
+
hostId: sequence.hostId,
|
|
68
|
+
text,
|
|
69
|
+
enabled: open
|
|
70
|
+
});
|
|
71
|
+
const contacts = useOutreachContactSearch({
|
|
72
|
+
orgId: open ? orgId : null,
|
|
73
|
+
hostId: sequence.hostId,
|
|
74
|
+
contactGroupId: props.contactGroupId,
|
|
75
|
+
text
|
|
76
|
+
});
|
|
77
|
+
const options = [
|
|
78
|
+
SAMPLE,
|
|
79
|
+
...leads.data.map((lead)=>_extends({}, lead, {
|
|
80
|
+
kind: 'lead'
|
|
81
|
+
})),
|
|
82
|
+
...contacts.data.map((contact)=>_extends({}, contact, {
|
|
83
|
+
kind: 'contact'
|
|
84
|
+
}))
|
|
85
|
+
];
|
|
86
|
+
const choose = (next)=>{
|
|
87
|
+
const chosen = next != null ? next : SAMPLE;
|
|
88
|
+
setPerson(chosen);
|
|
89
|
+
// The sample person comes with a line; a real person waits for one.
|
|
90
|
+
setPersonalLine(chosen.kind === 'sample' ? OUTREACH_SAMPLE_PERSON.personalLine : '');
|
|
91
|
+
};
|
|
92
|
+
const step = sequence.steps[stepIndex];
|
|
93
|
+
const inThread = isInThreadEmailStep(sequence.steps, stepIndex);
|
|
94
|
+
const line = personalLine.trim();
|
|
95
|
+
const tooLong = line.length > OUTREACH_PERSONAL_LINE_MAX;
|
|
96
|
+
const sending = outcome.status === 'sending';
|
|
97
|
+
const send = async ()=>{
|
|
98
|
+
setOutcome({
|
|
99
|
+
status: 'sending'
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const answer = await api.sendStepTest(_extends({
|
|
103
|
+
sequenceId: sequence.id,
|
|
104
|
+
stepIndex
|
|
105
|
+
}, person.kind === 'lead' ? {
|
|
106
|
+
leadId: person.id
|
|
107
|
+
} : person.kind === 'contact' ? {
|
|
108
|
+
contactId: person.id
|
|
109
|
+
} : {}, {
|
|
110
|
+
personalLine: line
|
|
111
|
+
}, to.trim() ? {
|
|
112
|
+
to: to.trim()
|
|
113
|
+
} : {}));
|
|
114
|
+
setOutcome({
|
|
115
|
+
status: 'sent',
|
|
116
|
+
answer
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
setOutcome({
|
|
120
|
+
status: 'failed',
|
|
121
|
+
message: error.message
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const title = `Send a test of step ${stepIndex + 1}`;
|
|
126
|
+
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
127
|
+
open: open,
|
|
128
|
+
onClose: sending ? undefined : props.onClose,
|
|
129
|
+
fullWidth: true,
|
|
130
|
+
maxWidth: "sm",
|
|
131
|
+
"aria-label": title,
|
|
132
|
+
children: [
|
|
133
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
134
|
+
children: title
|
|
135
|
+
}),
|
|
136
|
+
/*#__PURE__*/ _jsx(DialogContent, {
|
|
137
|
+
dividers: true,
|
|
138
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
139
|
+
spacing: 2,
|
|
140
|
+
children: [
|
|
141
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
142
|
+
variant: "body2",
|
|
143
|
+
color: "text.secondary",
|
|
144
|
+
children: `The email goes through ${sequence.name}’s mailbox, exactly as a recipient would get it — the footer, the links, ` + (inThread ? 'the “Re:” subject of a reply — ' : '') + 'with “[Test]” in front of the subject. It counts toward nothing, and nobody is enrolled.'
|
|
145
|
+
}),
|
|
146
|
+
props.unsaved ? /*#__PURE__*/ _jsx(Alert, {
|
|
147
|
+
severity: "info",
|
|
148
|
+
children: "You have unsaved changes. The test sends the step as it was last saved."
|
|
149
|
+
}) : null,
|
|
150
|
+
(step == null ? void 0 : step.kind) !== 'email' ? /*#__PURE__*/ _jsx(Alert, {
|
|
151
|
+
severity: "warning",
|
|
152
|
+
children: "This step does not send an email."
|
|
153
|
+
}) : null,
|
|
154
|
+
/*#__PURE__*/ _jsx(Autocomplete, {
|
|
155
|
+
options: options,
|
|
156
|
+
value: person,
|
|
157
|
+
disableClearable: true,
|
|
158
|
+
inputValue: text,
|
|
159
|
+
onInputChange: (_event, next, reason)=>{
|
|
160
|
+
if (reason === 'input') setText(next);
|
|
161
|
+
else if (reason === 'clear') setText('');
|
|
162
|
+
},
|
|
163
|
+
onChange: (_event, next)=>choose(next),
|
|
164
|
+
// The search is the CRM's own; nothing is filtered again here.
|
|
165
|
+
filterOptions: (list)=>list,
|
|
166
|
+
groupBy: (option)=>GROUP_LABELS[option.kind],
|
|
167
|
+
getOptionLabel: (option)=>option.name || option.email || option.id,
|
|
168
|
+
isOptionEqualToValue: (option, value)=>option.kind === value.kind && option.id === value.id,
|
|
169
|
+
loading: leads.status === 'loading' || contacts.status === 'loading',
|
|
170
|
+
disabled: sending,
|
|
171
|
+
renderOption: (itemProps, option)=>/*#__PURE__*/ _createElement("li", _extends({}, itemProps, {
|
|
172
|
+
key: `${option.kind}:${option.id}`
|
|
173
|
+
}), /*#__PURE__*/ _jsxs(Stack, {
|
|
174
|
+
children: [
|
|
175
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
176
|
+
variant: "body2",
|
|
177
|
+
children: option.name || option.email || option.id
|
|
178
|
+
}),
|
|
179
|
+
option.email && option.email !== option.name ? /*#__PURE__*/ _jsx(Typography, {
|
|
180
|
+
variant: "caption",
|
|
181
|
+
color: "text.secondary",
|
|
182
|
+
children: option.email
|
|
183
|
+
}) : null
|
|
184
|
+
]
|
|
185
|
+
})),
|
|
186
|
+
renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
|
|
187
|
+
label: "Fill the tokens from",
|
|
188
|
+
helperText: "A sample person, or type a name or address to pick an open lead or contact of this sequence’s site. Their record fills the merge fields; the test still goes to you."
|
|
189
|
+
}))
|
|
190
|
+
}),
|
|
191
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
192
|
+
label: "Personal line",
|
|
193
|
+
placeholder: "One sentence on why you're writing to this person now.",
|
|
194
|
+
value: personalLine,
|
|
195
|
+
onChange: (event)=>setPersonalLine(event.target.value),
|
|
196
|
+
disabled: sending,
|
|
197
|
+
error: tooLong,
|
|
198
|
+
helperText: tooLong ? `Keep it to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.` : 'Goes where the emails say {{enrollment.personalLine}}.',
|
|
199
|
+
fullWidth: true,
|
|
200
|
+
size: "small"
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
203
|
+
label: "Send to",
|
|
204
|
+
type: "email",
|
|
205
|
+
value: to,
|
|
206
|
+
onChange: (event)=>setTo(event.target.value),
|
|
207
|
+
disabled: sending,
|
|
208
|
+
helperText: props.selfEmail ? `Leave empty to send it to yourself (${props.selfEmail}).` : 'Leave empty to send it to your own address.',
|
|
209
|
+
fullWidth: true,
|
|
210
|
+
size: "small"
|
|
211
|
+
}),
|
|
212
|
+
outcome.status === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
|
|
213
|
+
severity: "error",
|
|
214
|
+
children: outcome.message
|
|
215
|
+
}) : null,
|
|
216
|
+
outcome.status === 'sent' ? /*#__PURE__*/ _jsxs(Alert, {
|
|
217
|
+
severity: "success",
|
|
218
|
+
children: [
|
|
219
|
+
`Sent to ${outcome.answer.sentTo} as “${outcome.answer.subject}”. Check that inbox.`,
|
|
220
|
+
((_outcome_answer_unresolvedFields = outcome.answer.unresolvedFields) == null ? void 0 : _outcome_answer_unresolvedFields.length) ? ` ${crmMergeUnresolvedMessage(outcome.answer.unresolvedFields)}; they were sent empty.` : ''
|
|
221
|
+
]
|
|
222
|
+
}) : null
|
|
223
|
+
]
|
|
224
|
+
})
|
|
225
|
+
}),
|
|
226
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
227
|
+
children: [
|
|
228
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
229
|
+
onClick: props.onClose,
|
|
230
|
+
disabled: sending,
|
|
231
|
+
children: outcome.status === 'sent' ? 'Done' : 'Cancel'
|
|
232
|
+
}),
|
|
233
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
234
|
+
variant: "contained",
|
|
235
|
+
onClick: ()=>void send(),
|
|
236
|
+
disabled: sending || tooLong || (step == null ? void 0 : step.kind) !== 'email',
|
|
237
|
+
children: sending ? 'Sending…' : outcome.status === 'sent' ? 'Send again' : 'Send the test'
|
|
238
|
+
})
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
OutreachStepTestDialog.displayName = 'OutreachStepTestDialog';
|
|
245
|
+
export default OutreachStepTestDialog;
|
|
246
|
+
|
|
247
|
+
//# sourceMappingURL=step-test-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/step-test-dialog.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 { crmMergeUnresolvedMessage } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport {\n Alert,\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { isInThreadEmailStep } from '../engine/sequence-validation'\nimport type { OutreachStepTestResponse } from '../model/outreach-api'\nimport { OUTREACH_PERSONAL_LINE_MAX, type OutreachSequence } from '../model/outreach.types'\nimport { OUTREACH_SAMPLE_PERSON } from '../model/step-preview'\nimport type { OutreachApi } from './use-outreach-api'\nimport {\n type OutreachContactOption,\n useOutreachContactSearch,\n useOutreachLeadSearch,\n} from './use-outreach-crm'\n\n/** Whose record fills the tokens: the sample person, or an open lead or contact. */\ninterface PersonChoice extends OutreachContactOption {\n kind: 'sample' | 'lead' | 'contact'\n}\n\nconst SAMPLE: PersonChoice = {\n kind: 'sample',\n id: '',\n name: `${OUTREACH_SAMPLE_PERSON.name} (sample)`,\n email: OUTREACH_SAMPLE_PERSON.email,\n}\n\nconst GROUP_LABELS: Record<PersonChoice['kind'], string> = {\n sample: 'Sample',\n lead: 'Leads',\n contact: 'Contacts',\n}\n\ntype Outcome =\n | { status: 'idle' }\n | { status: 'sending' }\n | { status: 'sent'; answer: OutreachStepTestResponse & { unresolvedFields?: string[] } }\n | { status: 'failed'; message: string }\n\nexport interface OutreachStepTestDialogProps {\n open: boolean\n onClose(): void\n orgId: string\n /** The stored sequence: the test is sent from what is saved. */\n sequence: OutreachSequence\n stepIndex: number\n /** The consent group of the sequence's site: whose names a contact search shows. */\n contactGroupId: string\n /** The member's own address, where the test goes unless they type another. */\n selfEmail: string | null\n /** True while the editor holds edits the stored sequence does not. */\n unsaved: boolean\n api: OutreachApi\n}\n\n/**\n * Send a test of one step (AGL-3325): the step as the sequence stores it,\n * rendered for a chosen person — the sample person the preview uses, or an\n * open lead or contact of the sequence's site — with the personal line the\n * member types, sent through the sequence's mailbox to the member's own\n * address or one they name. The route prefixes the subject with `[Test]`,\n * counts it on nothing but the mailbox's test tally, and never enrolls the\n * person whose record filled the tokens.\n */\nexport function OutreachStepTestDialog(props: OutreachStepTestDialogProps) {\n const { open, orgId, sequence, stepIndex, api } = props\n const [person, setPerson] = useState<PersonChoice>(SAMPLE)\n const [text, setText] = useState('')\n const [personalLine, setPersonalLine] = useState<string>(OUTREACH_SAMPLE_PERSON.personalLine)\n const [to, setTo] = useState('')\n const [outcome, setOutcome] = useState<Outcome>({ status: 'idle' })\n\n // A fresh dialog for each step: the last test's answer belongs to it.\n useEffect(() => {\n if (open) setOutcome({ status: 'idle' })\n }, [open, stepIndex])\n\n const leads = useOutreachLeadSearch({\n orgId,\n hostId: sequence.hostId,\n text,\n enabled: open,\n })\n const contacts = useOutreachContactSearch({\n orgId: open ? orgId : null,\n hostId: sequence.hostId,\n contactGroupId: props.contactGroupId,\n text,\n })\n const options: PersonChoice[] = [\n SAMPLE,\n ...leads.data.map((lead) => ({ ...lead, kind: 'lead' as const })),\n ...contacts.data.map((contact) => ({ ...contact, kind: 'contact' as const })),\n ]\n const choose = (next: PersonChoice | null) => {\n const chosen = next ?? SAMPLE\n setPerson(chosen)\n // The sample person comes with a line; a real person waits for one.\n setPersonalLine(chosen.kind === 'sample' ? OUTREACH_SAMPLE_PERSON.personalLine : '')\n }\n\n const step = sequence.steps[stepIndex]\n const inThread = isInThreadEmailStep(sequence.steps, stepIndex)\n const line = personalLine.trim()\n const tooLong = line.length > OUTREACH_PERSONAL_LINE_MAX\n const sending = outcome.status === 'sending'\n\n const send = async () => {\n setOutcome({ status: 'sending' })\n try {\n const answer = await api.sendStepTest({\n sequenceId: sequence.id,\n stepIndex,\n ...(person.kind === 'lead'\n ? { leadId: person.id }\n : person.kind === 'contact'\n ? { contactId: person.id }\n : {}),\n personalLine: line,\n ...(to.trim() ? { to: to.trim() } : {}),\n })\n setOutcome({ status: 'sent', answer })\n } catch (error) {\n setOutcome({ status: 'failed', message: (error as Error).message })\n }\n }\n\n const title = `Send a test of step ${stepIndex + 1}`\n return (\n <Dialog open={open} onClose={sending ? undefined : props.onClose} fullWidth maxWidth=\"sm\" aria-label={title}>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`The email goes through ${sequence.name}’s mailbox, exactly as a recipient would get it — the footer, the links, ` +\n (inThread ? 'the “Re:” subject of a reply — ' : '') +\n 'with “[Test]” in front of the subject. It counts toward nothing, and nobody is enrolled.'}\n </Typography>\n {props.unsaved ? (\n <Alert severity=\"info\">You have unsaved changes. The test sends the step as it was last saved.</Alert>\n ) : null}\n {step?.kind !== 'email' ? (\n <Alert severity=\"warning\">This step does not send an email.</Alert>\n ) : null}\n <Autocomplete<PersonChoice, false, true>\n options={options}\n value={person}\n disableClearable\n inputValue={text}\n onInputChange={(_event, next, reason) => {\n if (reason === 'input') setText(next)\n else if (reason === 'clear') setText('')\n }}\n onChange={(_event, next) => choose(next)}\n // The search is the CRM's own; nothing is filtered again here.\n filterOptions={(list) => list}\n groupBy={(option) => GROUP_LABELS[option.kind]}\n getOptionLabel={(option) => option.name || option.email || option.id}\n isOptionEqualToValue={(option, value) => option.kind === value.kind && option.id === value.id}\n loading={leads.status === 'loading' || contacts.status === 'loading'}\n disabled={sending}\n renderOption={(itemProps, option) => (\n <li {...itemProps} key={`${option.kind}:${option.id}`}>\n <Stack>\n <Typography variant=\"body2\">{option.name || option.email || option.id}</Typography>\n {option.email && option.email !== option.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {option.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Fill the tokens from\"\n helperText=\"A sample person, or type a name or address to pick an open lead or contact of this sequence’s site. Their record fills the merge fields; the test still goes to you.\"\n />\n )}\n />\n <TextField\n label=\"Personal line\"\n placeholder=\"One sentence on why you're writing to this person now.\"\n value={personalLine}\n onChange={(event) => setPersonalLine(event.target.value)}\n disabled={sending}\n error={tooLong}\n helperText={\n tooLong\n ? `Keep it to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.`\n : 'Goes where the emails say {{enrollment.personalLine}}.'\n }\n fullWidth\n size=\"small\"\n />\n <TextField\n label=\"Send to\"\n type=\"email\"\n value={to}\n onChange={(event) => setTo(event.target.value)}\n disabled={sending}\n helperText={\n props.selfEmail\n ? `Leave empty to send it to yourself (${props.selfEmail}).`\n : 'Leave empty to send it to your own address.'\n }\n fullWidth\n size=\"small\"\n />\n {outcome.status === 'failed' ? <Alert severity=\"error\">{outcome.message}</Alert> : null}\n {outcome.status === 'sent' ? (\n <Alert severity=\"success\">\n {`Sent to ${outcome.answer.sentTo} as “${outcome.answer.subject}”. Check that inbox.`}\n {outcome.answer.unresolvedFields?.length\n ? ` ${crmMergeUnresolvedMessage(outcome.answer.unresolvedFields)}; they were sent empty.`\n : ''}\n </Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={props.onClose} disabled={sending}>\n {outcome.status === 'sent' ? 'Done' : 'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n onClick={() => void send()}\n disabled={sending || tooLong || step?.kind !== 'email'}\n >\n {sending ? 'Sending…' : outcome.status === 'sent' ? 'Send again' : 'Send the test'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nOutreachStepTestDialog.displayName = 'OutreachStepTestDialog'\n\nexport default OutreachStepTestDialog\n"],"names":["crmMergeUnresolvedMessage","Alert","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useEffect","useState","isInThreadEmailStep","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_SAMPLE_PERSON","useOutreachContactSearch","useOutreachLeadSearch","SAMPLE","kind","id","name","email","GROUP_LABELS","sample","lead","contact","OutreachStepTestDialog","props","outcome","open","orgId","sequence","stepIndex","api","person","setPerson","text","setText","personalLine","setPersonalLine","to","setTo","setOutcome","status","leads","hostId","enabled","contacts","contactGroupId","options","data","map","choose","next","chosen","step","steps","inThread","line","trim","tooLong","length","sending","send","answer","sendStepTest","sequenceId","leadId","contactId","error","message","title","onClose","undefined","fullWidth","maxWidth","aria-label","dividers","spacing","variant","color","unsaved","severity","value","disableClearable","inputValue","onInputChange","_event","reason","onChange","filterOptions","list","groupBy","option","getOptionLabel","isOptionEqualToValue","loading","disabled","renderOption","itemProps","li","key","renderInput","params","label","helperText","placeholder","event","target","size","type","selfEmail","sentTo","subject","unresolvedFields","onClick","displayName"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OutreachComplianceIssue } from '../model/compliance-settings';
|
|
2
|
-
import type { OutreachCurateDraftRequest, OutreachCurateDraftResponse, OutreachCurateSaveRequest, OutreachCurateSaveResponse, OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse } from '../model/outreach-api';
|
|
2
|
+
import type { OutreachCurateDraftRequest, OutreachCurateDraftResponse, OutreachCurateSaveRequest, OutreachCurateSaveResponse, OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse, OutreachStepTestRequest, OutreachStepTestResponse } from '../model/outreach-api';
|
|
3
3
|
import type { OutreachSequenceDraft, OutreachSequenceIssue } from '../model/sequence-draft';
|
|
4
4
|
/**
|
|
5
5
|
* A route's refusal, carrying its sentence, its stable reason and any
|
|
@@ -27,6 +27,13 @@ export interface OutreachApi {
|
|
|
27
27
|
enroll(sequenceId: string, people: OutreachEnrollPersonRequest[]): Promise<OutreachEnrollResponse>;
|
|
28
28
|
actOnEnrollment(enrollmentId: string, action: OutreachEnrollmentAction, detail?: string): Promise<OutreachEnrollmentActionResponse>;
|
|
29
29
|
previewEmail(input: Omit<OutreachPreviewRequest, 'orgId'>): Promise<OutreachPreviewResponse>;
|
|
30
|
+
/**
|
|
31
|
+
* Sends one step of a saved sequence to the member as a test (AGL-3325):
|
|
32
|
+
* the preview's email, through the sequence's mailbox, `[Test]` in front.
|
|
33
|
+
*/
|
|
34
|
+
sendStepTest(input: Omit<OutreachStepTestRequest, 'orgId'>): Promise<OutreachStepTestResponse & {
|
|
35
|
+
unresolvedFields?: string[];
|
|
36
|
+
}>;
|
|
30
37
|
/**
|
|
31
38
|
* One person's own copies of the sequence's email steps, drafted by the
|
|
32
39
|
* workspace's AI (AGL-3324); nothing is stored until the member confirms.
|
|
@@ -131,6 +131,10 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
|
131
131
|
method: 'POST',
|
|
132
132
|
body: _extends({}, input)
|
|
133
133
|
}),
|
|
134
|
+
sendStepTest: (input)=>call(OUTREACH_API_ROUTES.stepTest, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: _extends({}, input)
|
|
137
|
+
}),
|
|
134
138
|
curateDrafts: (input)=>call(OUTREACH_API_ROUTES.curateDraft, {
|
|
135
139
|
method: 'POST',
|
|
136
140
|
body: _extends({}, input)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.ts"],"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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","previewEmail","preview","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.ts"],"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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","previewEmail","preview","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
|
|
@@ -20,8 +20,13 @@ export interface OutreachLoad<T> {
|
|
|
20
20
|
}
|
|
21
21
|
/** The most sequences the list reads. */
|
|
22
22
|
export declare const OUTREACH_SEQUENCES_LIMIT = 200;
|
|
23
|
+
/** The sequences read, and whether the organization holds older ones. */
|
|
24
|
+
export interface OutreachSequencesLoad extends OutreachLoad<OutreachSequence[]> {
|
|
25
|
+
/** True when there are sequences older than the `OUTREACH_SEQUENCES_LIMIT` read. */
|
|
26
|
+
truncated?: boolean;
|
|
27
|
+
}
|
|
23
28
|
/** The organization's sequences, newest first. */
|
|
24
|
-
export declare function useOutreachSequences(orgId: string | null):
|
|
29
|
+
export declare function useOutreachSequences(orgId: string | null): OutreachSequencesLoad;
|
|
25
30
|
/** One sequence, or `null` once it is known not to exist. */
|
|
26
31
|
export declare function useOutreachSequence(orgId: string | null, sequenceId: string | null): OutreachLoad<OutreachSequence | null>;
|
|
27
32
|
/**
|
|
@@ -47,11 +47,14 @@ function failed(data, error, what) {
|
|
|
47
47
|
if (!orgId) return undefined;
|
|
48
48
|
return onSnapshot(query(collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences), // Ordered, so the cap keeps the newest rather than an arbitrary set;
|
|
49
49
|
// every sequence carries `createdAtMs`, stamped by the save route.
|
|
50
|
-
orderBy('createdAtMs', 'desc'),
|
|
51
|
-
|
|
50
|
+
orderBy('createdAtMs', 'desc'), // One past the cap: the extra document says there are older
|
|
51
|
+
// sequences, and is never listed.
|
|
52
|
+
limit(OUTREACH_SEQUENCES_LIMIT + 1)), (snapshot)=>{
|
|
53
|
+
const sequences = snapshot.docs.slice(0, OUTREACH_SEQUENCES_LIMIT).map((entry)=>readStoredOutreachSequence(entry.id, entry.data())).filter((sequence)=>sequence !== null).sort((a, b)=>b.createdAtMs - a.createdAtMs);
|
|
52
54
|
setResult({
|
|
53
55
|
status: 'ready',
|
|
54
|
-
data: sequences
|
|
56
|
+
data: sequences,
|
|
57
|
+
truncated: snapshot.docs.length > OUTREACH_SEQUENCES_LIMIT
|
|
55
58
|
});
|
|
56
59
|
}, (error)=>setResult(failed([], error, 'sequences')));
|
|
57
60
|
}, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-data.ts"],"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 { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_LINK_ROLLUP_PATH,\n type OutreachDoNotContactDomainEntry,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n readStoredOutreachEnrollment,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/**\n * The Outreach documents the console reads LIVE (AGL-2980): sequences,\n * enrollments, and the mailboxes they send from.\n *\n * Read straight from Firestore — the rules let an org-wide member holding\n * `outreach.use` on an entitled organization read them, and every write goes\n * through a route — so what a page shows is what the routes and the sending\n * runtime wrote, the moment it lands.\n *\n * Every hook answers one of four states, and a page renders each: still\n * loading, ready, failed, or REFUSED — the rules said no, which is a reader\n * who may not see Outreach here rather than something that went wrong.\n */\n\nexport type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused'\n\nexport interface OutreachLoad<T> {\n status: OutreachLoadStatus\n data: T\n}\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\nfunction failed<T>(data: T, error: unknown, what: string): OutreachLoad<T> {\n if (denied(error)) return { status: 'refused', data }\n console.error(`[outreach] ${what} could not be read`, error)\n return { status: 'error', data }\n}\n\n/** The most sequences the list reads. */\nexport const OUTREACH_SEQUENCES_LIMIT = 200\n\n/** The organization's sequences, newest first. */\nexport function useOutreachSequences(\n orgId: string | null,\n): OutreachLoad<OutreachSequence[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences),\n // Ordered, so the cap keeps the newest rather than an arbitrary set;\n // every sequence carries `createdAtMs`, stamped by the save route.\n orderBy('createdAtMs', 'desc'),\n limit(OUTREACH_SEQUENCES_LIMIT),\n ),\n (snapshot) => {\n const sequences = snapshot.docs\n .map((entry) => readStoredOutreachSequence(entry.id, entry.data()))\n .filter((sequence): sequence is OutreachSequence => sequence !== null)\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: sequences })\n },\n (error) => setResult(failed([], error, 'sequences')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** One sequence, or `null` once it is known not to exist. */\nexport function useOutreachSequence(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachLoad<OutreachSequence | null> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence | null>>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: readStoredOutreachSequence(\n snapshot.id,\n snapshot.exists() ? snapshot.data() : undefined,\n ),\n }),\n (error) => setResult(failed(null, error, 'the sequence')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/**\n * A sequence's per-destination click rollup (AGL-3239), or `null` until a\n * click has been counted.\n *\n * One document, whatever the sequence's size: the aggregate exists so the\n * table is not a read of every enrollment. A sequence that tracks nothing\n * never has one, and an absent document is not an error.\n */\nexport type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>\n\nexport function useOutreachSequenceLinks(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachSequenceLinksLoad {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachSequenceLinksLoad>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.sequences,\n sequenceId,\n OUTREACH_LINK_ROLLUP_PATH[0],\n OUTREACH_LINK_ROLLUP_PATH[1],\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.exists() ? (snapshot.data() as CampaignLinkRollup) : null,\n }),\n (error) => setResult(failed(null, error, 'the link report')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/**\n * The domains on the organization's do-not-contact list (AGL-3244),\n * alphabetically, live: an add or a remove through the route shows up here\n * without a reload, and so does a domain the sending runtime files after a\n * gateway block.\n */\nexport function useOutreachDoNotContactDomains(\n orgId: string | null,\n): OutreachLoad<OutreachDoNotContactDomainEntry[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachDoNotContactDomainEntry[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.doNotContactDomains),\n (snapshot) => {\n const domains = snapshot.docs\n .map((entry) => ({ ...(entry.data() as OutreachDoNotContactDomainEntry), domain: entry.id }))\n .sort((a, b) => a.domain.localeCompare(b.domain))\n setResult({ status: 'ready', data: domains })\n },\n (error) => setResult(failed([], error, 'the do-not-contact domains')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** How many enrollments one page of the table reads; \"Show more\" reads another. */\nexport const OUTREACH_ENROLLMENTS_PAGE = 200\n\nexport interface OutreachEnrollmentsLoad extends OutreachLoad<\n OutreachEnrollment[]\n> {\n /** Whether a further page may exist. */\n hasMore: boolean\n showMore(): void\n}\n\n/**\n * A sequence's enrollments, the most recently enrolled first.\n *\n * Paged by document id — an equality on `sequenceId` in the id's own order\n * needs no composite index — and sorted by enrollment time once read, so a\n * table of up to a page reads in the order people were enrolled.\n */\nexport function useOutreachEnrollments(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachEnrollmentsLoad {\n const firestore = useFirestore()\n const [pages, setPages] = useState(1)\n const [result, setResult] = useState<\n OutreachLoad<OutreachEnrollment[]> & { size: number }\n >({\n status: 'loading',\n data: [],\n size: 0,\n })\n useEffect(() => setPages(1), [orgId, sequenceId])\n useEffect(() => {\n if (!orgId || !sequenceId) {\n setResult({ status: 'loading', data: [], size: 0 })\n return undefined\n }\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('sequenceId', '==', sequenceId),\n orderBy(documentId()),\n limit(pages * OUTREACH_ENROLLMENTS_PAGE),\n ),\n (snapshot) => {\n const enrollments = snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter(\n (enrollment): enrollment is OutreachEnrollment =>\n enrollment !== null,\n )\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: enrollments, size: snapshot.size })\n },\n (error) =>\n setResult({ ...failed([], error, 'the enrollments'), size: 0 }),\n )\n }, [firestore, orgId, sequenceId, pages])\n return useMemo(\n () => ({\n status: result.status,\n data: result.data,\n hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,\n showMore: () => setPages((count) => count + 1),\n }),\n [result, pages],\n )\n}\n\n/** The counts the sequence list shows beside each sequence. */\nexport interface OutreachSequenceCounts {\n enrolled: number\n active: number\n replied: number\n bounced: number\n optedOut: number\n}\n\nconst COUNTED: ReadonlyArray<\n [keyof OutreachSequenceCounts, OutreachEnrollmentStatus | null]\n> = [\n ['enrolled', null],\n ['active', 'active'],\n ['replied', 'replied'],\n ['bounced', 'bounced'],\n ['optedOut', 'opted_out'],\n]\n\n/**\n * Each sequence's enrollment counts, by server-side aggregation — one count\n * per figure, never a read of the enrollments themselves. Recounted when the\n * sequences asked about change — the list asks about the page on screen —\n * and absent while counting, or if a count failed.\n */\nexport function useOutreachSequenceCounts(\n orgId: string | null,\n sequenceIds: readonly string[],\n): Record<string, OutreachSequenceCounts> {\n const firestore = useFirestore()\n const [counts, setCounts] = useState<Record<string, OutreachSequenceCounts>>(\n {},\n )\n const key = sequenceIds.join(',')\n useEffect(() => {\n if (!orgId || !key) return undefined\n let current = true\n const enrollments = collection(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.enrollments,\n )\n Promise.all(\n key.split(',').map(async (sequenceId) => {\n const figures = await Promise.all(\n COUNTED.map(async ([, status]) => {\n const counted = await getCountFromServer(\n status\n ? query(\n enrollments,\n where('sequenceId', '==', sequenceId),\n where('status', '==', status),\n )\n : query(enrollments, where('sequenceId', '==', sequenceId)),\n )\n return counted.data().count\n }),\n )\n return [\n sequenceId,\n Object.fromEntries(\n COUNTED.map(([name], index) => [name, figures[index]]),\n ) as unknown as OutreachSequenceCounts,\n ] as const\n }),\n )\n .then((entries) => current && setCounts(Object.fromEntries(entries)))\n .catch((error: unknown) =>\n console.error('[outreach] enrollment counts could not be read', error),\n )\n return () => {\n current = false\n }\n }, [firestore, orgId, key])\n return counts\n}\n"],"names":["useFirestore","collection","doc","documentId","getCountFromServer","limit","onSnapshot","orderBy","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","OUTREACH_LINK_ROLLUP_PATH","readStoredOutreachEnrollment","readStoredOutreachSequence","denied","error","code","failed","data","what","status","console","OUTREACH_SEQUENCES_LIMIT","useOutreachSequences","orgId","firestore","result","setResult","undefined","sequences","snapshot","docs","map","entry","id","filter","sequence","sort","a","b","createdAtMs","useOutreachSequence","sequenceId","exists","useOutreachSequenceLinks","useOutreachDoNotContactDomains","doNotContactDomains","domains","domain","localeCompare","OUTREACH_ENROLLMENTS_PAGE","useOutreachEnrollments","pages","setPages","size","enrollments","enrollment","hasMore","showMore","count","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-data.ts"],"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 { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_LINK_ROLLUP_PATH,\n type OutreachDoNotContactDomainEntry,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n readStoredOutreachEnrollment,\n readStoredOutreachSequence,\n} from '../model/stored-records'\n\n/**\n * The Outreach documents the console reads LIVE (AGL-2980): sequences,\n * enrollments, and the mailboxes they send from.\n *\n * Read straight from Firestore — the rules let an org-wide member holding\n * `outreach.use` on an entitled organization read them, and every write goes\n * through a route — so what a page shows is what the routes and the sending\n * runtime wrote, the moment it lands.\n *\n * Every hook answers one of four states, and a page renders each: still\n * loading, ready, failed, or REFUSED — the rules said no, which is a reader\n * who may not see Outreach here rather than something that went wrong.\n */\n\nexport type OutreachLoadStatus = 'loading' | 'ready' | 'error' | 'refused'\n\nexport interface OutreachLoad<T> {\n status: OutreachLoadStatus\n data: T\n}\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\nfunction failed<T>(data: T, error: unknown, what: string): OutreachLoad<T> {\n if (denied(error)) return { status: 'refused', data }\n console.error(`[outreach] ${what} could not be read`, error)\n return { status: 'error', data }\n}\n\n/** The most sequences the list reads. */\nexport const OUTREACH_SEQUENCES_LIMIT = 200\n\n/** The sequences read, and whether the organization holds older ones. */\nexport interface OutreachSequencesLoad extends OutreachLoad<OutreachSequence[]> {\n /** True when there are sequences older than the `OUTREACH_SEQUENCES_LIMIT` read. */\n truncated?: boolean\n}\n\n/** The organization's sequences, newest first. */\nexport function useOutreachSequences(\n orgId: string | null,\n): OutreachSequencesLoad {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachSequencesLoad>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences),\n // Ordered, so the cap keeps the newest rather than an arbitrary set;\n // every sequence carries `createdAtMs`, stamped by the save route.\n orderBy('createdAtMs', 'desc'),\n // One past the cap: the extra document says there are older\n // sequences, and is never listed.\n limit(OUTREACH_SEQUENCES_LIMIT + 1),\n ),\n (snapshot) => {\n const sequences = snapshot.docs\n .slice(0, OUTREACH_SEQUENCES_LIMIT)\n .map((entry) => readStoredOutreachSequence(entry.id, entry.data()))\n .filter((sequence): sequence is OutreachSequence => sequence !== null)\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({\n status: 'ready',\n data: sequences,\n truncated: snapshot.docs.length > OUTREACH_SEQUENCES_LIMIT,\n })\n },\n (error) => setResult(failed([], error, 'sequences')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** One sequence, or `null` once it is known not to exist. */\nexport function useOutreachSequence(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachLoad<OutreachSequence | null> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachSequence | null>>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences, sequenceId),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: readStoredOutreachSequence(\n snapshot.id,\n snapshot.exists() ? snapshot.data() : undefined,\n ),\n }),\n (error) => setResult(failed(null, error, 'the sequence')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/**\n * A sequence's per-destination click rollup (AGL-3239), or `null` until a\n * click has been counted.\n *\n * One document, whatever the sequence's size: the aggregate exists so the\n * table is not a read of every enrollment. A sequence that tracks nothing\n * never has one, and an absent document is not an error.\n */\nexport type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>\n\nexport function useOutreachSequenceLinks(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachSequenceLinksLoad {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachSequenceLinksLoad>({\n status: 'loading',\n data: null,\n })\n useEffect(() => {\n setResult({ status: 'loading', data: null })\n if (!orgId || !sequenceId) return undefined\n return onSnapshot(\n doc(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.sequences,\n sequenceId,\n OUTREACH_LINK_ROLLUP_PATH[0],\n OUTREACH_LINK_ROLLUP_PATH[1],\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.exists() ? (snapshot.data() as CampaignLinkRollup) : null,\n }),\n (error) => setResult(failed(null, error, 'the link report')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\n/**\n * The domains on the organization's do-not-contact list (AGL-3244),\n * alphabetically, live: an add or a remove through the route shows up here\n * without a reload, and so does a domain the sending runtime files after a\n * gateway block.\n */\nexport function useOutreachDoNotContactDomains(\n orgId: string | null,\n): OutreachLoad<OutreachDoNotContactDomainEntry[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachDoNotContactDomainEntry[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n return onSnapshot(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.doNotContactDomains),\n (snapshot) => {\n const domains = snapshot.docs\n .map((entry) => ({ ...(entry.data() as OutreachDoNotContactDomainEntry), domain: entry.id }))\n .sort((a, b) => a.domain.localeCompare(b.domain))\n setResult({ status: 'ready', data: domains })\n },\n (error) => setResult(failed([], error, 'the do-not-contact domains')),\n )\n }, [firestore, orgId])\n return result\n}\n\n/** How many enrollments one page of the table reads; \"Show more\" reads another. */\nexport const OUTREACH_ENROLLMENTS_PAGE = 200\n\nexport interface OutreachEnrollmentsLoad extends OutreachLoad<\n OutreachEnrollment[]\n> {\n /** Whether a further page may exist. */\n hasMore: boolean\n showMore(): void\n}\n\n/**\n * A sequence's enrollments, the most recently enrolled first.\n *\n * Paged by document id — an equality on `sequenceId` in the id's own order\n * needs no composite index — and sorted by enrollment time once read, so a\n * table of up to a page reads in the order people were enrolled.\n */\nexport function useOutreachEnrollments(\n orgId: string | null,\n sequenceId: string | null,\n): OutreachEnrollmentsLoad {\n const firestore = useFirestore()\n const [pages, setPages] = useState(1)\n const [result, setResult] = useState<\n OutreachLoad<OutreachEnrollment[]> & { size: number }\n >({\n status: 'loading',\n data: [],\n size: 0,\n })\n useEffect(() => setPages(1), [orgId, sequenceId])\n useEffect(() => {\n if (!orgId || !sequenceId) {\n setResult({ status: 'loading', data: [], size: 0 })\n return undefined\n }\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('sequenceId', '==', sequenceId),\n orderBy(documentId()),\n limit(pages * OUTREACH_ENROLLMENTS_PAGE),\n ),\n (snapshot) => {\n const enrollments = snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter(\n (enrollment): enrollment is OutreachEnrollment =>\n enrollment !== null,\n )\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n setResult({ status: 'ready', data: enrollments, size: snapshot.size })\n },\n (error) =>\n setResult({ ...failed([], error, 'the enrollments'), size: 0 }),\n )\n }, [firestore, orgId, sequenceId, pages])\n return useMemo(\n () => ({\n status: result.status,\n data: result.data,\n hasMore: result.size >= pages * OUTREACH_ENROLLMENTS_PAGE,\n showMore: () => setPages((count) => count + 1),\n }),\n [result, pages],\n )\n}\n\n/** The counts the sequence list shows beside each sequence. */\nexport interface OutreachSequenceCounts {\n enrolled: number\n active: number\n replied: number\n bounced: number\n optedOut: number\n}\n\nconst COUNTED: ReadonlyArray<\n [keyof OutreachSequenceCounts, OutreachEnrollmentStatus | null]\n> = [\n ['enrolled', null],\n ['active', 'active'],\n ['replied', 'replied'],\n ['bounced', 'bounced'],\n ['optedOut', 'opted_out'],\n]\n\n/**\n * Each sequence's enrollment counts, by server-side aggregation — one count\n * per figure, never a read of the enrollments themselves. Recounted when the\n * sequences asked about change — the list asks about the page on screen —\n * and absent while counting, or if a count failed.\n */\nexport function useOutreachSequenceCounts(\n orgId: string | null,\n sequenceIds: readonly string[],\n): Record<string, OutreachSequenceCounts> {\n const firestore = useFirestore()\n const [counts, setCounts] = useState<Record<string, OutreachSequenceCounts>>(\n {},\n )\n const key = sequenceIds.join(',')\n useEffect(() => {\n if (!orgId || !key) return undefined\n let current = true\n const enrollments = collection(\n firestore,\n 'orgs',\n orgId,\n OUTREACH_COLLECTIONS.enrollments,\n )\n Promise.all(\n key.split(',').map(async (sequenceId) => {\n const figures = await Promise.all(\n COUNTED.map(async ([, status]) => {\n const counted = await getCountFromServer(\n status\n ? query(\n enrollments,\n where('sequenceId', '==', sequenceId),\n where('status', '==', status),\n )\n : query(enrollments, where('sequenceId', '==', sequenceId)),\n )\n return counted.data().count\n }),\n )\n return [\n sequenceId,\n Object.fromEntries(\n COUNTED.map(([name], index) => [name, figures[index]]),\n ) as unknown as OutreachSequenceCounts,\n ] as const\n }),\n )\n .then((entries) => current && setCounts(Object.fromEntries(entries)))\n .catch((error: unknown) =>\n console.error('[outreach] enrollment counts could not be read', error),\n )\n return () => {\n current = false\n }\n }, [firestore, orgId, key])\n return counts\n}\n"],"names":["useFirestore","collection","doc","documentId","getCountFromServer","limit","onSnapshot","orderBy","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","OUTREACH_LINK_ROLLUP_PATH","readStoredOutreachEnrollment","readStoredOutreachSequence","denied","error","code","failed","data","what","status","console","OUTREACH_SEQUENCES_LIMIT","useOutreachSequences","orgId","firestore","result","setResult","undefined","sequences","snapshot","docs","slice","map","entry","id","filter","sequence","sort","a","b","createdAtMs","truncated","length","useOutreachSequence","sequenceId","exists","useOutreachSequenceLinks","useOutreachDoNotContactDomains","doNotContactDomains","domains","domain","localeCompare","OUTREACH_ENROLLMENTS_PAGE","useOutreachEnrollments","pages","setPages","size","enrollments","enrollment","hasMore","showMore","count","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|
|
@@ -74,6 +74,13 @@ export declare const OUTREACH_API_ROUTES: {
|
|
|
74
74
|
readonly linkDomains: "outreach/link-domains";
|
|
75
75
|
/** `POST` — one email of a saved sequence, for a contact or a sample person. */
|
|
76
76
|
readonly preview: "outreach/preview";
|
|
77
|
+
/**
|
|
78
|
+
* `POST` — one step of a saved sequence, rendered as the preview renders
|
|
79
|
+
* it and sent through the sequence's mailbox to the member (AGL-3325):
|
|
80
|
+
* `[Test]` in the subject, its links minted as test links, and counted
|
|
81
|
+
* on nothing but the mailbox's test tally.
|
|
82
|
+
*/
|
|
83
|
+
readonly stepTest: "outreach/steps/test";
|
|
77
84
|
/**
|
|
78
85
|
* `POST` — one person's own copies of a sequence's email steps, drafted by
|
|
79
86
|
* the workspace's AI (AGL-3324): for a person about to be enrolled, or for
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
*/ linkDomains: 'outreach/link-domains',
|
|
59
59
|
/** `POST` — one email of a saved sequence, for a contact or a sample person. */ preview: 'outreach/preview',
|
|
60
60
|
/**
|
|
61
|
+
* `POST` — one step of a saved sequence, rendered as the preview renders
|
|
62
|
+
* it and sent through the sequence's mailbox to the member (AGL-3325):
|
|
63
|
+
* `[Test]` in the subject, its links minted as test links, and counted
|
|
64
|
+
* on nothing but the mailbox's test tally.
|
|
65
|
+
*/ stepTest: 'outreach/steps/test',
|
|
66
|
+
/**
|
|
61
67
|
* `POST` — one person's own copies of a sequence's email steps, drafted by
|
|
62
68
|
* the workspace's AI (AGL-3324): for a person about to be enrolled, or for
|
|
63
69
|
* the next step of an enrollment. Nothing is stored.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/api-routes.ts"],"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\n/**\n * Outreach's console API routes, as the dispatcher keys them (AGL-2974).\n *\n * One table both halves import, because the server registers the path and the\n * client fetches it, and a route spelled twice is a route that 404s the day\n * one spelling changes. Every path sits under the `outreach` prefix that\n * `plugins.config.json` gives this plugin, which is what lets the console's\n * `/api/[...pluginApi]` dispatcher gate a request on the plugin being enabled\n * and released before the handler runs. Client-safe: constants only.\n */\nexport const OUTREACH_API_ROUTES = {\n /** `GET` — proves the server bundle loaded and registered; see `server.ts`. */\n ping: 'outreach/ping',\n // Mailboxes (AGL-2978). Every one names its org (`orgId`, query or body)\n // so the dispatcher's release gate asks about that organization.\n /** `GET ?orgId` — whether this deployment can connect a Google mailbox. */\n mailboxesAvailability: 'outreach/mailboxes/availability',\n /** `POST` — Google's consent address for a new connect. */\n mailboxesConnect: 'outreach/mailboxes/connect',\n /**\n * `GET` — Google's redirect back. It carries no bearer token: the signed\n * state names the org and the member, and the route hands the code to the\n * Mailboxes page in a fragment rather than acting on it.\n */\n mailboxesOAuthCallback: 'outreach/mailboxes/oauth/callback',\n /** `POST` — finishes a connect with the member's own session. */\n mailboxesConnectComplete: 'outreach/mailboxes/connect/complete',\n /** `POST` — send-as, display name, daily cap, sending window, timezone. */\n mailboxesSettings: 'outreach/mailboxes/settings',\n /** `POST` — pause or resume. */\n mailboxesStatus: 'outreach/mailboxes/status',\n /** `POST` — a plain-text test to the account's own address. */\n mailboxesTest: 'outreach/mailboxes/test',\n /** `POST` — revoke the grant at Google and delete the mailbox. */\n mailboxesDisconnect: 'outreach/mailboxes/disconnect',\n // Settings, sequences and enrollments (AGL-2980). Every one names its org\n // (`orgId`, query or body); the contract is `model/outreach-api.ts`.\n /** `GET ?orgId` — the compliance settings; `POST` — save them. */\n settings: 'outreach/settings',\n /** `POST` — create a sequence, or save an edit to one. */\n sequencesSave: 'outreach/sequences/save',\n /** `POST` — activate, pause or archive a sequence. */\n sequencesStatus: 'outreach/sequences/status',\n /** `POST` — delete a draft nobody was enrolled in. */\n sequencesDelete: 'outreach/sequences/delete',\n /** `POST` — who a saved view or a search would enroll, and where each stands. */\n enrollPreview: 'outreach/enroll/preview',\n /** `POST` — enroll them, every gate checked again. */\n enroll: 'outreach/enroll',\n /** `POST` — pause, resume, stop, or mark do-not-contact. */\n enrollmentsAction: 'outreach/enrollments/action',\n /**\n * `GET ?orgId` — the domains on the organization's do-not-contact list\n * (AGL-3244); `POST` — add one, or take one off.\n */\n doNotContactDomains: 'outreach/do-not-contact/domains',\n /**\n * `GET ?orgId` — each connected mailbox domain's click-tracking host\n * (AGL-3306), `links.<domain>`; `POST` — set one up, check it, or remove\n * it. Setting up and removing are an owner's or admin's.\n */\n linkDomains: 'outreach/link-domains',\n /** `POST` — one email of a saved sequence, for a contact or a sample person. */\n preview: 'outreach/preview',\n /**\n * `POST` — one person's own copies of a sequence's email steps, drafted by\n * the workspace's AI (AGL-3324): for a person about to be enrolled, or for\n * the next step of an enrollment. Nothing is stored.\n */\n curateDraft: 'outreach/curate/draft',\n /** `POST` — stores a member's confirmed copy of one step on an enrollment, or clears it. */\n curateSave: 'outreach/curate/save',\n // The one route a RECIPIENT reaches (AGL-2981): no session, a signed\n // token instead, and registered as a recipient link so it answers whether\n // or not Outreach is released for the organization now.\n /** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */\n unsubscribe: 'outreach/unsubscribe',\n /**\n * `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed\n * token>`; answers a redirect to the destination the token names, and\n * records the click afterwards. A recipient link like the unsubscribe\n * one: no session, and it answers whether or not Outreach is released to\n * the organization now, because a link in an email that already left must\n * not stop working when a rollout is paused.\n */\n click: 'outreach/click',\n /**\n * `GET` — the short tracking link (AGL-3297): `outreach/l/<id>`, where the\n * id names a stored `outreachLinks` document holding the destination. It\n * replaced the signed `click` link for every send since; `click` keeps\n * answering the links already in inboxes. A recipient link, for `click`'s\n * reasons.\n */\n shortLink: 'outreach/l/:linkId',\n} as const\n"],"names":["OUTREACH_API_ROUTES","ping","mailboxesAvailability","mailboxesConnect","mailboxesOAuthCallback","mailboxesConnectComplete","mailboxesSettings","mailboxesStatus","mailboxesTest","mailboxesDisconnect","settings","sequencesSave","sequencesStatus","sequencesDelete","enrollPreview","enroll","enrollmentsAction","doNotContactDomains","linkDomains","preview","curateDraft","curateSave","unsubscribe","click","shortLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GACD,OAAO,MAAMA,sBAAsB;IACjC,6EAA6E,GAC7EC,MAAM;IACN,yEAAyE;IACzE,iEAAiE;IACjE,yEAAyE,GACzEC,uBAAuB;IACvB,yDAAyD,GACzDC,kBAAkB;IAClB;;;;GAIC,GACDC,wBAAwB;IACxB,+DAA+D,GAC/DC,0BAA0B;IAC1B,yEAAyE,GACzEC,mBAAmB;IACnB,8BAA8B,GAC9BC,iBAAiB;IACjB,6DAA6D,GAC7DC,eAAe;IACf,gEAAgE,GAChEC,qBAAqB;IACrB,0EAA0E;IAC1E,qEAAqE;IACrE,gEAAgE,GAChEC,UAAU;IACV,wDAAwD,GACxDC,eAAe;IACf,oDAAoD,GACpDC,iBAAiB;IACjB,oDAAoD,GACpDC,iBAAiB;IACjB,+EAA+E,GAC/EC,eAAe;IACf,oDAAoD,GACpDC,QAAQ;IACR,0DAA0D,GAC1DC,mBAAmB;IACnB;;;GAGC,GACDC,qBAAqB;IACrB;;;;GAIC,GACDC,aAAa;IACb,8EAA8E,GAC9EC,SAAS;IACT;;;;GAIC,GACDC,aAAa;IACb,0FAA0F,GAC1FC,YAAY;IACZ,qEAAqE;IACrE,0EAA0E;IAC1E,wDAAwD;IACxD,mEAAmE,GACnEC,aAAa;IACb;;;;;;;GAOC,GACDC,OAAO;IACP;;;;;;GAMC,GACDC,WAAW;AACb,EAAU"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/api-routes.ts"],"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\n/**\n * Outreach's console API routes, as the dispatcher keys them (AGL-2974).\n *\n * One table both halves import, because the server registers the path and the\n * client fetches it, and a route spelled twice is a route that 404s the day\n * one spelling changes. Every path sits under the `outreach` prefix that\n * `plugins.config.json` gives this plugin, which is what lets the console's\n * `/api/[...pluginApi]` dispatcher gate a request on the plugin being enabled\n * and released before the handler runs. Client-safe: constants only.\n */\nexport const OUTREACH_API_ROUTES = {\n /** `GET` — proves the server bundle loaded and registered; see `server.ts`. */\n ping: 'outreach/ping',\n // Mailboxes (AGL-2978). Every one names its org (`orgId`, query or body)\n // so the dispatcher's release gate asks about that organization.\n /** `GET ?orgId` — whether this deployment can connect a Google mailbox. */\n mailboxesAvailability: 'outreach/mailboxes/availability',\n /** `POST` — Google's consent address for a new connect. */\n mailboxesConnect: 'outreach/mailboxes/connect',\n /**\n * `GET` — Google's redirect back. It carries no bearer token: the signed\n * state names the org and the member, and the route hands the code to the\n * Mailboxes page in a fragment rather than acting on it.\n */\n mailboxesOAuthCallback: 'outreach/mailboxes/oauth/callback',\n /** `POST` — finishes a connect with the member's own session. */\n mailboxesConnectComplete: 'outreach/mailboxes/connect/complete',\n /** `POST` — send-as, display name, daily cap, sending window, timezone. */\n mailboxesSettings: 'outreach/mailboxes/settings',\n /** `POST` — pause or resume. */\n mailboxesStatus: 'outreach/mailboxes/status',\n /** `POST` — a plain-text test to the account's own address. */\n mailboxesTest: 'outreach/mailboxes/test',\n /** `POST` — revoke the grant at Google and delete the mailbox. */\n mailboxesDisconnect: 'outreach/mailboxes/disconnect',\n // Settings, sequences and enrollments (AGL-2980). Every one names its org\n // (`orgId`, query or body); the contract is `model/outreach-api.ts`.\n /** `GET ?orgId` — the compliance settings; `POST` — save them. */\n settings: 'outreach/settings',\n /** `POST` — create a sequence, or save an edit to one. */\n sequencesSave: 'outreach/sequences/save',\n /** `POST` — activate, pause or archive a sequence. */\n sequencesStatus: 'outreach/sequences/status',\n /** `POST` — delete a draft nobody was enrolled in. */\n sequencesDelete: 'outreach/sequences/delete',\n /** `POST` — who a saved view or a search would enroll, and where each stands. */\n enrollPreview: 'outreach/enroll/preview',\n /** `POST` — enroll them, every gate checked again. */\n enroll: 'outreach/enroll',\n /** `POST` — pause, resume, stop, or mark do-not-contact. */\n enrollmentsAction: 'outreach/enrollments/action',\n /**\n * `GET ?orgId` — the domains on the organization's do-not-contact list\n * (AGL-3244); `POST` — add one, or take one off.\n */\n doNotContactDomains: 'outreach/do-not-contact/domains',\n /**\n * `GET ?orgId` — each connected mailbox domain's click-tracking host\n * (AGL-3306), `links.<domain>`; `POST` — set one up, check it, or remove\n * it. Setting up and removing are an owner's or admin's.\n */\n linkDomains: 'outreach/link-domains',\n /** `POST` — one email of a saved sequence, for a contact or a sample person. */\n preview: 'outreach/preview',\n /**\n * `POST` — one step of a saved sequence, rendered as the preview renders\n * it and sent through the sequence's mailbox to the member (AGL-3325):\n * `[Test]` in the subject, its links minted as test links, and counted\n * on nothing but the mailbox's test tally.\n */\n stepTest: 'outreach/steps/test',\n /**\n * `POST` — one person's own copies of a sequence's email steps, drafted by\n * the workspace's AI (AGL-3324): for a person about to be enrolled, or for\n * the next step of an enrollment. Nothing is stored.\n */\n curateDraft: 'outreach/curate/draft',\n /** `POST` — stores a member's confirmed copy of one step on an enrollment, or clears it. */\n curateSave: 'outreach/curate/save',\n // The one route a RECIPIENT reaches (AGL-2981): no session, a signed\n // token instead, and registered as a recipient link so it answers whether\n // or not Outreach is released for the organization now.\n /** `GET` and the RFC 8058 one-click `POST` — `?t=<signed token>`. */\n unsubscribe: 'outreach/unsubscribe',\n /**\n * `GET` — a link in a tracked sequence email (AGL-3239). `?t=<signed\n * token>`; answers a redirect to the destination the token names, and\n * records the click afterwards. A recipient link like the unsubscribe\n * one: no session, and it answers whether or not Outreach is released to\n * the organization now, because a link in an email that already left must\n * not stop working when a rollout is paused.\n */\n click: 'outreach/click',\n /**\n * `GET` — the short tracking link (AGL-3297): `outreach/l/<id>`, where the\n * id names a stored `outreachLinks` document holding the destination. It\n * replaced the signed `click` link for every send since; `click` keeps\n * answering the links already in inboxes. A recipient link, for `click`'s\n * reasons.\n */\n shortLink: 'outreach/l/:linkId',\n} as const\n"],"names":["OUTREACH_API_ROUTES","ping","mailboxesAvailability","mailboxesConnect","mailboxesOAuthCallback","mailboxesConnectComplete","mailboxesSettings","mailboxesStatus","mailboxesTest","mailboxesDisconnect","settings","sequencesSave","sequencesStatus","sequencesDelete","enrollPreview","enroll","enrollmentsAction","doNotContactDomains","linkDomains","preview","stepTest","curateDraft","curateSave","unsubscribe","click","shortLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GACD,OAAO,MAAMA,sBAAsB;IACjC,6EAA6E,GAC7EC,MAAM;IACN,yEAAyE;IACzE,iEAAiE;IACjE,yEAAyE,GACzEC,uBAAuB;IACvB,yDAAyD,GACzDC,kBAAkB;IAClB;;;;GAIC,GACDC,wBAAwB;IACxB,+DAA+D,GAC/DC,0BAA0B;IAC1B,yEAAyE,GACzEC,mBAAmB;IACnB,8BAA8B,GAC9BC,iBAAiB;IACjB,6DAA6D,GAC7DC,eAAe;IACf,gEAAgE,GAChEC,qBAAqB;IACrB,0EAA0E;IAC1E,qEAAqE;IACrE,gEAAgE,GAChEC,UAAU;IACV,wDAAwD,GACxDC,eAAe;IACf,oDAAoD,GACpDC,iBAAiB;IACjB,oDAAoD,GACpDC,iBAAiB;IACjB,+EAA+E,GAC/EC,eAAe;IACf,oDAAoD,GACpDC,QAAQ;IACR,0DAA0D,GAC1DC,mBAAmB;IACnB;;;GAGC,GACDC,qBAAqB;IACrB;;;;GAIC,GACDC,aAAa;IACb,8EAA8E,GAC9EC,SAAS;IACT;;;;;GAKC,GACDC,UAAU;IACV;;;;GAIC,GACDC,aAAa;IACb,0FAA0F,GAC1FC,YAAY;IACZ,qEAAqE;IACrE,0EAA0E;IAC1E,wDAAwD;IACxD,mEAAmE,GACnEC,aAAa;IACb;;;;;;;GAOC,GACDC,OAAO;IACP;;;;;;GAMC,GACDC,WAAW;AACb,EAAU"}
|