@aglyn/plugins-outreach 1.0.0-beta.220 → 1.0.0-beta.222
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/do-not-contact-domains.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.js.map +1 -0
|
@@ -9,14 +9,14 @@ export interface OutreachViewOption {
|
|
|
9
9
|
/** The most saved views the picker lists. */
|
|
10
10
|
export declare const OUTREACH_VIEWS_LIMIT = 50;
|
|
11
11
|
/**
|
|
12
|
-
* The saved Contacts views this reader may enroll from: their own
|
|
13
|
-
* shared ones — a colleague's private view is theirs
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the view shows.
|
|
12
|
+
* The saved Contacts and Leads views this reader may enroll from: their own
|
|
13
|
+
* and the shared ones — a colleague's private view is theirs, and the record
|
|
14
|
+
* system leaves it out — that it says can be taken whole as an audience. A
|
|
15
|
+
* view filtering on something enrolling cannot apply is not offered, because
|
|
16
|
+
* dropping that filter would enroll more people than the view shows.
|
|
17
17
|
*/
|
|
18
18
|
export declare function useOutreachSavedViews(orgId: string | null, uid: string | null): OutreachLoad<OutreachViewOption[]>;
|
|
19
|
-
/**
|
|
19
|
+
/** An email template the record system keeps, which a step can send the body of. */
|
|
20
20
|
export interface OutreachTemplateOption {
|
|
21
21
|
id: string;
|
|
22
22
|
name: string;
|
|
@@ -24,9 +24,9 @@ export interface OutreachTemplateOption {
|
|
|
24
24
|
body: string;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* The
|
|
28
|
-
* snippets
|
|
29
|
-
* organization or for this site.
|
|
27
|
+
* The email templates a step at this site can use: whole letters, not
|
|
28
|
+
* snippets — and, as the record system lists them, the team's and this
|
|
29
|
+
* reader's own, written for the whole organization or for this site.
|
|
30
30
|
*/
|
|
31
31
|
export declare function useOutreachEmailTemplates(orgId: string | null, hostId: string | null, uid: string | null): OutreachLoad<OutreachTemplateOption[]>;
|
|
32
32
|
/** A contact a search found. */
|
|
@@ -35,12 +35,14 @@ export interface OutreachContactOption {
|
|
|
35
35
|
name: string;
|
|
36
36
|
email: string | null;
|
|
37
37
|
}
|
|
38
|
+
/** The most templates the picker reads — the record system's own ceiling. */
|
|
39
|
+
export declare const OUTREACH_TEMPLATES_LIMIT = 200;
|
|
38
40
|
/** The most contacts one search answers with. */
|
|
39
41
|
export declare const OUTREACH_SEARCH_LIMIT = 25;
|
|
40
42
|
/**
|
|
41
|
-
* The contacts at `hostId` a search finds
|
|
42
|
-
* otherwise by a word of the name
|
|
43
|
-
* the
|
|
43
|
+
* The contacts at `hostId` a search finds — by address when the text is one,
|
|
44
|
+
* otherwise by a word of the name, the record system's own search — named as
|
|
45
|
+
* the sequence's consent group knows them. `idle` until something is typed.
|
|
44
46
|
*/
|
|
45
47
|
export declare function useOutreachContactSearch(input: {
|
|
46
48
|
orgId: string | null;
|
|
@@ -56,9 +58,10 @@ export declare const OUTREACH_LEADS_WINDOW = 200;
|
|
|
56
58
|
* The open leads at `hostId` a search finds (AGL-3234): the site's most
|
|
57
59
|
* recently seen leads, read once while the tab is open and narrowed in
|
|
58
60
|
* the browser by a word of the name or the address — a lead carries no
|
|
59
|
-
* search tokens, and the window is the Leads list's own.
|
|
60
|
-
*
|
|
61
|
-
* enroll. `idle` until something is
|
|
61
|
+
* search tokens, and the window is the Leads list's own. Leads the record
|
|
62
|
+
* system says are no longer open — converted or closed — are left out: they
|
|
63
|
+
* are a contact's, or a verdict's, to enroll. `idle` until something is
|
|
64
|
+
* typed.
|
|
62
65
|
*/
|
|
63
66
|
export declare function useOutreachLeadSearch(input: {
|
|
64
67
|
orgId: string | null;
|
|
@@ -15,35 +15,35 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
-
import {
|
|
19
|
-
import { CRM_COLLECTIONS, crmLeadDisplayName, crmViewIsListed, isCrmLeadOpen, normalizeCrmViewFilters } from "@aglyn/aglyn/app-utils/crm";
|
|
20
|
-
import { CRM_EMAIL_TEMPLATES_LIMIT, crmEmailTemplateIsListed, normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
21
|
-
import { dynamicListDimensionsForCrmView } from "@aglyn/aglyn/app-utils/dynamic-list-rule";
|
|
22
|
-
import { nameSearchToken } from "@aglyn/aglyn/app-utils/name-search";
|
|
23
|
-
import { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
24
|
-
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
18
|
+
import { pluginRecordListQuery, pluginRecordsFromRows } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
|
|
25
19
|
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
26
|
-
import {
|
|
20
|
+
import { getDocs, onSnapshot } from "firebase/firestore";
|
|
27
21
|
import { useEffect, useState } from "react";
|
|
28
22
|
/**
|
|
29
|
-
* What Outreach
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* What Outreach lists from the organization's record system (AGL-2980): the
|
|
24
|
+
* saved views people can be enrolled from, the email templates a step can
|
|
25
|
+
* send, and the people a search finds.
|
|
32
26
|
*
|
|
33
|
-
* Through the
|
|
34
|
-
*
|
|
35
|
-
* through
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
27
|
+
* Through the record system's own list sources (`plugin-record-lists`,
|
|
28
|
+
* AGL-3080) — the kinds `savedView`, `messageTemplate`, `contact` and `lead`
|
|
29
|
+
* — and never through its collections or its plugin, which a plugin may not
|
|
30
|
+
* import. The owner builds the query its security rules prove and applies
|
|
31
|
+
* its own listing rules to what comes back; with no plugin keeping people,
|
|
32
|
+
* each list reads as empty. The rules gate every one of these reads on
|
|
33
|
+
* `data.manage`, the same permission the enroll routes ask for, so a member
|
|
34
|
+
* who cannot read the people cannot enroll them either.
|
|
39
35
|
*/ const denied = (error)=>(error == null ? void 0 : error.code) === 'permission-denied';
|
|
36
|
+
/** A listener's documents as rows a list source reads back, the id beside the fields. */ const rowsOf = (snapshot)=>snapshot.docs.map((entry)=>_extends({}, entry.data(), {
|
|
37
|
+
$id: entry.id
|
|
38
|
+
}));
|
|
39
|
+
const text = (value)=>typeof value === 'string' ? value : '';
|
|
40
40
|
/** The most saved views the picker lists. */ export const OUTREACH_VIEWS_LIMIT = 50;
|
|
41
41
|
/**
|
|
42
|
-
* The saved Contacts views this reader may enroll from: their own
|
|
43
|
-
* shared ones — a colleague's private view is theirs
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* the view shows.
|
|
42
|
+
* The saved Contacts and Leads views this reader may enroll from: their own
|
|
43
|
+
* and the shared ones — a colleague's private view is theirs, and the record
|
|
44
|
+
* system leaves it out — that it says can be taken whole as an audience. A
|
|
45
|
+
* view filtering on something enrolling cannot apply is not offered, because
|
|
46
|
+
* dropping that filter would enroll more people than the view shows.
|
|
47
47
|
*/ export function useOutreachSavedViews(orgId, uid) {
|
|
48
48
|
const firestore = useFirestore();
|
|
49
49
|
const [result, setResult] = useState({
|
|
@@ -56,26 +56,28 @@ import { useEffect, useState } from "react";
|
|
|
56
56
|
data: []
|
|
57
57
|
});
|
|
58
58
|
if (!orgId) return undefined;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const request = {
|
|
60
|
+
orgId,
|
|
61
|
+
viewerUid: uid,
|
|
62
|
+
limit: OUTREACH_VIEWS_LIMIT
|
|
63
|
+
};
|
|
64
|
+
const views = pluginRecordListQuery('savedView', firestore, request);
|
|
65
|
+
if (!views) {
|
|
66
|
+
setResult({
|
|
67
|
+
status: 'ready',
|
|
68
|
+
data: []
|
|
69
|
+
});
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return onSnapshot(views, (snapshot)=>setResult({
|
|
63
73
|
status: 'ready',
|
|
64
|
-
data:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
ownerUid: String((_data_ownerUid = data['ownerUid']) != null ? _data_ownerUid : '')
|
|
72
|
-
}, uid) && // A Leads view narrows by status alone, which enrolling can
|
|
73
|
-
// always apply; a Contacts view must be readable as a list.
|
|
74
|
-
(data['section'] === 'leads' || dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters'])).unsupported.length === 0);
|
|
75
|
-
}).map(({ id, data })=>({
|
|
76
|
-
id,
|
|
77
|
-
name: String(data['name']),
|
|
78
|
-
section: data['section'] === 'leads' ? 'leads' : 'contacts'
|
|
74
|
+
data: pluginRecordsFromRows('savedView', rowsOf(snapshot), '$id', request)// A view the owner says cannot be taken whole is not offered:
|
|
75
|
+
// dropping the filter it could not apply would enroll more
|
|
76
|
+
// people than the view shows.
|
|
77
|
+
.filter((view)=>view.facts['takeable'] === true).map((view)=>({
|
|
78
|
+
id: view.id,
|
|
79
|
+
name: view.name,
|
|
80
|
+
section: view.facts['recordKind'] === 'lead' ? 'leads' : 'contacts'
|
|
79
81
|
}))
|
|
80
82
|
}), (error)=>{
|
|
81
83
|
if (!denied(error)) console.error('[outreach] saved views could not be read', error);
|
|
@@ -92,9 +94,9 @@ import { useEffect, useState } from "react";
|
|
|
92
94
|
return result;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
|
-
* The
|
|
96
|
-
* snippets
|
|
97
|
-
* organization or for this site.
|
|
97
|
+
* The email templates a step at this site can use: whole letters, not
|
|
98
|
+
* snippets — and, as the record system lists them, the team's and this
|
|
99
|
+
* reader's own, written for the whole organization or for this site.
|
|
98
100
|
*/ export function useOutreachEmailTemplates(orgId, hostId, uid) {
|
|
99
101
|
const firestore = useFirestore();
|
|
100
102
|
const [result, setResult] = useState({
|
|
@@ -107,16 +109,28 @@ import { useEffect, useState } from "react";
|
|
|
107
109
|
data: []
|
|
108
110
|
});
|
|
109
111
|
if (!orgId) return undefined;
|
|
110
|
-
|
|
112
|
+
const request = {
|
|
113
|
+
orgId,
|
|
114
|
+
hostId,
|
|
115
|
+
viewerUid: uid,
|
|
116
|
+
limit: OUTREACH_TEMPLATES_LIMIT
|
|
117
|
+
};
|
|
118
|
+
const templates = pluginRecordListQuery('messageTemplate', firestore, request);
|
|
119
|
+
if (!templates) {
|
|
120
|
+
setResult({
|
|
111
121
|
status: 'ready',
|
|
112
|
-
data:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
data: []
|
|
123
|
+
});
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return onSnapshot(templates, (snapshot)=>setResult({
|
|
127
|
+
status: 'ready',
|
|
128
|
+
data: pluginRecordsFromRows('messageTemplate', rowsOf(snapshot), '$id', request)// Whole letters, not snippets.
|
|
129
|
+
.filter((template)=>template.facts['kind'] === 'template').map((template)=>({
|
|
130
|
+
id: template.id,
|
|
117
131
|
name: template.name,
|
|
118
|
-
subject: template.subject,
|
|
119
|
-
body: template.body
|
|
132
|
+
subject: text(template.facts['subject']),
|
|
133
|
+
body: text(template.facts['body'])
|
|
120
134
|
}))
|
|
121
135
|
}), (error)=>{
|
|
122
136
|
if (!denied(error)) console.error('[outreach] email templates could not be read', error);
|
|
@@ -133,48 +147,55 @@ import { useEffect, useState } from "react";
|
|
|
133
147
|
]);
|
|
134
148
|
return result;
|
|
135
149
|
}
|
|
150
|
+
/** The most templates the picker reads — the record system's own ceiling. */ export const OUTREACH_TEMPLATES_LIMIT = 200;
|
|
136
151
|
/** The most contacts one search answers with. */ export const OUTREACH_SEARCH_LIMIT = 25;
|
|
137
152
|
/**
|
|
138
|
-
* The contacts at `hostId` a search finds
|
|
139
|
-
* otherwise by a word of the name
|
|
140
|
-
* the
|
|
153
|
+
* The contacts at `hostId` a search finds — by address when the text is one,
|
|
154
|
+
* otherwise by a word of the name, the record system's own search — named as
|
|
155
|
+
* the sequence's consent group knows them. `idle` until something is typed.
|
|
141
156
|
*/ export function useOutreachContactSearch(input) {
|
|
142
|
-
const { orgId, hostId, contactGroupId
|
|
157
|
+
const { orgId, hostId, contactGroupId } = input;
|
|
143
158
|
const firestore = useFirestore();
|
|
144
159
|
const [result, setResult] = useState({
|
|
145
160
|
status: 'ready',
|
|
146
161
|
data: []
|
|
147
162
|
});
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
163
|
+
const search = input.text;
|
|
164
|
+
const request = orgId && hostId ? {
|
|
165
|
+
orgId,
|
|
166
|
+
hostId,
|
|
167
|
+
consentGroupId: contactGroupId,
|
|
168
|
+
search,
|
|
169
|
+
limit: OUTREACH_SEARCH_LIMIT
|
|
170
|
+
} : null;
|
|
171
|
+
// Nothing to look for — no site, nothing typed, or no record system — is
|
|
172
|
+
// idle rather than an empty answer.
|
|
173
|
+
const idle = !request || !pluginRecordListQuery('contact', firestore, request);
|
|
151
174
|
useEffect(()=>{
|
|
152
|
-
if (idle || !
|
|
175
|
+
if (idle || !request) {
|
|
153
176
|
setResult({
|
|
154
177
|
status: 'ready',
|
|
155
178
|
data: []
|
|
156
179
|
});
|
|
157
180
|
return undefined;
|
|
158
181
|
}
|
|
182
|
+
const contacts = pluginRecordListQuery('contact', firestore, request);
|
|
183
|
+
if (!contacts) return undefined;
|
|
159
184
|
let current = true;
|
|
160
185
|
setResult((previous)=>({
|
|
161
186
|
status: 'loading',
|
|
162
187
|
data: previous.data
|
|
163
188
|
}));
|
|
164
|
-
const contacts = collection(firestore, 'orgs', orgId, 'contacts');
|
|
165
189
|
// Settles the typing before it reads: one query per pause, not per key.
|
|
166
190
|
const timer = setTimeout(()=>{
|
|
167
|
-
getDocs(
|
|
191
|
+
getDocs(contacts).then((snapshot)=>{
|
|
168
192
|
if (!current) return;
|
|
169
193
|
setResult({
|
|
170
194
|
status: 'ready',
|
|
171
|
-
data: snapshot.
|
|
172
|
-
id:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
id,
|
|
176
|
-
name: contactDisplayName(data, contactGroupId != null ? contactGroupId : hostId).trim(),
|
|
177
|
-
email: normalizeContactEmail(data['email'])
|
|
195
|
+
data: pluginRecordsFromRows('contact', rowsOf(snapshot), '$id', request).map((contact)=>({
|
|
196
|
+
id: contact.id,
|
|
197
|
+
name: contact.name,
|
|
198
|
+
email: text(contact.facts['email']) || null
|
|
178
199
|
}))
|
|
179
200
|
});
|
|
180
201
|
}).catch((error)=>{
|
|
@@ -190,13 +211,14 @@ import { useEffect, useState } from "react";
|
|
|
190
211
|
current = false;
|
|
191
212
|
clearTimeout(timer);
|
|
192
213
|
};
|
|
214
|
+
// The request is data; these are its parts.
|
|
215
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
193
216
|
}, [
|
|
194
217
|
firestore,
|
|
195
218
|
orgId,
|
|
196
219
|
hostId,
|
|
197
220
|
contactGroupId,
|
|
198
|
-
|
|
199
|
-
token,
|
|
221
|
+
search,
|
|
200
222
|
idle
|
|
201
223
|
]);
|
|
202
224
|
return _extends({}, result, {
|
|
@@ -208,11 +230,12 @@ import { useEffect, useState } from "react";
|
|
|
208
230
|
* The open leads at `hostId` a search finds (AGL-3234): the site's most
|
|
209
231
|
* recently seen leads, read once while the tab is open and narrowed in
|
|
210
232
|
* the browser by a word of the name or the address — a lead carries no
|
|
211
|
-
* search tokens, and the window is the Leads list's own.
|
|
212
|
-
*
|
|
213
|
-
* enroll. `idle` until something is
|
|
233
|
+
* search tokens, and the window is the Leads list's own. Leads the record
|
|
234
|
+
* system says are no longer open — converted or closed — are left out: they
|
|
235
|
+
* are a contact's, or a verdict's, to enroll. `idle` until something is
|
|
236
|
+
* typed.
|
|
214
237
|
*/ export function useOutreachLeadSearch(input) {
|
|
215
|
-
const { orgId, hostId, text, enabled } = input;
|
|
238
|
+
const { orgId, hostId, text: typed, enabled } = input;
|
|
216
239
|
const firestore = useFirestore();
|
|
217
240
|
const [window, setWindow] = useState({
|
|
218
241
|
status: 'ready',
|
|
@@ -226,25 +249,33 @@ import { useEffect, useState } from "react";
|
|
|
226
249
|
});
|
|
227
250
|
return undefined;
|
|
228
251
|
}
|
|
252
|
+
const request = {
|
|
253
|
+
orgId,
|
|
254
|
+
hostId,
|
|
255
|
+
limit: OUTREACH_LEADS_WINDOW
|
|
256
|
+
};
|
|
257
|
+
const leads = pluginRecordListQuery('lead', firestore, request);
|
|
258
|
+
if (!leads) {
|
|
259
|
+
setWindow({
|
|
260
|
+
status: 'ready',
|
|
261
|
+
data: []
|
|
262
|
+
});
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
229
265
|
let current = true;
|
|
230
266
|
setWindow({
|
|
231
267
|
status: 'loading',
|
|
232
268
|
data: []
|
|
233
269
|
});
|
|
234
|
-
getDocs(
|
|
235
|
-
// (AGL-3275) — unscoped it would offer one agency client another
|
|
236
|
-
// client's people to enroll.
|
|
237
|
-
collection(firestore, 'orgs', orgId, 'leads'), where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)), orderBy('lastSeenAtMs', 'desc'), limit(OUTREACH_LEADS_WINDOW))).then((snapshot)=>{
|
|
270
|
+
getDocs(leads).then((snapshot)=>{
|
|
238
271
|
if (!current) return;
|
|
239
272
|
setWindow({
|
|
240
273
|
status: 'ready',
|
|
241
|
-
data:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
name: crmLeadDisplayName(data),
|
|
247
|
-
email: normalizeContactEmail(data['email'])
|
|
274
|
+
data: pluginRecordsFromRows('lead', rowsOf(snapshot), '$id', request)// Converted and closed leads are a contact's, or a verdict's, to enroll.
|
|
275
|
+
.filter((lead)=>lead.facts['open'] === true).map((lead)=>({
|
|
276
|
+
id: lead.id,
|
|
277
|
+
name: lead.name,
|
|
278
|
+
email: text(lead.facts['email']) || null
|
|
248
279
|
}))
|
|
249
280
|
});
|
|
250
281
|
}).catch((error)=>{
|
|
@@ -264,7 +295,7 @@ import { useEffect, useState } from "react";
|
|
|
264
295
|
hostId,
|
|
265
296
|
enabled
|
|
266
297
|
]);
|
|
267
|
-
const needle =
|
|
298
|
+
const needle = typed.trim().toLowerCase();
|
|
268
299
|
const idle = !hostId || !needle;
|
|
269
300
|
const data = idle ? [] : window.data.filter((lead)=>{
|
|
270
301
|
var _lead_email;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-crm.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 {\n contactDisplayName,\n normalizeContactEmail,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n CRM_COLLECTIONS,\n crmLeadDisplayName,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CRM_EMAIL_TEMPLATES_LIMIT,\n crmEmailTemplateIsListed,\n normalizeCrmEmailTemplate,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport { nameSearchToken } from '@aglyn/aglyn/app-utils/name-search'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport type { OutreachLoad } from './use-outreach-data'\n\n/**\n * What Outreach reads from the organization's CRM (AGL-2980): the saved\n * Contacts views people can be enrolled from, the email templates a step\n * can send, and the contacts a search finds.\n *\n * Through the core's own CRM model — the collection names, the listing\n * rules, the view translation the dynamic-list sweep uses — and never\n * through the CRM plugin, which a plugin may not import. The Firestore\n * rules gate every one of these reads on `data.manage`, the same permission\n * the enroll routes ask for, so a member who cannot read the people cannot\n * enroll them either.\n */\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\n/** A saved Contacts or Leads view people can be enrolled from (AGL-3234). */\nexport interface OutreachViewOption {\n id: string\n name: string\n /** Which list the view is of; a Leads view selects the sequence's site's leads. */\n section: 'contacts' | 'leads'\n}\n\n/** The most saved views the picker lists. */\nexport const OUTREACH_VIEWS_LIMIT = 50\n\n/**\n * The saved Contacts views this reader may enroll from: their own and the\n * shared ones — a colleague's private view is theirs — that translate whole\n * into an audience. A view filtering on something enrolling cannot apply is\n * not offered, because dropping that filter would enroll more people than\n * the view shows.\n */\nexport function useOutreachSavedViews(\n orgId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachViewOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachViewOption[]>>({\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, CRM_COLLECTIONS.views),\n where('section', 'in', ['contacts', 'leads']),\n orderBy('name'),\n limit(OUTREACH_VIEWS_LIMIT),\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n data: entry.data() as Record<string, unknown>,\n }))\n .filter(\n ({ data }) =>\n typeof data['name'] === 'string' &&\n data['name'] !== '' &&\n crmViewIsListed(\n {\n shared: data['shared'] === true,\n ownerUid: String(data['ownerUid'] ?? ''),\n },\n uid,\n ) &&\n // A Leads view narrows by status alone, which enrolling can\n // always apply; a Contacts view must be readable as a list.\n (data['section'] === 'leads' ||\n dynamicListDimensionsForCrmView(\n normalizeCrmViewFilters(data['filters']),\n ).unsupported.length === 0),\n )\n .map(({ id, data }) => ({\n id,\n name: String(data['name']),\n section: data['section'] === 'leads' ? ('leads' as const) : ('contacts' as const),\n })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] saved views could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, uid])\n return result\n}\n\n/** A CRM email template a step can send the body of. */\nexport interface OutreachTemplateOption {\n id: string\n name: string\n subject: string\n body: string\n}\n\n/**\n * The CRM email templates a step at this site can use: whole letters, not\n * snippets; the team's and this reader's own; written for the whole\n * organization or for this site.\n */\nexport function useOutreachEmailTemplates(\n orgId: string | null,\n hostId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachTemplateOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachTemplateOption[]>>({\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, CRM_COLLECTIONS.emailTemplates),\n orderBy('name'),\n limit(CRM_EMAIL_TEMPLATES_LIMIT),\n ),\n (snapshot) =>\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n template: normalizeCrmEmailTemplate(entry.data()),\n }))\n .filter(\n ({ template }) =>\n template.kind === 'template' &&\n crmEmailTemplateIsListed(template, uid) &&\n (!hostId || visibleToHost(template.visibleTo, hostId)),\n )\n .map(({ id, template }) => ({\n id,\n name: template.name,\n subject: template.subject,\n body: template.body,\n })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] email templates could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, hostId, uid])\n return result\n}\n\n/** A contact a search found. */\nexport interface OutreachContactOption {\n id: string\n name: string\n email: string | null\n}\n\n/** The most contacts one search answers with. */\nexport const OUTREACH_SEARCH_LIMIT = 25\n\n/**\n * The contacts at `hostId` a search finds: by address when the text is one,\n * otherwise by a word of the name — the `nameTokens` the contacts carry for\n * the CRM's own search. `idle` until something is typed.\n */\nexport function useOutreachContactSearch(input: {\n orgId: string | null\n hostId: string | null\n contactGroupId: string | null\n text: string\n}): OutreachLoad<OutreachContactOption[]> & { idle: boolean } {\n const { orgId, hostId, contactGroupId, text } = input\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachContactOption[]>>({\n status: 'ready',\n data: [],\n })\n const email = normalizeContactEmail(text)\n const token = email ? '' : nameSearchToken(text)\n const idle = !orgId || !hostId || (!email && !token)\n useEffect(() => {\n if (idle || !orgId || !hostId) {\n setResult({ status: 'ready', data: [] })\n return undefined\n }\n let current = true\n setResult((previous) => ({ status: 'loading', data: previous.data }))\n const contacts = collection(firestore, 'orgs', orgId, 'contacts')\n // Settles the typing before it reads: one query per pause, not per key.\n const timer = setTimeout(() => {\n getDocs(\n email\n ? query(\n contacts,\n where('email', '==', email),\n limit(OUTREACH_SEARCH_LIMIT),\n )\n : query(\n contacts,\n where('nameTokens', 'array-contains', token),\n orderBy('nameLower'),\n limit(OUTREACH_SEARCH_LIMIT),\n ),\n )\n .then((snapshot) => {\n if (!current) return\n setResult({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({\n id: entry.id,\n data: entry.data() as Record<string, unknown>,\n }))\n .filter(({ data }) =>\n visibleToHost(\n data['visibleTo'] as string[] | undefined,\n hostId,\n ),\n )\n .map(({ id, data }) => ({\n id,\n name: contactDisplayName(data, contactGroupId ?? hostId).trim(),\n email: normalizeContactEmail(data['email']),\n })),\n })\n })\n .catch((error: unknown) => {\n if (!current) return\n if (!denied(error))\n console.error('[outreach] the contact search failed', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n })\n }, 300)\n return () => {\n current = false\n clearTimeout(timer)\n }\n }, [firestore, orgId, hostId, contactGroupId, email, token, idle])\n return { ...result, idle }\n}\n\n/** The most leads the picker reads — the Leads list's own window. */\nexport const OUTREACH_LEADS_WINDOW = 200\n\n/**\n * The open leads at `hostId` a search finds (AGL-3234): the site's most\n * recently seen leads, read once while the tab is open and narrowed in\n * the browser by a word of the name or the address — a lead carries no\n * search tokens, and the window is the Leads list's own. Converted and\n * closed leads are left out: they are a contact's, or a verdict's, to\n * enroll. `idle` until something is typed.\n */\nexport function useOutreachLeadSearch(input: {\n orgId: string | null\n hostId: string | null\n text: string\n enabled: boolean\n}): OutreachLoad<OutreachContactOption[]> & { idle: boolean } {\n const { orgId, hostId, text, enabled } = input\n const firestore = useFirestore()\n const [window, setWindow] = useState<OutreachLoad<OutreachContactOption[]>>({\n status: 'ready',\n data: [],\n })\n useEffect(() => {\n if (!enabled || !hostId || !orgId) {\n setWindow({ status: 'ready', data: [] })\n return undefined\n }\n let current = true\n setWindow({ status: 'loading', data: [] })\n getDocs(\n query(\n // The org collection, narrowed to the site this view belongs to\n // (AGL-3275) — unscoped it would offer one agency client another\n // client's people to enroll.\n collection(firestore, 'orgs', orgId, 'leads'),\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n orderBy('lastSeenAtMs', 'desc'),\n limit(OUTREACH_LEADS_WINDOW),\n ),\n )\n .then((snapshot) => {\n if (!current) return\n setWindow({\n status: 'ready',\n data: snapshot.docs\n .map((entry) => ({ id: entry.id, data: entry.data() as Record<string, unknown> }))\n .filter(({ data }) => isCrmLeadOpen(data as never) && !data['convertedContactId'])\n .map(({ id, data }) => ({\n id,\n name: crmLeadDisplayName(data),\n email: normalizeContactEmail(data['email']),\n })),\n })\n })\n .catch((error: unknown) => {\n if (!current) return\n if (!denied(error)) console.error('[outreach] the leads could not be read', error)\n setWindow({ status: denied(error) ? 'refused' : 'error', data: [] })\n })\n return () => {\n current = false\n }\n }, [firestore, orgId, hostId, enabled])\n const needle = text.trim().toLowerCase()\n const idle = !hostId || !needle\n const data = idle\n ? []\n : window.data\n .filter(\n (lead) =>\n lead.name.toLowerCase().includes(needle) ||\n (lead.email ?? '').includes(needle),\n )\n .slice(0, OUTREACH_SEARCH_LIMIT)\n return { ...window, data, idle }\n}\n"],"names":["contactDisplayName","normalizeContactEmail","CRM_COLLECTIONS","crmLeadDisplayName","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","CRM_EMAIL_TEMPLATES_LIMIT","crmEmailTemplateIsListed","normalizeCrmEmailTemplate","dynamicListDimensionsForCrmView","nameSearchToken","scopeTokensForHost","visibleToHost","useFirestore","collection","getDocs","limit","onSnapshot","orderBy","query","where","useEffect","useState","denied","error","code","OUTREACH_VIEWS_LIMIT","useOutreachSavedViews","orgId","uid","firestore","result","setResult","status","data","undefined","views","snapshot","docs","map","entry","id","filter","shared","ownerUid","String","unsupported","length","name","section","console","useOutreachEmailTemplates","hostId","emailTemplates","template","kind","visibleTo","subject","body","OUTREACH_SEARCH_LIMIT","useOutreachContactSearch","input","contactGroupId","text","email","token","idle","current","previous","contacts","timer","setTimeout","then","trim","catch","clearTimeout","OUTREACH_LEADS_WINDOW","useOutreachLeadSearch","enabled","window","setWindow","needle","toLowerCase","lead","includes","slice"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-crm.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 {\n pluginRecordListQuery,\n pluginRecordsFromRows,\n type PluginRecordListRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { getDocs, onSnapshot, type DocumentData, type QuerySnapshot } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport type { OutreachLoad } from './use-outreach-data'\n\n/**\n * What Outreach lists from the organization's record system (AGL-2980): the\n * saved views people can be enrolled from, the email templates a step can\n * send, and the people a search finds.\n *\n * Through the record system's own list sources (`plugin-record-lists`,\n * AGL-3080) — the kinds `savedView`, `messageTemplate`, `contact` and `lead`\n * — and never through its collections or its plugin, which a plugin may not\n * import. The owner builds the query its security rules prove and applies\n * its own listing rules to what comes back; with no plugin keeping people,\n * each list reads as empty. The rules gate every one of these reads on\n * `data.manage`, the same permission the enroll routes ask for, so a member\n * who cannot read the people cannot enroll them either.\n */\n\nconst denied = (error: unknown) =>\n (error as { code?: unknown } | null)?.code === 'permission-denied'\n\n/** A listener's documents as rows a list source reads back, the id beside the fields. */\nconst rowsOf = (snapshot: QuerySnapshot<DocumentData>) =>\n snapshot.docs.map((entry) => ({ ...entry.data(), $id: entry.id }))\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A saved Contacts or Leads view people can be enrolled from (AGL-3234). */\nexport interface OutreachViewOption {\n id: string\n name: string\n /** Which list the view is of; a Leads view selects the sequence's site's leads. */\n section: 'contacts' | 'leads'\n}\n\n/** The most saved views the picker lists. */\nexport const OUTREACH_VIEWS_LIMIT = 50\n\n/**\n * The saved Contacts and Leads views this reader may enroll from: their own\n * and the shared ones — a colleague's private view is theirs, and the record\n * system leaves it out — that it says can be taken whole as an audience. A\n * view filtering on something enrolling cannot apply is not offered, because\n * dropping that filter would enroll more people than the view shows.\n */\nexport function useOutreachSavedViews(\n orgId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachViewOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachViewOption[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n const request: PluginRecordListRequest = { orgId, viewerUid: uid, limit: OUTREACH_VIEWS_LIMIT }\n const views = pluginRecordListQuery('savedView', firestore, request)\n if (!views) {\n setResult({ status: 'ready', data: [] })\n return undefined\n }\n return onSnapshot(\n views,\n (snapshot) =>\n setResult({\n status: 'ready',\n data: pluginRecordsFromRows('savedView', rowsOf(snapshot), '$id', request)\n // A view the owner says cannot be taken whole is not offered:\n // dropping the filter it could not apply would enroll more\n // people than the view shows.\n .filter((view) => view.facts['takeable'] === true)\n .map((view) => ({\n id: view.id,\n name: view.name,\n section: view.facts['recordKind'] === 'lead' ? ('leads' as const) : ('contacts' as const),\n })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] saved views could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, uid])\n return result\n}\n\n/** An email template the record system keeps, which a step can send the body of. */\nexport interface OutreachTemplateOption {\n id: string\n name: string\n subject: string\n body: string\n}\n\n/**\n * The email templates a step at this site can use: whole letters, not\n * snippets — and, as the record system lists them, the team's and this\n * reader's own, written for the whole organization or for this site.\n */\nexport function useOutreachEmailTemplates(\n orgId: string | null,\n hostId: string | null,\n uid: string | null,\n): OutreachLoad<OutreachTemplateOption[]> {\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachTemplateOption[]>>({\n status: 'loading',\n data: [],\n })\n useEffect(() => {\n setResult({ status: 'loading', data: [] })\n if (!orgId) return undefined\n const request: PluginRecordListRequest = {\n orgId,\n hostId,\n viewerUid: uid,\n limit: OUTREACH_TEMPLATES_LIMIT,\n }\n const templates = pluginRecordListQuery('messageTemplate', firestore, request)\n if (!templates) {\n setResult({ status: 'ready', data: [] })\n return undefined\n }\n return onSnapshot(\n templates,\n (snapshot) =>\n setResult({\n status: 'ready',\n data: pluginRecordsFromRows('messageTemplate', rowsOf(snapshot), '$id', request)\n // Whole letters, not snippets.\n .filter((template) => template.facts['kind'] === 'template')\n .map((template) => ({\n id: template.id,\n name: template.name,\n subject: text(template.facts['subject']),\n body: text(template.facts['body']),\n })),\n }),\n (error) => {\n if (!denied(error))\n console.error('[outreach] email templates could not be read', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n },\n )\n }, [firestore, orgId, hostId, uid])\n return result\n}\n\n/** A contact a search found. */\nexport interface OutreachContactOption {\n id: string\n name: string\n email: string | null\n}\n\n/** The most templates the picker reads — the record system's own ceiling. */\nexport const OUTREACH_TEMPLATES_LIMIT = 200\n\n/** The most contacts one search answers with. */\nexport const OUTREACH_SEARCH_LIMIT = 25\n\n/**\n * The contacts at `hostId` a search finds — by address when the text is one,\n * otherwise by a word of the name, the record system's own search — named as\n * the sequence's consent group knows them. `idle` until something is typed.\n */\nexport function useOutreachContactSearch(input: {\n orgId: string | null\n hostId: string | null\n contactGroupId: string | null\n text: string\n}): OutreachLoad<OutreachContactOption[]> & { idle: boolean } {\n const { orgId, hostId, contactGroupId } = input\n const firestore = useFirestore()\n const [result, setResult] = useState<OutreachLoad<OutreachContactOption[]>>({\n status: 'ready',\n data: [],\n })\n const search = input.text\n const request: PluginRecordListRequest | null =\n orgId && hostId\n ? { orgId, hostId, consentGroupId: contactGroupId, search, limit: OUTREACH_SEARCH_LIMIT }\n : null\n // Nothing to look for — no site, nothing typed, or no record system — is\n // idle rather than an empty answer.\n const idle = !request || !pluginRecordListQuery('contact', firestore, request)\n useEffect(() => {\n if (idle || !request) {\n setResult({ status: 'ready', data: [] })\n return undefined\n }\n const contacts = pluginRecordListQuery('contact', firestore, request)\n if (!contacts) return undefined\n let current = true\n setResult((previous) => ({ status: 'loading', data: previous.data }))\n // Settles the typing before it reads: one query per pause, not per key.\n const timer = setTimeout(() => {\n getDocs(contacts)\n .then((snapshot) => {\n if (!current) return\n setResult({\n status: 'ready',\n data: pluginRecordsFromRows('contact', rowsOf(snapshot), '$id', request).map((contact) => ({\n id: contact.id,\n name: contact.name,\n email: text(contact.facts['email']) || null,\n })),\n })\n })\n .catch((error: unknown) => {\n if (!current) return\n if (!denied(error))\n console.error('[outreach] the contact search failed', error)\n setResult({ status: denied(error) ? 'refused' : 'error', data: [] })\n })\n }, 300)\n return () => {\n current = false\n clearTimeout(timer)\n }\n // The request is data; these are its parts.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [firestore, orgId, hostId, contactGroupId, search, idle])\n return { ...result, idle }\n}\n\n/** The most leads the picker reads — the Leads list's own window. */\nexport const OUTREACH_LEADS_WINDOW = 200\n\n/**\n * The open leads at `hostId` a search finds (AGL-3234): the site's most\n * recently seen leads, read once while the tab is open and narrowed in\n * the browser by a word of the name or the address — a lead carries no\n * search tokens, and the window is the Leads list's own. Leads the record\n * system says are no longer open — converted or closed — are left out: they\n * are a contact's, or a verdict's, to enroll. `idle` until something is\n * typed.\n */\nexport function useOutreachLeadSearch(input: {\n orgId: string | null\n hostId: string | null\n text: string\n enabled: boolean\n}): OutreachLoad<OutreachContactOption[]> & { idle: boolean } {\n const { orgId, hostId, text: typed, enabled } = input\n const firestore = useFirestore()\n const [window, setWindow] = useState<OutreachLoad<OutreachContactOption[]>>({\n status: 'ready',\n data: [],\n })\n useEffect(() => {\n if (!enabled || !hostId || !orgId) {\n setWindow({ status: 'ready', data: [] })\n return undefined\n }\n const request: PluginRecordListRequest = { orgId, hostId, limit: OUTREACH_LEADS_WINDOW }\n const leads = pluginRecordListQuery('lead', firestore, request)\n if (!leads) {\n setWindow({ status: 'ready', data: [] })\n return undefined\n }\n let current = true\n setWindow({ status: 'loading', data: [] })\n getDocs(leads)\n .then((snapshot) => {\n if (!current) return\n setWindow({\n status: 'ready',\n data: pluginRecordsFromRows('lead', rowsOf(snapshot), '$id', request)\n // Converted and closed leads are a contact's, or a verdict's, to enroll.\n .filter((lead) => lead.facts['open'] === true)\n .map((lead) => ({\n id: lead.id,\n name: lead.name,\n email: text(lead.facts['email']) || null,\n })),\n })\n })\n .catch((error: unknown) => {\n if (!current) return\n if (!denied(error)) console.error('[outreach] the leads could not be read', error)\n setWindow({ status: denied(error) ? 'refused' : 'error', data: [] })\n })\n return () => {\n current = false\n }\n }, [firestore, orgId, hostId, enabled])\n const needle = typed.trim().toLowerCase()\n const idle = !hostId || !needle\n const data = idle\n ? []\n : window.data\n .filter(\n (lead) =>\n lead.name.toLowerCase().includes(needle) ||\n (lead.email ?? '').includes(needle),\n )\n .slice(0, OUTREACH_SEARCH_LIMIT)\n return { ...window, data, idle }\n}\n"],"names":["pluginRecordListQuery","pluginRecordsFromRows","useFirestore","getDocs","onSnapshot","useEffect","useState","denied","error","code","rowsOf","snapshot","docs","map","entry","data","$id","id","text","value","OUTREACH_VIEWS_LIMIT","useOutreachSavedViews","orgId","uid","firestore","result","setResult","status","undefined","request","viewerUid","limit","views","filter","view","facts","name","section","console","useOutreachEmailTemplates","hostId","OUTREACH_TEMPLATES_LIMIT","templates","template","subject","body","OUTREACH_SEARCH_LIMIT","useOutreachContactSearch","input","contactGroupId","search","consentGroupId","idle","contacts","current","previous","timer","setTimeout","then","contact","email","catch","clearTimeout","OUTREACH_LEADS_WINDOW","useOutreachLeadSearch","typed","enabled","window","setWindow","leads","lead","needle","trim","toLowerCase","includes","slice"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SendLinkRollup } from '@aglyn/shared-ui-email-campaigns/model/send-report';
|
|
2
2
|
import type { ListQueryPlan, ListQueryRequest } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
3
3
|
import { type OutreachDoNotContactDomainEntry, type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
|
|
4
4
|
/**
|
|
@@ -50,7 +50,7 @@ export declare function useOutreachSequence(orgId: string | null, sequenceId: st
|
|
|
50
50
|
* table is not a read of every enrollment. A sequence that tracks nothing
|
|
51
51
|
* never has one, and an absent document is not an error.
|
|
52
52
|
*/
|
|
53
|
-
export type OutreachSequenceLinksLoad = OutreachLoad<
|
|
53
|
+
export type OutreachSequenceLinksLoad = OutreachLoad<SendLinkRollup | null>;
|
|
54
54
|
export declare function useOutreachSequenceLinks(orgId: string | null, sequenceId: string | null): OutreachSequenceLinksLoad;
|
|
55
55
|
/**
|
|
56
56
|
* The domains on the organization's do-not-contact list (AGL-3244),
|
|
@@ -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 type {\n ListQueryDeclaration,\n ListQueryPlan,\n ListQueryRequest,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n collection,\n doc,\n type DocumentData,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n type Query,\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'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_ENROLLMENT_LIST_QUERY,\n outreachEnrollmentListBase,\n} from '../model/enrollment-list-query'\nimport { OUTREACH_SEQUENCE_LIST_QUERY } from '../model/sequence-list-query'\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/**\n * One page of a list whose every filter and search word is on its Firestore\n * query (AGL-3321): the rows of the page on screen, whether another page\n * exists, the pager, and the plan — what the query holds and what it could\n * not take, which the list says above itself.\n */\nexport interface OutreachListPage<T> {\n status: OutreachLoadStatus\n rows: T[]\n hasMore: boolean\n page: number\n setPage(page: number): void\n pageSize: number\n setPageSize(pageSize: number): void\n plan: ListQueryPlan\n}\n\n/**\n * A list read through `useListQuery`, in this module's four states and with\n * each document read through its stored-record reader. A document the reader\n * cannot make sense of is left out, as every Outreach read leaves it out.\n */\nfunction useOutreachListQuery<T>(options: {\n collection: Query<DocumentData> | null\n declaration: ListQueryDeclaration\n request: ListQueryRequest\n deps: readonly unknown[]\n read(id: string, data: DocumentData): T | null\n what: string\n}): OutreachListPage<T> {\n const listed = useListQuery<DocumentData & { $id: string }>({\n collection: options.collection,\n declaration: options.declaration,\n request: options.request,\n deps: options.deps,\n idField: '$id',\n })\n const { read, what } = options\n const rows = useMemo(\n () =>\n listed.rows\n .map((row) => read(String(row.$id), row))\n .filter((row): row is NonNullable<typeof row> => row !== null) as T[],\n [listed.rows, read],\n )\n const refused = listed.serverDenied || denied(listed.error)\n useEffect(() => {\n if (listed.status === 'error' && !refused) {\n console.error(`[outreach] ${what} could not be read`, listed.error)\n }\n }, [listed.status, listed.error, refused, what])\n return {\n status:\n listed.status === 'loading'\n ? 'loading'\n : listed.status === 'error'\n ? refused\n ? 'refused'\n : 'error'\n : 'ready',\n rows,\n hasMore: listed.hasMore,\n page: listed.page,\n setPage: listed.setPage,\n pageSize: listed.pageSize,\n setPageSize: listed.setPageSize,\n plan: listed.plan,\n }\n}\n\nconst readSequence = (id: string, data: DocumentData) =>\n readStoredOutreachSequence(id, data)\n\n/**\n * The organization's sequences, newest first, one page at a time — each\n * clause and the search on the query (`OUTREACH_SEQUENCE_LIST_QUERY`).\n */\nexport function useOutreachSequenceList(\n orgId: string | null,\n request: ListQueryRequest,\n): OutreachListPage<OutreachSequence> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection: orgId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences)\n : null,\n declaration: OUTREACH_SEQUENCE_LIST_QUERY,\n request,\n deps: [firestore, orgId],\n read: readSequence,\n what: 'sequences',\n })\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\nconst readDomain = (id: string, data: DocumentData): OutreachDoNotContactDomainEntry => ({\n ...(data as OutreachDoNotContactDomainEntry),\n domain: id,\n})\n\n/**\n * The domains on the organization's do-not-contact list (AGL-3244),\n * alphabetically — the document id is the domain — one page at a time, live:\n * an add or a remove through the route shows up without a reload, and so\n * does a domain the sending runtime files after a gateway block. Each clause\n * and the search are on the query (`OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`).\n */\nexport function useOutreachDoNotContactDomainList(\n orgId: string | null,\n request: ListQueryRequest,\n): OutreachListPage<OutreachDoNotContactDomainEntry> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection: orgId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.doNotContactDomains)\n : null,\n declaration: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY,\n request,\n deps: [firestore, orgId],\n read: readDomain,\n what: 'the do-not-contact domains',\n })\n}\n\nconst readEnrollment = (id: string, data: DocumentData) =>\n readStoredOutreachEnrollment(id, data)\n\n/**\n * A sequence's enrollments, the most recently enrolled first, one page at a\n * time — the sequence is the query's base (`sequenceId ==`), and each clause\n * and the search are on it beside that (`OUTREACH_ENROLLMENT_LIST_QUERY`).\n */\nexport function useOutreachEnrollmentList(\n orgId: string | null,\n sequenceId: string | null,\n request: Omit<ListQueryRequest, 'base'>,\n): OutreachListPage<OutreachEnrollment> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection:\n orgId && sequenceId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments)\n : null,\n declaration: OUTREACH_ENROLLMENT_LIST_QUERY,\n request: { ...request, base: sequenceId ? outreachEnrollmentListBase(sequenceId) : [] },\n deps: [firestore, orgId, sequenceId],\n read: readEnrollment,\n what: 'the enrollments',\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","useListQuery","collection","doc","getCountFromServer","onSnapshot","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","OUTREACH_LINK_ROLLUP_PATH","readStoredOutreachEnrollment","readStoredOutreachSequence","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_ENROLLMENT_LIST_QUERY","outreachEnrollmentListBase","OUTREACH_SEQUENCE_LIST_QUERY","denied","error","code","failed","data","what","status","console","useOutreachListQuery","options","listed","declaration","request","deps","idField","read","rows","map","row","String","$id","filter","refused","serverDenied","hasMore","page","setPage","pageSize","setPageSize","plan","readSequence","id","useOutreachSequenceList","orgId","firestore","sequences","useOutreachSequence","sequenceId","result","setResult","undefined","snapshot","exists","useOutreachSequenceLinks","readDomain","domain","useOutreachDoNotContactDomainList","doNotContactDomains","readEnrollment","useOutreachEnrollmentList","enrollments","base","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","count","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 { SendLinkRollup } from '@aglyn/shared-ui-email-campaigns/model/send-report'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport type {\n ListQueryDeclaration,\n ListQueryPlan,\n ListQueryRequest,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n collection,\n doc,\n type DocumentData,\n documentId,\n getCountFromServer,\n limit,\n onSnapshot,\n orderBy,\n type Query,\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'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_ENROLLMENT_LIST_QUERY,\n outreachEnrollmentListBase,\n} from '../model/enrollment-list-query'\nimport { OUTREACH_SEQUENCE_LIST_QUERY } from '../model/sequence-list-query'\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/**\n * One page of a list whose every filter and search word is on its Firestore\n * query (AGL-3321): the rows of the page on screen, whether another page\n * exists, the pager, and the plan — what the query holds and what it could\n * not take, which the list says above itself.\n */\nexport interface OutreachListPage<T> {\n status: OutreachLoadStatus\n rows: T[]\n hasMore: boolean\n page: number\n setPage(page: number): void\n pageSize: number\n setPageSize(pageSize: number): void\n plan: ListQueryPlan\n}\n\n/**\n * A list read through `useListQuery`, in this module's four states and with\n * each document read through its stored-record reader. A document the reader\n * cannot make sense of is left out, as every Outreach read leaves it out.\n */\nfunction useOutreachListQuery<T>(options: {\n collection: Query<DocumentData> | null\n declaration: ListQueryDeclaration\n request: ListQueryRequest\n deps: readonly unknown[]\n read(id: string, data: DocumentData): T | null\n what: string\n}): OutreachListPage<T> {\n const listed = useListQuery<DocumentData & { $id: string }>({\n collection: options.collection,\n declaration: options.declaration,\n request: options.request,\n deps: options.deps,\n idField: '$id',\n })\n const { read, what } = options\n const rows = useMemo(\n () =>\n listed.rows\n .map((row) => read(String(row.$id), row))\n .filter((row): row is NonNullable<typeof row> => row !== null) as T[],\n [listed.rows, read],\n )\n const refused = listed.serverDenied || denied(listed.error)\n useEffect(() => {\n if (listed.status === 'error' && !refused) {\n console.error(`[outreach] ${what} could not be read`, listed.error)\n }\n }, [listed.status, listed.error, refused, what])\n return {\n status:\n listed.status === 'loading'\n ? 'loading'\n : listed.status === 'error'\n ? refused\n ? 'refused'\n : 'error'\n : 'ready',\n rows,\n hasMore: listed.hasMore,\n page: listed.page,\n setPage: listed.setPage,\n pageSize: listed.pageSize,\n setPageSize: listed.setPageSize,\n plan: listed.plan,\n }\n}\n\nconst readSequence = (id: string, data: DocumentData) =>\n readStoredOutreachSequence(id, data)\n\n/**\n * The organization's sequences, newest first, one page at a time — each\n * clause and the search on the query (`OUTREACH_SEQUENCE_LIST_QUERY`).\n */\nexport function useOutreachSequenceList(\n orgId: string | null,\n request: ListQueryRequest,\n): OutreachListPage<OutreachSequence> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection: orgId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences)\n : null,\n declaration: OUTREACH_SEQUENCE_LIST_QUERY,\n request,\n deps: [firestore, orgId],\n read: readSequence,\n what: 'sequences',\n })\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<SendLinkRollup | 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 SendLinkRollup) : null,\n }),\n (error) => setResult(failed(null, error, 'the link report')),\n )\n }, [firestore, orgId, sequenceId])\n return result\n}\n\nconst readDomain = (id: string, data: DocumentData): OutreachDoNotContactDomainEntry => ({\n ...(data as OutreachDoNotContactDomainEntry),\n domain: id,\n})\n\n/**\n * The domains on the organization's do-not-contact list (AGL-3244),\n * alphabetically — the document id is the domain — one page at a time, live:\n * an add or a remove through the route shows up without a reload, and so\n * does a domain the sending runtime files after a gateway block. Each clause\n * and the search are on the query (`OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`).\n */\nexport function useOutreachDoNotContactDomainList(\n orgId: string | null,\n request: ListQueryRequest,\n): OutreachListPage<OutreachDoNotContactDomainEntry> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection: orgId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.doNotContactDomains)\n : null,\n declaration: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY,\n request,\n deps: [firestore, orgId],\n read: readDomain,\n what: 'the do-not-contact domains',\n })\n}\n\nconst readEnrollment = (id: string, data: DocumentData) =>\n readStoredOutreachEnrollment(id, data)\n\n/**\n * A sequence's enrollments, the most recently enrolled first, one page at a\n * time — the sequence is the query's base (`sequenceId ==`), and each clause\n * and the search are on it beside that (`OUTREACH_ENROLLMENT_LIST_QUERY`).\n */\nexport function useOutreachEnrollmentList(\n orgId: string | null,\n sequenceId: string | null,\n request: Omit<ListQueryRequest, 'base'>,\n): OutreachListPage<OutreachEnrollment> {\n const firestore = useFirestore()\n return useOutreachListQuery({\n collection:\n orgId && sequenceId\n ? collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments)\n : null,\n declaration: OUTREACH_ENROLLMENT_LIST_QUERY,\n request: { ...request, base: sequenceId ? outreachEnrollmentListBase(sequenceId) : [] },\n deps: [firestore, orgId, sequenceId],\n read: readEnrollment,\n what: 'the enrollments',\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","useListQuery","collection","doc","getCountFromServer","onSnapshot","query","where","useEffect","useMemo","useState","OUTREACH_COLLECTIONS","OUTREACH_LINK_ROLLUP_PATH","readStoredOutreachEnrollment","readStoredOutreachSequence","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_ENROLLMENT_LIST_QUERY","outreachEnrollmentListBase","OUTREACH_SEQUENCE_LIST_QUERY","denied","error","code","failed","data","what","status","console","useOutreachListQuery","options","listed","declaration","request","deps","idField","read","rows","map","row","String","$id","filter","refused","serverDenied","hasMore","page","setPage","pageSize","setPageSize","plan","readSequence","id","useOutreachSequenceList","orgId","firestore","sequences","useOutreachSequence","sequenceId","result","setResult","undefined","snapshot","exists","useOutreachSequenceLinks","readDomain","domain","useOutreachDoNotContactDomainList","doNotContactDomains","readEnrollment","useOutreachEnrollmentList","enrollments","base","COUNTED","useOutreachSequenceCounts","sequenceIds","counts","setCounts","key","join","current","Promise","all","split","figures","counted","count","Object","fromEntries","name","index","then","entries","catch"],"mappings":"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"}
|