@aglyn/plugins-outreach 1.0.0-beta.199 → 1.0.0-beta.201
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.js +60 -42
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +25 -2
- package/src/lib/components/enrollments-table.js +303 -283
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +73 -11
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +13 -1
- package/src/lib/components/sequences-section.js +108 -83
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +30 -25
- package/src/lib/components/use-outreach-data.js +93 -104
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/enrollment/enrollment-search.d.ts +32 -0
- package/src/lib/enrollment/enrollment-search.js +40 -0
- package/src/lib/enrollment/enrollment-search.js.map +1 -0
- package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
- package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +28 -0
- package/src/lib/model/enrollment-list-query.js +151 -0
- package/src/lib/model/enrollment-list-query.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +90 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-list-query.d.ts +22 -0
- package/src/lib/model/sequence-list-query.js +99 -0
- package/src/lib/model/sequence-list-query.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +34 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +6 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.js +6 -1
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
|
@@ -19,5 +19,22 @@ export interface OutreachSequenceReportCardProps {
|
|
|
19
19
|
links: OutreachSequenceLinksLoad;
|
|
20
20
|
/** The mailbox's zone, which times are read in; `null` for the reader's own. */
|
|
21
21
|
timeZone: string | null;
|
|
22
|
+
/** Lists the people who clicked, on the Enrollments tab (AGL-3332). */
|
|
23
|
+
onShowClickers?: () => void;
|
|
24
|
+
/** Lists the people who followed one destination, on the Enrollments tab (AGL-3332). */
|
|
25
|
+
onShowLink?: (url: string) => void;
|
|
22
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* One figure, with what it counts under it — and where it leads, when it
|
|
29
|
+
* leads somewhere. The sequence's Results and one person's page (AGL-3332)
|
|
30
|
+
* draw their numbers alike.
|
|
31
|
+
*/
|
|
32
|
+
export declare function OutreachFigure(props: {
|
|
33
|
+
label: string;
|
|
34
|
+
value: string;
|
|
35
|
+
hint?: string;
|
|
36
|
+
/** Opens the list behind the figure; named for a screen reader by `action`. */
|
|
37
|
+
onClick?: () => void;
|
|
38
|
+
action?: string;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
23
40
|
export declare function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps): import("react").JSX.Element;
|
|
@@ -15,24 +15,25 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
-
import { Alert, Card, CardContent, Divider, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { Alert, ButtonBase, Card, CardContent, Divider, Link, Stack, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from "@mui/material";
|
|
20
20
|
// Not a raw MUI `Table`: a destination is a long string, and a raw table is
|
|
21
21
|
// cut off by the card around it with nothing able to scroll to what is past
|
|
22
22
|
// the edge (AGL-3045).
|
|
23
23
|
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
24
24
|
import { outreachSequenceLinkReport, outreachSequenceReport } from "../model/sequence-report.js";
|
|
25
25
|
import { formatOutreachTime, OutreachLoadProblem } from "./outreach-ui.js";
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
/**
|
|
27
|
+
* One figure, with what it counts under it — and where it leads, when it
|
|
28
|
+
* leads somewhere. The sequence's Results and one person's page (AGL-3332)
|
|
29
|
+
* draw their numbers alike.
|
|
30
|
+
*/ export function OutreachFigure(props) {
|
|
31
|
+
const body = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
32
32
|
children: [
|
|
33
33
|
/*#__PURE__*/ _jsx(Typography, {
|
|
34
34
|
variant: "h5",
|
|
35
35
|
component: "p",
|
|
36
|
+
color: props.onClick ? 'primary' : undefined,
|
|
36
37
|
children: props.value
|
|
37
38
|
}),
|
|
38
39
|
/*#__PURE__*/ _jsx(Typography, {
|
|
@@ -46,6 +47,36 @@ import { formatOutreachTime, OutreachLoadProblem } from "./outreach-ui.js";
|
|
|
46
47
|
}) : null
|
|
47
48
|
]
|
|
48
49
|
});
|
|
50
|
+
if (!props.onClick) {
|
|
51
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
52
|
+
spacing: 0,
|
|
53
|
+
sx: {
|
|
54
|
+
minWidth: 110
|
|
55
|
+
},
|
|
56
|
+
children: body
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
60
|
+
title: props.action,
|
|
61
|
+
children: /*#__PURE__*/ _jsx(ButtonBase, {
|
|
62
|
+
onClick: props.onClick,
|
|
63
|
+
"aria-label": `${props.value} ${props.label}. ${props.action}`,
|
|
64
|
+
sx: {
|
|
65
|
+
minWidth: 110,
|
|
66
|
+
display: 'flex',
|
|
67
|
+
flexDirection: 'column',
|
|
68
|
+
alignItems: 'flex-start',
|
|
69
|
+
textAlign: 'left',
|
|
70
|
+
borderRadius: 1,
|
|
71
|
+
px: 1,
|
|
72
|
+
mx: -1,
|
|
73
|
+
'&:hover, &.Mui-focusVisible': {
|
|
74
|
+
bgcolor: 'action.hover'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
children: body
|
|
78
|
+
})
|
|
79
|
+
});
|
|
49
80
|
}
|
|
50
81
|
/** A whole number as the card prints one. */ const count = (value)=>value.toLocaleString();
|
|
51
82
|
/**
|
|
@@ -86,27 +117,30 @@ export function OutreachSequenceReportCard(props) {
|
|
|
86
117
|
rowGap: 2
|
|
87
118
|
},
|
|
88
119
|
children: [
|
|
89
|
-
/*#__PURE__*/ _jsx(
|
|
120
|
+
/*#__PURE__*/ _jsx(OutreachFigure, {
|
|
90
121
|
label: "Emails sent",
|
|
91
122
|
value: count(report.sent)
|
|
92
123
|
}),
|
|
93
|
-
/*#__PURE__*/ _jsx(
|
|
124
|
+
/*#__PURE__*/ _jsx(OutreachFigure, _extends({
|
|
94
125
|
label: "People emailed",
|
|
95
126
|
value: report.people === null ? '—' : count(report.people)
|
|
96
127
|
}, report.people === null ? {
|
|
97
128
|
hint: 'Not counted for this sequence'
|
|
98
129
|
} : {})),
|
|
99
|
-
/*#__PURE__*/ _jsx(
|
|
130
|
+
/*#__PURE__*/ _jsx(OutreachFigure, _extends({
|
|
100
131
|
label: "Clicked",
|
|
101
132
|
value: count(report.uniqueClicks),
|
|
102
133
|
hint: report.clicks === report.uniqueClicks ? undefined : `${count(report.clicks)} clicks in all`
|
|
103
|
-
}
|
|
104
|
-
|
|
134
|
+
}, props.onShowClickers && report.uniqueClicks > 0 ? {
|
|
135
|
+
onClick: props.onShowClickers,
|
|
136
|
+
action: 'Show who clicked'
|
|
137
|
+
} : {})),
|
|
138
|
+
/*#__PURE__*/ _jsx(OutreachFigure, {
|
|
105
139
|
label: "Click rate",
|
|
106
140
|
value: clickRate.value,
|
|
107
141
|
hint: clickRate.hint
|
|
108
142
|
}),
|
|
109
|
-
report.machineClicks > 0 ? /*#__PURE__*/ _jsx(
|
|
143
|
+
report.machineClicks > 0 ? /*#__PURE__*/ _jsx(OutreachFigure, {
|
|
110
144
|
label: "Scanner clicks",
|
|
111
145
|
value: count(report.machineClicks),
|
|
112
146
|
hint: "Not in the rate"
|
|
@@ -156,12 +190,25 @@ export function OutreachSequenceReportCard(props) {
|
|
|
156
190
|
}),
|
|
157
191
|
/*#__PURE__*/ _jsx(TableBody, {
|
|
158
192
|
children: links.rows.map((row)=>/*#__PURE__*/ _jsxs(TableRow, {
|
|
193
|
+
hover: Boolean(props.onShowLink),
|
|
159
194
|
children: [
|
|
160
195
|
/*#__PURE__*/ _jsx(TableCell, {
|
|
161
196
|
sx: {
|
|
162
197
|
wordBreak: 'break-all'
|
|
163
198
|
},
|
|
164
|
-
children:
|
|
199
|
+
children: props.onShowLink ? /*#__PURE__*/ _jsx(Link, {
|
|
200
|
+
component: "button",
|
|
201
|
+
type: "button",
|
|
202
|
+
variant: "body2",
|
|
203
|
+
underline: "hover",
|
|
204
|
+
onClick: ()=>props.onShowLink == null ? void 0 : props.onShowLink.call(props, row.url),
|
|
205
|
+
"aria-label": `Show who followed ${row.url}`,
|
|
206
|
+
sx: {
|
|
207
|
+
textAlign: 'left',
|
|
208
|
+
wordBreak: 'break-all'
|
|
209
|
+
},
|
|
210
|
+
children: row.url
|
|
211
|
+
}) : row.url
|
|
165
212
|
}),
|
|
166
213
|
/*#__PURE__*/ _jsx(TableCell, {
|
|
167
214
|
align: "right",
|
|
@@ -176,6 +223,7 @@ export function OutreachSequenceReportCard(props) {
|
|
|
176
223
|
variant: "caption",
|
|
177
224
|
color: "text.secondary",
|
|
178
225
|
children: [
|
|
226
|
+
props.onShowLink ? 'Select a destination to see who followed it. ' : '',
|
|
179
227
|
"Two links to the same page are counted as one destination, whatever tracking parameters follow the address.",
|
|
180
228
|
links.truncated ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.` : ''
|
|
181
229
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CampaignRate } from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n Alert,\n Card,\n CardContent,\n Divider,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\n// Not a raw MUI `Table`: a destination is a long string, and a raw table is\n// cut off by the card around it with nothing able to scroll to what is past\n// the edge (AGL-3045).\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport {\n outreachSequenceLinkReport,\n outreachSequenceReport,\n} from '../model/sequence-report'\nimport { formatOutreachTime, OutreachLoadProblem } from './outreach-ui'\nimport type { OutreachSequenceLinksLoad } from './use-outreach-data'\n\n/**\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Four figures and one rate, and the sentences saying what each of them is\n * not. The decisions are all in `model/sequence-report.ts`; this renders\n * them and adds no arithmetic of its own — in particular it never divides,\n * so there is no second place a percentage can be computed differently from\n * the way a campaign's report computes it.\n *\n * The caveats are rendered as prominently as the numbers rather than as\n * small print. \"Opens aren't measured\" is the thing a reader coming from\n * the campaign report most needs told, and a blank where an open rate would\n * be says the opposite of what is true.\n */\n\nexport interface OutreachSequenceReportCardProps {\n sequence: Pick<OutreachSequence, 'stats' | 'settings'>\n links: OutreachSequenceLinksLoad\n /** The mailbox's zone, which times are read in; `null` for the reader's own. */\n timeZone: string | null\n}\n\n/** One figure, with what it counts under it. */\nfunction Figure(props: { label: string; value: string; hint?: string }) {\n return (\n <Stack spacing={0} sx={{ minWidth: 110 }}>\n <Typography variant=\"h5\" component=\"p\">\n {props.value}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n {props.hint ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.hint}\n </Typography>\n ) : null}\n </Stack>\n )\n}\n\n/** A whole number as the card prints one. */\nconst count = (value: number): string => value.toLocaleString()\n\n/**\n * A rate and the denominator it was taken over, or the em dash that stands\n * for a rate the model refused to take. The denominator is printed beside\n * the percentage, never dropped: \"12% of people emailed\" and \"12% of people\n * who opened\" are different claims and a bare \"12%\" is neither.\n */\nfunction rateText(rate: CampaignRate | null): { value: string; hint?: string } {\n if (!rate) return { value: '—' }\n return {\n value: `${(rate.value * 100).toFixed(1)}%`,\n hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`,\n }\n}\n\nexport function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps) {\n const report = outreachSequenceReport(\n props.sequence.stats,\n props.sequence.settings.trackClicks,\n )\n const clickRate = rateText(report.rates.click)\n const links = outreachSequenceLinkReport(\n props.links.status === 'ready' ? (props.links.data ?? undefined) : undefined,\n )\n\n return (\n <Card variant=\"outlined\">\n <CardContent>\n <Stack spacing={2}>\n <Typography variant=\"h6\" component=\"h2\">\n Results\n </Typography>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap', rowGap: 2 }}>\n <Figure label=\"Emails sent\" value={count(report.sent)} />\n <Figure\n label=\"People emailed\"\n value={report.people === null ? '—' : count(report.people)}\n {...(report.people === null ? { hint: 'Not counted for this sequence' } : {})}\n />\n <Figure\n label=\"Clicked\"\n value={count(report.uniqueClicks)}\n hint={\n report.clicks === report.uniqueClicks\n ? undefined\n : `${count(report.clicks)} clicks in all`\n }\n />\n <Figure label=\"Click rate\" value={clickRate.value} hint={clickRate.hint} />\n {report.machineClicks > 0 ? (\n <Figure\n label=\"Scanner clicks\"\n value={count(report.machineClicks)}\n hint=\"Not in the rate\"\n />\n ) : null}\n </Stack>\n {report.lastClickAtMs === null ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n Last click {formatOutreachTime(report.lastClickAtMs, props.timeZone)}\n </Typography>\n )}\n\n {report.caveats.map((entry) => (\n <Alert key={entry.id} severity=\"info\" variant=\"outlined\">\n {entry.message}\n </Alert>\n ))}\n\n {props.links.status === 'error' || props.links.status === 'refused' ? (\n <OutreachLoadProblem status={props.links.status} what=\"the link report\" />\n ) : links.rows.length ? (\n <Stack spacing={1}>\n <Divider />\n <Typography variant=\"subtitle2\" component=\"h3\">\n Links followed\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Destination</TableCell>\n <TableCell align=\"right\">Clicks</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {links.rows.map((row) => (\n <TableRow key={row.url}>\n <TableCell sx={{ wordBreak: 'break-all' }}>{row.url}</TableCell>\n <TableCell align=\"right\">{count(row.clicks)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n Two links to the same page are counted as one destination, whatever\n tracking parameters follow the address.\n {links.truncated\n ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.`\n : ''}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"],"names":["Alert","Card","CardContent","Divider","Stack","TableBody","TableCell","TableHead","TableRow","Typography","ScrollTable","outreachSequenceLinkReport","outreachSequenceReport","formatOutreachTime","OutreachLoadProblem","Figure","props","spacing","sx","minWidth","variant","component","value","label","hint","color","count","toLocaleString","rateText","rate","toFixed","numerator","denominator","denominatorLabel","OutreachSequenceReportCard","report","sequence","stats","settings","trackClicks","clickRate","rates","click","links","status","data","undefined","direction","flexWrap","rowGap","sent","people","uniqueClicks","clicks","machineClicks","lastClickAtMs","timeZone","caveats","map","entry","severity","message","id","what","rows","length","size","align","row","wordBreak","url","truncated","overflowClicks"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-report-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CampaignRate } from '@aglyn/shared-ui-email-campaigns/model'\nimport {\n Alert,\n ButtonBase,\n Card,\n CardContent,\n Divider,\n Link,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { ReactNode } from 'react'\n// Not a raw MUI `Table`: a destination is a long string, and a raw table is\n// cut off by the card around it with nothing able to scroll to what is past\n// the edge (AGL-3045).\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport {\n outreachSequenceLinkReport,\n outreachSequenceReport,\n} from '../model/sequence-report'\nimport { formatOutreachTime, OutreachLoadProblem } from './outreach-ui'\nimport type { OutreachSequenceLinksLoad } from './use-outreach-data'\n\n/**\n * WHAT A SEQUENCE MEASURED (AGL-3239).\n *\n * Four figures and one rate, and the sentences saying what each of them is\n * not. The decisions are all in `model/sequence-report.ts`; this renders\n * them and adds no arithmetic of its own — in particular it never divides,\n * so there is no second place a percentage can be computed differently from\n * the way a campaign's report computes it.\n *\n * The caveats are rendered as prominently as the numbers rather than as\n * small print. \"Opens aren't measured\" is the thing a reader coming from\n * the campaign report most needs told, and a blank where an open rate would\n * be says the opposite of what is true.\n */\n\nexport interface OutreachSequenceReportCardProps {\n sequence: Pick<OutreachSequence, 'stats' | 'settings'>\n links: OutreachSequenceLinksLoad\n /** The mailbox's zone, which times are read in; `null` for the reader's own. */\n timeZone: string | null\n /** Lists the people who clicked, on the Enrollments tab (AGL-3332). */\n onShowClickers?: () => void\n /** Lists the people who followed one destination, on the Enrollments tab (AGL-3332). */\n onShowLink?: (url: string) => void\n}\n\n/**\n * One figure, with what it counts under it — and where it leads, when it\n * leads somewhere. The sequence's Results and one person's page (AGL-3332)\n * draw their numbers alike.\n */\nexport function OutreachFigure(props: {\n label: string\n value: string\n hint?: string\n /** Opens the list behind the figure; named for a screen reader by `action`. */\n onClick?: () => void\n action?: string\n}) {\n const body: ReactNode = (\n <>\n <Typography variant=\"h5\" component=\"p\" color={props.onClick ? 'primary' : undefined}>\n {props.value}\n </Typography>\n <Typography variant=\"body2\">{props.label}</Typography>\n {props.hint ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.hint}\n </Typography>\n ) : null}\n </>\n )\n if (!props.onClick) {\n return (\n <Stack spacing={0} sx={{ minWidth: 110 }}>\n {body}\n </Stack>\n )\n }\n return (\n <Tooltip title={props.action}>\n <ButtonBase\n onClick={props.onClick}\n aria-label={`${props.value} ${props.label}. ${props.action}`}\n sx={{\n minWidth: 110,\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n textAlign: 'left',\n borderRadius: 1,\n px: 1,\n mx: -1,\n '&:hover, &.Mui-focusVisible': { bgcolor: 'action.hover' },\n }}\n >\n {body}\n </ButtonBase>\n </Tooltip>\n )\n}\n\n/** A whole number as the card prints one. */\nconst count = (value: number): string => value.toLocaleString()\n\n/**\n * A rate and the denominator it was taken over, or the em dash that stands\n * for a rate the model refused to take. The denominator is printed beside\n * the percentage, never dropped: \"12% of people emailed\" and \"12% of people\n * who opened\" are different claims and a bare \"12%\" is neither.\n */\nfunction rateText(rate: CampaignRate | null): { value: string; hint?: string } {\n if (!rate) return { value: '—' }\n return {\n value: `${(rate.value * 100).toFixed(1)}%`,\n hint: `${count(rate.numerator)} of ${count(rate.denominator)} ${rate.denominatorLabel}`,\n }\n}\n\nexport function OutreachSequenceReportCard(props: OutreachSequenceReportCardProps) {\n const report = outreachSequenceReport(\n props.sequence.stats,\n props.sequence.settings.trackClicks,\n )\n const clickRate = rateText(report.rates.click)\n const links = outreachSequenceLinkReport(\n props.links.status === 'ready' ? (props.links.data ?? undefined) : undefined,\n )\n\n return (\n <Card variant=\"outlined\">\n <CardContent>\n <Stack spacing={2}>\n <Typography variant=\"h6\" component=\"h2\">\n Results\n </Typography>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap', rowGap: 2 }}>\n <OutreachFigure label=\"Emails sent\" value={count(report.sent)} />\n <OutreachFigure\n label=\"People emailed\"\n value={report.people === null ? '—' : count(report.people)}\n {...(report.people === null ? { hint: 'Not counted for this sequence' } : {})}\n />\n <OutreachFigure\n label=\"Clicked\"\n value={count(report.uniqueClicks)}\n hint={\n report.clicks === report.uniqueClicks\n ? undefined\n : `${count(report.clicks)} clicks in all`\n }\n {...(props.onShowClickers && report.uniqueClicks > 0\n ? { onClick: props.onShowClickers, action: 'Show who clicked' }\n : {})}\n />\n <OutreachFigure label=\"Click rate\" value={clickRate.value} hint={clickRate.hint} />\n {report.machineClicks > 0 ? (\n <OutreachFigure\n label=\"Scanner clicks\"\n value={count(report.machineClicks)}\n hint=\"Not in the rate\"\n />\n ) : null}\n </Stack>\n {report.lastClickAtMs === null ? null : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n Last click {formatOutreachTime(report.lastClickAtMs, props.timeZone)}\n </Typography>\n )}\n\n {report.caveats.map((entry) => (\n <Alert key={entry.id} severity=\"info\" variant=\"outlined\">\n {entry.message}\n </Alert>\n ))}\n\n {props.links.status === 'error' || props.links.status === 'refused' ? (\n <OutreachLoadProblem status={props.links.status} what=\"the link report\" />\n ) : links.rows.length ? (\n <Stack spacing={1}>\n <Divider />\n <Typography variant=\"subtitle2\" component=\"h3\">\n Links followed\n </Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>Destination</TableCell>\n <TableCell align=\"right\">Clicks</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {links.rows.map((row) => (\n <TableRow key={row.url} hover={Boolean(props.onShowLink)}>\n <TableCell sx={{ wordBreak: 'break-all' }}>\n {props.onShowLink ? (\n <Link\n component=\"button\"\n type=\"button\"\n variant=\"body2\"\n underline=\"hover\"\n onClick={() => props.onShowLink?.(row.url)}\n aria-label={`Show who followed ${row.url}`}\n sx={{ textAlign: 'left', wordBreak: 'break-all' }}\n >\n {row.url}\n </Link>\n ) : (\n row.url\n )}\n </TableCell>\n <TableCell align=\"right\">{count(row.clicks)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.onShowLink ? 'Select a destination to see who followed it. ' : ''}\n Two links to the same page are counted as one destination, whatever\n tracking parameters follow the address.\n {links.truncated\n ? ` ${count(links.overflowClicks)} more clicks went to destinations past the ones listed.`\n : ''}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"],"names":["Alert","ButtonBase","Card","CardContent","Divider","Link","Stack","TableBody","TableCell","TableHead","TableRow","Tooltip","Typography","ScrollTable","outreachSequenceLinkReport","outreachSequenceReport","formatOutreachTime","OutreachLoadProblem","OutreachFigure","props","body","variant","component","color","onClick","undefined","value","label","hint","spacing","sx","minWidth","title","action","aria-label","display","flexDirection","alignItems","textAlign","borderRadius","px","mx","bgcolor","count","toLocaleString","rateText","rate","toFixed","numerator","denominator","denominatorLabel","OutreachSequenceReportCard","report","sequence","stats","settings","trackClicks","clickRate","rates","click","links","status","data","direction","flexWrap","rowGap","sent","people","uniqueClicks","clicks","onShowClickers","machineClicks","lastClickAtMs","timeZone","caveats","map","entry","severity","message","id","what","rows","length","size","align","row","hover","Boolean","onShowLink","wordBreak","type","underline","url","truncated","overflowClicks"],"mappings":"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"}
|
|
@@ -7,7 +7,10 @@ export interface OutreachSequencesSectionProps {
|
|
|
7
7
|
org?: Record<string, unknown> | null;
|
|
8
8
|
/** The section's own path: `/[orgSlug]/outreach/sequences`. */
|
|
9
9
|
sectionPath: string;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The path below the section: `[]`, `['new']`, `[id]`, `[id, 'enrollments']`
|
|
12
|
+
* or `[id, 'enrollments', enrollmentId]` — one person's page (AGL-3332).
|
|
13
|
+
*/
|
|
11
14
|
subpath: readonly string[];
|
|
12
15
|
/** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */
|
|
13
16
|
mailboxesPath: string;
|
|
@@ -28,6 +31,15 @@ export declare namespace OutreachSequencesSection {
|
|
|
28
31
|
/**
|
|
29
32
|
* Every sequence: its mailbox, its status, and how many people it enrolled
|
|
30
33
|
* and where they stand — a table on wider screens, cards on a phone.
|
|
34
|
+
*
|
|
35
|
+
* ## Asked of Firestore, a page at a time
|
|
36
|
+
*
|
|
37
|
+
* The Filters panel and the search box are the grid's; the clauses and the
|
|
38
|
+
* words go onto ONE query over the organization's sequences, newest first,
|
|
39
|
+
* and each page is a page of that query's matches (AGL-3321). A clause the
|
|
40
|
+
* query cannot hold beside the others is not applied, and the notice above
|
|
41
|
+
* the list says which and why — never answered over the sequences that
|
|
42
|
+
* happen to be loaded.
|
|
31
43
|
*/
|
|
32
44
|
export declare function OutreachSequenceList(props: {
|
|
33
45
|
orgId: string;
|
|
@@ -23,17 +23,20 @@ import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.com
|
|
|
23
23
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
24
24
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
25
25
|
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
26
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
27
|
+
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
28
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
28
29
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
30
|
+
import { Button, Card, CardContent, Chip, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
31
|
+
import { useMemo } from "react";
|
|
31
32
|
import { OUTREACH_SEQUENCE_STATUSES } from "../model/outreach.types.js";
|
|
33
|
+
import { OUTREACH_SEQUENCE_LIST_QUERY } from "../model/sequence-list-query.js";
|
|
32
34
|
import { OUTREACH_SEQUENCE_STATUS_LABELS, OutreachLink, OutreachLoading, OutreachLoadProblem, OutreachSequenceStatusChip, useOutreachNavigate } from "./outreach-ui.js";
|
|
35
|
+
import { OutreachEnrollmentDetail } from "./enrollment-detail.js";
|
|
33
36
|
import { OutreachSequenceDetail } from "./sequence-detail.js";
|
|
34
37
|
import { OutreachSequenceEditor } from "./sequence-editor.js";
|
|
35
38
|
import { useOutreachApi } from "./use-outreach-api.js";
|
|
36
|
-
import { useOutreachSequenceCounts,
|
|
39
|
+
import { useOutreachSequenceCounts, useOutreachSequenceList } from "./use-outreach-data.js";
|
|
37
40
|
import { useOutreachMailboxes } from "./use-outreach-mailboxes.js";
|
|
38
41
|
import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
|
|
39
42
|
/** The new-sequence page's segment below the section. */ export const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new';
|
|
@@ -50,7 +53,7 @@ import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
|
|
|
50
53
|
if (!orgId) return /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
51
54
|
label: "Loading sequences…"
|
|
52
55
|
});
|
|
53
|
-
const [first, second] = subpath;
|
|
56
|
+
const [first, second, third] = subpath;
|
|
54
57
|
if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {
|
|
55
58
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
56
59
|
spacing: 2,
|
|
@@ -77,6 +80,16 @@ import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
|
|
|
77
80
|
]
|
|
78
81
|
});
|
|
79
82
|
}
|
|
83
|
+
if (first && second === 'enrollments' && third) {
|
|
84
|
+
return /*#__PURE__*/ _jsx(OutreachEnrollmentDetail, {
|
|
85
|
+
orgId: orgId,
|
|
86
|
+
orgMount: props.orgMount,
|
|
87
|
+
sectionPath: sectionPath,
|
|
88
|
+
sequenceId: first,
|
|
89
|
+
enrollmentId: third,
|
|
90
|
+
mailboxes: mailboxes
|
|
91
|
+
});
|
|
92
|
+
}
|
|
80
93
|
if (first) {
|
|
81
94
|
return /*#__PURE__*/ _jsx(OutreachSequenceDetail, {
|
|
82
95
|
orgId: orgId,
|
|
@@ -121,85 +134,76 @@ const COUNT_COLUMNS = [
|
|
|
121
134
|
]
|
|
122
135
|
];
|
|
123
136
|
/*
|
|
124
|
-
* What the sequences grid's Filters panel offers (AGL-3317)
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
*/ const SEQUENCE_FILTER_FIELDS = [
|
|
128
|
-
inMemoryListField('name', 'text'),
|
|
129
|
-
inMemoryListField('mailbox', 'text'),
|
|
130
|
-
inMemoryListField('status', 'select')
|
|
131
|
-
];
|
|
137
|
+
* What the sequences grid's Filters panel offers (AGL-3317), each field on
|
|
138
|
+
* the list's Firestore query (AGL-3321, `OUTREACH_SEQUENCE_LIST_QUERY`).
|
|
139
|
+
*/ const SEQUENCE_FILTER_FIELDS = OUTREACH_SEQUENCE_LIST_QUERY.fields;
|
|
132
140
|
const SEQUENCE_FILTER_HEADERS = {
|
|
133
141
|
name: 'Name',
|
|
134
142
|
mailbox: 'Mailbox',
|
|
135
|
-
status: 'Status'
|
|
143
|
+
status: 'Status',
|
|
144
|
+
created: 'Created'
|
|
136
145
|
};
|
|
137
|
-
const
|
|
138
|
-
status: OUTREACH_SEQUENCE_STATUSES.map((status)=>({
|
|
139
|
-
value: status,
|
|
140
|
-
label: OUTREACH_SEQUENCE_STATUS_LABELS[status]
|
|
141
|
-
}))
|
|
142
|
-
};
|
|
143
|
-
/** What the quick search reads on a sequence row. */ const SEQUENCE_SEARCH_FIELDS = [
|
|
146
|
+
/** `created` is no column of its own: a filter-only column, never shown. */ const SEQUENCE_HIDDEN_COLUMNS = hiddenFilterVisibility(SEQUENCE_FILTER_FIELDS, [
|
|
144
147
|
'name',
|
|
145
|
-
'mailbox'
|
|
146
|
-
|
|
148
|
+
'mailbox',
|
|
149
|
+
'status'
|
|
150
|
+
]);
|
|
151
|
+
const SEQUENCE_STATUS_OPTIONS = OUTREACH_SEQUENCE_STATUSES.map((status)=>({
|
|
152
|
+
value: status,
|
|
153
|
+
label: OUTREACH_SEQUENCE_STATUS_LABELS[status]
|
|
154
|
+
}));
|
|
155
|
+
/** How a mailbox reads in the list: the address it sends as. */ const mailboxName = (mailbox)=>mailbox.sendAs || mailbox.email;
|
|
147
156
|
/**
|
|
148
157
|
* Every sequence: its mailbox, its status, and how many people it enrolled
|
|
149
158
|
* and where they stand — a table on wider screens, cards on a phone.
|
|
159
|
+
*
|
|
160
|
+
* ## Asked of Firestore, a page at a time
|
|
161
|
+
*
|
|
162
|
+
* The Filters panel and the search box are the grid's; the clauses and the
|
|
163
|
+
* words go onto ONE query over the organization's sequences, newest first,
|
|
164
|
+
* and each page is a page of that query's matches (AGL-3321). A clause the
|
|
165
|
+
* query cannot hold beside the others is not applied, and the notice above
|
|
166
|
+
* the list says which and why — never answered over the sequences that
|
|
167
|
+
* happen to be loaded.
|
|
150
168
|
*/ export function OutreachSequenceList(props) {
|
|
151
169
|
const { orgId, sectionPath } = props;
|
|
152
170
|
const theme = useTheme();
|
|
153
171
|
const narrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
154
172
|
const navigate = useOutreachNavigate();
|
|
155
|
-
const sequences = useOutreachSequences(orgId);
|
|
156
|
-
const [page, setPage] = useState(0);
|
|
157
|
-
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
158
173
|
const gridFilter = useListGridFilter({
|
|
159
174
|
selectFields: [
|
|
160
|
-
'status'
|
|
175
|
+
'status',
|
|
176
|
+
'mailbox'
|
|
161
177
|
]
|
|
162
178
|
});
|
|
179
|
+
const sequences = useOutreachSequenceList(orgId, {
|
|
180
|
+
clauses: gridFilter.clauses,
|
|
181
|
+
search: gridFilter.searchWords
|
|
182
|
+
});
|
|
183
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
184
|
+
const filterOptions = useMemo(()=>({
|
|
185
|
+
status: SEQUENCE_STATUS_OPTIONS,
|
|
186
|
+
mailbox: props.mailboxes.map((mailbox)=>({
|
|
187
|
+
value: mailbox.id,
|
|
188
|
+
label: mailboxName(mailbox)
|
|
189
|
+
}))
|
|
190
|
+
}), [
|
|
191
|
+
props.mailboxes
|
|
192
|
+
]);
|
|
163
193
|
const mailboxLabel = (sequence)=>{
|
|
164
194
|
const mailbox = props.mailboxes.find((entry)=>entry.id === sequence.mailboxId);
|
|
165
|
-
return mailbox ? mailbox
|
|
195
|
+
return mailbox ? mailboxName(mailbox) : sequence.mailboxId ? 'Mailbox removed' : 'No mailbox yet';
|
|
166
196
|
};
|
|
167
|
-
|
|
168
|
-
* Filtered BEFORE the page is cut: the list read every sequence (under
|
|
169
|
-
* `OUTREACH_SEQUENCES_LIMIT`), so a clause or a search word answers over
|
|
170
|
-
* all of them, and the footer counts the matches.
|
|
171
|
-
*/ const searchKey = gridFilter.searchWords.join(' ');
|
|
172
|
-
const matching = useMemo(()=>filterListRows(sequences.data.map((sequence)=>({
|
|
173
|
-
sequence,
|
|
174
|
-
name: sequence.name || 'Untitled',
|
|
175
|
-
mailbox: mailboxLabel(sequence),
|
|
176
|
-
status: sequence.status
|
|
177
|
-
})), SEQUENCE_FILTER_FIELDS, gridFilter.clauses, {
|
|
178
|
-
paths: SEQUENCE_SEARCH_FIELDS,
|
|
179
|
-
words: gridFilter.searchWords
|
|
180
|
-
}).map((row)=>row.sequence), // `searchKey` stands for the words, which are a new array each render.
|
|
181
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
-
[
|
|
183
|
-
sequences.data,
|
|
184
|
-
props.mailboxes,
|
|
185
|
-
gridFilter.clauses,
|
|
186
|
-
searchKey
|
|
187
|
-
]);
|
|
188
|
-
// A narrowed list starts again at its first page.
|
|
189
|
-
useEffect(()=>setPage(0), [
|
|
190
|
-
gridFilter.clauses,
|
|
191
|
-
searchKey
|
|
192
|
-
]);
|
|
193
|
-
const pageRows = matching.slice(page * pageSize, (page + 1) * pageSize);
|
|
197
|
+
const pageRows = sequences.rows;
|
|
194
198
|
// Counted for the page on screen only: five aggregations a sequence.
|
|
195
199
|
const counts = useOutreachSequenceCounts(orgId, pageRows.map((sequence)=>sequence.id));
|
|
196
200
|
const footer = /*#__PURE__*/ _jsx(ListPagination, {
|
|
197
|
-
page: page,
|
|
198
|
-
pageSize: pageSize,
|
|
201
|
+
page: sequences.page,
|
|
202
|
+
pageSize: sequences.pageSize,
|
|
199
203
|
rowCount: pageRows.length,
|
|
200
|
-
|
|
201
|
-
onPageChange: setPage,
|
|
202
|
-
onPageSizeChange: setPageSize
|
|
204
|
+
hasMore: sequences.hasMore,
|
|
205
|
+
onPageChange: sequences.setPage,
|
|
206
|
+
onPageSizeChange: sequences.setPageSize
|
|
203
207
|
});
|
|
204
208
|
const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`;
|
|
205
209
|
const newButton = /*#__PURE__*/ _jsx(Button, {
|
|
@@ -216,30 +220,38 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
216
220
|
children: "New sequence"
|
|
217
221
|
});
|
|
218
222
|
const count = (sequence, key)=>counts[sequence.id] ? String(counts[sequence.id][key]) : '—';
|
|
219
|
-
/*
|
|
220
|
-
* The read stops at `OUTREACH_SEQUENCES_LIMIT`, newest first, and probes one
|
|
221
|
-
* past it, so this shows only when older sequences really exist.
|
|
222
|
-
*/ const truncatedNotice = sequences.truncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
223
|
-
severity: "info",
|
|
224
|
-
children: `Showing the ${sequences.data.length.toLocaleString()} newest sequences. ` + 'The filters and the search narrow these only; older sequences are ' + 'not listed here.'
|
|
225
|
-
}) : null;
|
|
226
223
|
const chips = /*#__PURE__*/ _jsx(ListFilterChips, {
|
|
227
224
|
fields: SEQUENCE_FILTER_FIELDS,
|
|
228
225
|
headers: SEQUENCE_FILTER_HEADERS,
|
|
229
226
|
clauses: gridFilter.clauses,
|
|
230
227
|
onChange: gridFilter.setClauses,
|
|
231
|
-
options:
|
|
228
|
+
options: filterOptions
|
|
232
229
|
});
|
|
230
|
+
const notices = /*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
231
|
+
refused: listQueryRefusals(sequences.plan.refused, {
|
|
232
|
+
fields: SEQUENCE_FILTER_FIELDS,
|
|
233
|
+
headers: SEQUENCE_FILTER_HEADERS,
|
|
234
|
+
options: filterOptions
|
|
235
|
+
}),
|
|
236
|
+
notices: sequences.plan.notices
|
|
237
|
+
});
|
|
238
|
+
/** Nothing stored at all — not a filter that matched nothing. */ const none = sequences.status === 'ready' && !pageRows.length && !filtering && sequences.page === 0;
|
|
233
239
|
let body;
|
|
234
|
-
if (sequences.status === 'loading') body = /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
240
|
+
if (sequences.status === 'loading' && !filtering) body = /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
235
241
|
label: "Loading sequences…"
|
|
236
242
|
});
|
|
237
243
|
else if (sequences.status === 'error' || sequences.status === 'refused') {
|
|
238
|
-
body = /*#__PURE__*/
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
body = /*#__PURE__*/ _jsxs(Stack, {
|
|
245
|
+
spacing: 1,
|
|
246
|
+
children: [
|
|
247
|
+
chips,
|
|
248
|
+
/*#__PURE__*/ _jsx(OutreachLoadProblem, {
|
|
249
|
+
status: sequences.status,
|
|
250
|
+
what: "sequences"
|
|
251
|
+
})
|
|
252
|
+
]
|
|
241
253
|
});
|
|
242
|
-
} else if (
|
|
254
|
+
} else if (none) {
|
|
243
255
|
body = /*#__PURE__*/ _jsx(Card, {
|
|
244
256
|
variant: "outlined",
|
|
245
257
|
children: /*#__PURE__*/ _jsx(EmptyStateComponent, {
|
|
@@ -253,7 +265,8 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
253
265
|
spacing: 1,
|
|
254
266
|
children: [
|
|
255
267
|
chips,
|
|
256
|
-
|
|
268
|
+
notices,
|
|
269
|
+
pageRows.length ? /*#__PURE__*/ _jsx(Stack, {
|
|
257
270
|
spacing: 1,
|
|
258
271
|
component: "ul",
|
|
259
272
|
sx: {
|
|
@@ -308,9 +321,12 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
308
321
|
})
|
|
309
322
|
})
|
|
310
323
|
}, sequence.id))
|
|
324
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
325
|
+
variant: "body2",
|
|
326
|
+
color: "text.secondary",
|
|
327
|
+
children: sequences.status === 'loading' ? 'Loading sequences…' : 'No sequences match these filters'
|
|
311
328
|
}),
|
|
312
|
-
footer
|
|
313
|
-
truncatedNotice
|
|
329
|
+
footer
|
|
314
330
|
]
|
|
315
331
|
});
|
|
316
332
|
} else {
|
|
@@ -342,14 +358,20 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
342
358
|
align: 'right',
|
|
343
359
|
headerAlign: 'right'
|
|
344
360
|
}))
|
|
345
|
-
], SEQUENCE_FILTER_FIELDS,
|
|
361
|
+
], SEQUENCE_FILTER_FIELDS, filterOptions, SEQUENCE_FILTER_HEADERS);
|
|
346
362
|
body = /*#__PURE__*/ _jsxs(Stack, {
|
|
347
363
|
spacing: 1,
|
|
348
364
|
children: [
|
|
349
365
|
chips,
|
|
366
|
+
notices,
|
|
350
367
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
351
368
|
"aria-label": "Sequences",
|
|
352
369
|
columns: columns,
|
|
370
|
+
initialState: {
|
|
371
|
+
columns: {
|
|
372
|
+
columnVisibilityModel: SEQUENCE_HIDDEN_COLUMNS
|
|
373
|
+
}
|
|
374
|
+
},
|
|
353
375
|
rows: pageRows.map((sequence)=>_extends({
|
|
354
376
|
$id: sequence.id,
|
|
355
377
|
name: sequence.name || 'Untitled',
|
|
@@ -360,18 +382,21 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
360
382
|
count(sequence, key)
|
|
361
383
|
])))),
|
|
362
384
|
onOpen: (id)=>navigate(`${sectionPath}/${id}`),
|
|
385
|
+
loading: sequences.status === 'loading',
|
|
363
386
|
/*
|
|
364
|
-
* The panel and the search are the grid's;
|
|
365
|
-
*
|
|
387
|
+
* The panel and the search are the grid's; every clause and the
|
|
388
|
+
* search word are on the list's query (AGL-3321), and the grid
|
|
389
|
+
* neither filters nor sorts the page it is handed: the query's
|
|
390
|
+
* one order, newest first, is the list's.
|
|
366
391
|
*/ filterMode: "server",
|
|
367
392
|
filterModel: gridFilter.filterModel,
|
|
368
393
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
369
394
|
quickFilter: true,
|
|
395
|
+
disableColumnSorting: true,
|
|
370
396
|
noRowsLabel: "No sequences match these filters",
|
|
371
397
|
hideFooter: true
|
|
372
398
|
}),
|
|
373
|
-
footer
|
|
374
|
-
truncatedNotice
|
|
399
|
+
footer
|
|
375
400
|
]
|
|
376
401
|
});
|
|
377
402
|
}
|
|
@@ -381,7 +406,7 @@ const SEQUENCE_FILTER_OPTIONS = {
|
|
|
381
406
|
anchor: '#sequences'
|
|
382
407
|
}),
|
|
383
408
|
HeaderProps: {
|
|
384
|
-
action: sequences.status === '
|
|
409
|
+
action: none || sequences.status === 'loading' ? undefined : newButton
|
|
385
410
|
},
|
|
386
411
|
contentGutterX: true,
|
|
387
412
|
contentGutterY: true,
|