@aglyn/plugins-outreach 1.0.0-beta.231 → 1.0.0-beta.233
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/curated-step-editor.js +1 -0
- package/src/lib/components/curated-step-editor.js.map +1 -1
- package/src/lib/components/do-not-contact-domains.js +23 -8
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enroll-dialog.js +6 -0
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollment-detail.js +3 -0
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/enrollments-table.js +94 -48
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/link-domains-card.js +2 -0
- package/src/lib/components/link-domains-card.js.map +1 -1
- package/src/lib/components/mailboxes-section.js +1 -0
- package/src/lib/components/mailboxes-section.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +1 -0
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +47 -15
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/model/do-not-contact-domain-list-query.js +60 -2
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -1
- package/src/lib/model/enrollment-list-query.js +55 -2
- package/src/lib/model/enrollment-list-query.js.map +1 -1
- package/src/lib/model/sequence-list-query.js +36 -2
- package/src/lib/model/sequence-list-query.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.233",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.233",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.233",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.233",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.233",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.233",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.233",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.233",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.233",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.233",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.233",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.233",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.233",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -101,6 +101,7 @@ export function OutreachCuratedStepEditor(props) {
|
|
|
101
101
|
},
|
|
102
102
|
"aria-label": `${label} — ${draft.decision === 'use' ? 'curated' : 'template kept'}`,
|
|
103
103
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
104
|
+
useFlexGap: true,
|
|
104
105
|
direction: "row",
|
|
105
106
|
spacing: 1,
|
|
106
107
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/curated-step-editor.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\n'use client'\n\nimport {\n Alert,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport {\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n type OutreachValidationIssue,\n} from '../engine/sequence-validation'\nimport type { OutreachCurateDraft, OutreachStepOverrideRequest } from '../model/outreach-api'\nimport {\n OUTREACH_CURATION_CONFIRMATION_LABEL,\n type OutreachSequenceStep,\n type OutreachStepOverrideSource,\n} from '../model/outreach.types'\n\n/**\n * ONE PERSON'S COPY OF ONE STEP, AS THE MEMBER READS AND DECIDES IT\n * (AGL-3324).\n *\n * The enroll dialog shows one of these under each person for every email\n * step the AI drafted; the Enrollments tab shows one for the next step. The\n * member edits the subject and the body, reads what the validator says,\n * ticks the confirmation — a fourth attestation beside the three a cold\n * contact asks for — and either USES the copy or KEEPS the template. Until\n * they decide, nothing about the draft leaves the browser: the enroll and\n * the save carry only what was used.\n *\n * Validated as they type, with the same rules the route applies again: a\n * draft that breaks one cannot be used as it stands.\n */\n\n/** What the member holds for one drafted step. */\nexport interface OutreachCuratedDraftState {\n stepIndex: number\n /** Whether the email starts a thread, and so has a subject of its own. */\n startsThread: boolean\n subject: string\n body: string\n /** The words as they were drafted, so an edit can be told from a confirmation. */\n original: { subject: string; body: string }\n source: OutreachStepOverrideSource\n /** The member ticked the confirmation. */\n confirmed: boolean\n /** Used, kept as the template, or still open. */\n decision: 'pending' | 'use' | 'keep'\n /** The AI's prompt and model, for the override's audit record. */\n prompt?: string\n model?: string\n}\n\n/** A drafted step as the route answered it, ready for the member. */\nexport function outreachDraftState(\n draft: Pick<OutreachCurateDraft, 'stepIndex' | 'subject' | 'body'>,\n steps: readonly OutreachSequenceStep[],\n source: OutreachStepOverrideSource,\n audit?: { prompt: string; model: string },\n): OutreachCuratedDraftState {\n const startsThread = !isInThreadEmailStep(steps, draft.stepIndex)\n const subject = startsThread ? (draft.subject ?? '') : ''\n return {\n stepIndex: draft.stepIndex,\n startsThread,\n subject,\n body: draft.body,\n original: { subject, body: draft.body },\n source,\n confirmed: false,\n decision: 'pending',\n ...(audit ? { prompt: audit.prompt, model: audit.model } : {}),\n }\n}\n\n/** The member's own starting point: the step's words, for them to rewrite by hand. */\nexport function outreachMemberDraftStates(\n steps: readonly OutreachSequenceStep[],\n stepIndexes: readonly number[],\n): OutreachCuratedDraftState[] {\n return stepIndexes.flatMap((stepIndex) => {\n const step = steps[stepIndex]\n if (step?.kind !== 'email') return []\n return [\n outreachDraftState(\n // A step that sends a CRM template has no words here to start from.\n { stepIndex, subject: step.subject, body: step.templateId ? '' : step.body },\n steps,\n 'member',\n ),\n ]\n })\n}\n\n/** The request a USED draft becomes; `null` for one that was kept or is still open. */\nexport function outreachDraftToOverride(\n draft: OutreachCuratedDraftState,\n): OutreachStepOverrideRequest | null {\n if (draft.decision !== 'use') return null\n const edited =\n draft.source === 'ai' &&\n (draft.body !== draft.original.body || (draft.startsThread && draft.subject !== draft.original.subject))\n return {\n stepIndex: draft.stepIndex,\n ...(draft.startsThread ? { subject: draft.subject } : {}),\n body: draft.body,\n source: draft.source,\n ...(edited ? { edited: true } : {}),\n ...(draft.source === 'ai' && draft.prompt ? { prompt: draft.prompt } : {}),\n ...(draft.source === 'ai' && draft.model ? { model: draft.model } : {}),\n }\n}\n\n/** The validator's verdict on the draft as it stands. */\nexport function outreachDraftIssues(draft: OutreachCuratedDraftState): OutreachValidationIssue[] {\n return validateOutreachCuratedStep(\n { subject: draft.startsThread ? draft.subject : undefined, body: draft.body, startsThread: draft.startsThread },\n `stepOverrides.${draft.stepIndex}`,\n )\n}\n\n/** Whether the draft may be used as it stands: confirmed, and nothing the validator refuses. */\nexport function outreachDraftUsable(draft: OutreachCuratedDraftState): boolean {\n return draft.confirmed && !outreachDraftIssues(draft).some((issue) => issue.severity === 'error')\n}\n\nexport interface OutreachCuratedStepEditorProps {\n draft: OutreachCuratedDraftState\n /** \"Email 1 · step 1\", as the sequence's preview names it. */\n label: string\n onChange(next: OutreachCuratedDraftState): void\n disabled?: boolean\n}\n\nexport function OutreachCuratedStepEditor(props: OutreachCuratedStepEditorProps) {\n const { draft, label, onChange } = props\n const disabled = props.disabled === true\n const issues = useMemo(() => outreachDraftIssues(draft), [draft])\n const errors = issues.filter((issue) => issue.severity === 'error')\n const warnings = issues.filter((issue) => issue.severity === 'warning')\n const change = (next: Partial<OutreachCuratedDraftState>) => onChange({ ...draft, ...next })\n const who = draft.source === 'ai' ? 'AI draft' : 'Your words'\n\n if (draft.decision !== 'pending') {\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — ${draft.decision === 'use' ? 'curated' : 'template kept'}`}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Typography variant=\"body2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip\n size=\"small\"\n color={draft.decision === 'use' ? 'success' : 'default'}\n label={draft.decision === 'use' ? `Curated · ${who}` : 'Template kept'}\n />\n <Button size=\"small\" onClick={() => change({ decision: 'pending' })} disabled={disabled}>\n Change\n </Button>\n </Stack>\n </Paper>\n )\n }\n\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — draft`}>\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip size=\"small\" variant=\"outlined\" label={who} />\n </Stack>\n {draft.startsThread ? (\n <TextField\n label=\"Subject\"\n value={draft.subject}\n onChange={(event) => change({ subject: event.target.value, confirmed: false })}\n disabled={disabled}\n size=\"small\"\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.subject'))}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n Sent as a reply in the thread, under the thread’s subject.\n </Typography>\n )}\n <TextField\n label=\"Email\"\n value={draft.body}\n onChange={(event) => change({ body: event.target.value, confirmed: false })}\n disabled={disabled}\n multiline\n minRows={6}\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.body'))}\n helperText=\"Plain text. The footer is added when it’s sent.\"\n />\n {errors.length ? (\n <Alert severity=\"error\">\n <Stack component=\"ul\" spacing={0.5} sx={{ m: 0, pl: 2 }}>\n {errors.map((issue) => (\n <li key={`${issue.path}:${issue.code}`}>{issue.message}</li>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {warnings.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {warnings.map((issue) => issue.message).join(' ')}\n </Typography>\n ) : null}\n <FormControlLabel\n control={\n <Checkbox\n checked={draft.confirmed}\n onChange={(event) => change({ confirmed: event.target.checked })}\n disabled={disabled || errors.length > 0}\n />\n }\n label={OUTREACH_CURATION_CONFIRMATION_LABEL}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={disabled || !outreachDraftUsable(draft)}\n onClick={() => change({ decision: 'use' })}\n >\n Use this\n </Button>\n <Button size=\"small\" disabled={disabled} onClick={() => change({ decision: 'keep' })}>\n Keep the template\n </Button>\n </Stack>\n </Stack>\n </Paper>\n )\n}\nOutreachCuratedStepEditor.displayName = 'OutreachCuratedStepEditor'\n\nexport default OutreachCuratedStepEditor\n"],"names":["Alert","Button","Checkbox","Chip","FormControlLabel","Paper","Stack","TextField","Typography","useMemo","isInThreadEmailStep","validateOutreachCuratedStep","OUTREACH_CURATION_CONFIRMATION_LABEL","outreachDraftState","draft","steps","source","audit","startsThread","stepIndex","subject","body","original","confirmed","decision","prompt","model","outreachMemberDraftStates","stepIndexes","flatMap","step","kind","templateId","outreachDraftToOverride","edited","outreachDraftIssues","undefined","outreachDraftUsable","some","issue","severity","OutreachCuratedStepEditor","props","label","onChange","disabled","issues","errors","filter","warnings","change","next","who","variant","sx","p","aria-label","direction","spacing","alignItems","flexWrap","rowGap","flexGrow","size","color","onClick","value","event","target","fullWidth","error","path","endsWith","multiline","minRows","helperText","length","component","m","pl","map","li","message","code","join","control","checked","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/curated-step-editor.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\n'use client'\n\nimport {\n Alert,\n Button,\n Checkbox,\n Chip,\n FormControlLabel,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport {\n isInThreadEmailStep,\n validateOutreachCuratedStep,\n type OutreachValidationIssue,\n} from '../engine/sequence-validation'\nimport type { OutreachCurateDraft, OutreachStepOverrideRequest } from '../model/outreach-api'\nimport {\n OUTREACH_CURATION_CONFIRMATION_LABEL,\n type OutreachSequenceStep,\n type OutreachStepOverrideSource,\n} from '../model/outreach.types'\n\n/**\n * ONE PERSON'S COPY OF ONE STEP, AS THE MEMBER READS AND DECIDES IT\n * (AGL-3324).\n *\n * The enroll dialog shows one of these under each person for every email\n * step the AI drafted; the Enrollments tab shows one for the next step. The\n * member edits the subject and the body, reads what the validator says,\n * ticks the confirmation — a fourth attestation beside the three a cold\n * contact asks for — and either USES the copy or KEEPS the template. Until\n * they decide, nothing about the draft leaves the browser: the enroll and\n * the save carry only what was used.\n *\n * Validated as they type, with the same rules the route applies again: a\n * draft that breaks one cannot be used as it stands.\n */\n\n/** What the member holds for one drafted step. */\nexport interface OutreachCuratedDraftState {\n stepIndex: number\n /** Whether the email starts a thread, and so has a subject of its own. */\n startsThread: boolean\n subject: string\n body: string\n /** The words as they were drafted, so an edit can be told from a confirmation. */\n original: { subject: string; body: string }\n source: OutreachStepOverrideSource\n /** The member ticked the confirmation. */\n confirmed: boolean\n /** Used, kept as the template, or still open. */\n decision: 'pending' | 'use' | 'keep'\n /** The AI's prompt and model, for the override's audit record. */\n prompt?: string\n model?: string\n}\n\n/** A drafted step as the route answered it, ready for the member. */\nexport function outreachDraftState(\n draft: Pick<OutreachCurateDraft, 'stepIndex' | 'subject' | 'body'>,\n steps: readonly OutreachSequenceStep[],\n source: OutreachStepOverrideSource,\n audit?: { prompt: string; model: string },\n): OutreachCuratedDraftState {\n const startsThread = !isInThreadEmailStep(steps, draft.stepIndex)\n const subject = startsThread ? (draft.subject ?? '') : ''\n return {\n stepIndex: draft.stepIndex,\n startsThread,\n subject,\n body: draft.body,\n original: { subject, body: draft.body },\n source,\n confirmed: false,\n decision: 'pending',\n ...(audit ? { prompt: audit.prompt, model: audit.model } : {}),\n }\n}\n\n/** The member's own starting point: the step's words, for them to rewrite by hand. */\nexport function outreachMemberDraftStates(\n steps: readonly OutreachSequenceStep[],\n stepIndexes: readonly number[],\n): OutreachCuratedDraftState[] {\n return stepIndexes.flatMap((stepIndex) => {\n const step = steps[stepIndex]\n if (step?.kind !== 'email') return []\n return [\n outreachDraftState(\n // A step that sends a CRM template has no words here to start from.\n { stepIndex, subject: step.subject, body: step.templateId ? '' : step.body },\n steps,\n 'member',\n ),\n ]\n })\n}\n\n/** The request a USED draft becomes; `null` for one that was kept or is still open. */\nexport function outreachDraftToOverride(\n draft: OutreachCuratedDraftState,\n): OutreachStepOverrideRequest | null {\n if (draft.decision !== 'use') return null\n const edited =\n draft.source === 'ai' &&\n (draft.body !== draft.original.body || (draft.startsThread && draft.subject !== draft.original.subject))\n return {\n stepIndex: draft.stepIndex,\n ...(draft.startsThread ? { subject: draft.subject } : {}),\n body: draft.body,\n source: draft.source,\n ...(edited ? { edited: true } : {}),\n ...(draft.source === 'ai' && draft.prompt ? { prompt: draft.prompt } : {}),\n ...(draft.source === 'ai' && draft.model ? { model: draft.model } : {}),\n }\n}\n\n/** The validator's verdict on the draft as it stands. */\nexport function outreachDraftIssues(draft: OutreachCuratedDraftState): OutreachValidationIssue[] {\n return validateOutreachCuratedStep(\n { subject: draft.startsThread ? draft.subject : undefined, body: draft.body, startsThread: draft.startsThread },\n `stepOverrides.${draft.stepIndex}`,\n )\n}\n\n/** Whether the draft may be used as it stands: confirmed, and nothing the validator refuses. */\nexport function outreachDraftUsable(draft: OutreachCuratedDraftState): boolean {\n return draft.confirmed && !outreachDraftIssues(draft).some((issue) => issue.severity === 'error')\n}\n\nexport interface OutreachCuratedStepEditorProps {\n draft: OutreachCuratedDraftState\n /** \"Email 1 · step 1\", as the sequence's preview names it. */\n label: string\n onChange(next: OutreachCuratedDraftState): void\n disabled?: boolean\n}\n\nexport function OutreachCuratedStepEditor(props: OutreachCuratedStepEditorProps) {\n const { draft, label, onChange } = props\n const disabled = props.disabled === true\n const issues = useMemo(() => outreachDraftIssues(draft), [draft])\n const errors = issues.filter((issue) => issue.severity === 'error')\n const warnings = issues.filter((issue) => issue.severity === 'warning')\n const change = (next: Partial<OutreachCuratedDraftState>) => onChange({ ...draft, ...next })\n const who = draft.source === 'ai' ? 'AI draft' : 'Your words'\n\n if (draft.decision !== 'pending') {\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — ${draft.decision === 'use' ? 'curated' : 'template kept'}`}>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Typography variant=\"body2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip\n size=\"small\"\n color={draft.decision === 'use' ? 'success' : 'default'}\n label={draft.decision === 'use' ? `Curated · ${who}` : 'Template kept'}\n />\n <Button size=\"small\" onClick={() => change({ decision: 'pending' })} disabled={disabled}>\n Change\n </Button>\n </Stack>\n </Paper>\n )\n }\n\n return (\n <Paper variant=\"outlined\" sx={{ p: 1.5 }} aria-label={`${label} — draft`}>\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n {label}\n </Typography>\n <Chip size=\"small\" variant=\"outlined\" label={who} />\n </Stack>\n {draft.startsThread ? (\n <TextField\n label=\"Subject\"\n value={draft.subject}\n onChange={(event) => change({ subject: event.target.value, confirmed: false })}\n disabled={disabled}\n size=\"small\"\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.subject'))}\n />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n Sent as a reply in the thread, under the thread’s subject.\n </Typography>\n )}\n <TextField\n label=\"Email\"\n value={draft.body}\n onChange={(event) => change({ body: event.target.value, confirmed: false })}\n disabled={disabled}\n multiline\n minRows={6}\n fullWidth\n error={errors.some((issue) => issue.path.endsWith('.body'))}\n helperText=\"Plain text. The footer is added when it’s sent.\"\n />\n {errors.length ? (\n <Alert severity=\"error\">\n <Stack component=\"ul\" spacing={0.5} sx={{ m: 0, pl: 2 }}>\n {errors.map((issue) => (\n <li key={`${issue.path}:${issue.code}`}>{issue.message}</li>\n ))}\n </Stack>\n </Alert>\n ) : null}\n {warnings.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {warnings.map((issue) => issue.message).join(' ')}\n </Typography>\n ) : null}\n <FormControlLabel\n control={\n <Checkbox\n checked={draft.confirmed}\n onChange={(event) => change({ confirmed: event.target.checked })}\n disabled={disabled || errors.length > 0}\n />\n }\n label={OUTREACH_CURATION_CONFIRMATION_LABEL}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={disabled || !outreachDraftUsable(draft)}\n onClick={() => change({ decision: 'use' })}\n >\n Use this\n </Button>\n <Button size=\"small\" disabled={disabled} onClick={() => change({ decision: 'keep' })}>\n Keep the template\n </Button>\n </Stack>\n </Stack>\n </Paper>\n )\n}\nOutreachCuratedStepEditor.displayName = 'OutreachCuratedStepEditor'\n\nexport default OutreachCuratedStepEditor\n"],"names":["Alert","Button","Checkbox","Chip","FormControlLabel","Paper","Stack","TextField","Typography","useMemo","isInThreadEmailStep","validateOutreachCuratedStep","OUTREACH_CURATION_CONFIRMATION_LABEL","outreachDraftState","draft","steps","source","audit","startsThread","stepIndex","subject","body","original","confirmed","decision","prompt","model","outreachMemberDraftStates","stepIndexes","flatMap","step","kind","templateId","outreachDraftToOverride","edited","outreachDraftIssues","undefined","outreachDraftUsable","some","issue","severity","OutreachCuratedStepEditor","props","label","onChange","disabled","issues","errors","filter","warnings","change","next","who","variant","sx","p","aria-label","useFlexGap","direction","spacing","alignItems","flexWrap","rowGap","flexGrow","size","color","onClick","value","event","target","fullWidth","error","path","endsWith","multiline","minRows","helperText","length","component","m","pl","map","li","message","code","join","control","checked","displayName"],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/compon
|
|
|
26
26
|
import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
27
27
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
28
28
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
29
|
+
import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
|
|
29
30
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
31
|
import { Button, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
|
|
31
32
|
import { useState } from "react";
|
|
@@ -162,9 +163,12 @@ function addedOn(entry) {
|
|
|
162
163
|
* One page of the domains, alphabetical, with every clause and the search
|
|
163
164
|
* word on the query (AGL-3321) — never matched over the domains that
|
|
164
165
|
* happen to be loaded.
|
|
165
|
-
*/
|
|
166
|
+
*/ // Every header orders the query (AGL-3680): `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts`.
|
|
167
|
+
const [askedSort, setAskedSort] = useState(null);
|
|
168
|
+
const listed = useOutreachDoNotContactDomainList(orgId, {
|
|
166
169
|
clauses: gridFilter.clauses,
|
|
167
|
-
search: gridFilter.searchWords
|
|
170
|
+
search: gridFilter.searchWords,
|
|
171
|
+
sort: askedSort
|
|
168
172
|
});
|
|
169
173
|
const rows = listed.rows.map((entry)=>{
|
|
170
174
|
var _entry_addedAtMs, _entry_detail;
|
|
@@ -177,6 +181,14 @@ function addedOn(entry) {
|
|
|
177
181
|
detail: (_entry_detail = entry.detail) != null ? _entry_detail : ''
|
|
178
182
|
};
|
|
179
183
|
});
|
|
184
|
+
const columnSort = useListColumnSort({
|
|
185
|
+
sorts: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts,
|
|
186
|
+
defaultSort: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts[0],
|
|
187
|
+
sort: askedSort,
|
|
188
|
+
onSortChange: setAskedSort,
|
|
189
|
+
orderBy: listed.plan.orderBy,
|
|
190
|
+
rows
|
|
191
|
+
});
|
|
180
192
|
/** Nothing listed at all — not a filter that matched nothing. */ const none = listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0;
|
|
181
193
|
/*
|
|
182
194
|
* Import adds domains and addresses from a file; Export downloads the
|
|
@@ -319,24 +331,27 @@ function addedOn(entry) {
|
|
|
319
331
|
headers: DOMAIN_FILTER_HEADERS,
|
|
320
332
|
options: DOMAIN_FILTER_OPTIONS
|
|
321
333
|
}),
|
|
322
|
-
notices:
|
|
334
|
+
notices: [
|
|
335
|
+
...listed.plan.notices,
|
|
336
|
+
...columnSort.notices
|
|
337
|
+
]
|
|
323
338
|
}),
|
|
324
339
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
325
340
|
"aria-label": "Do not contact domains",
|
|
326
341
|
columns: domainColumns(busy !== null, (domain)=>void change('remove', domain)),
|
|
327
|
-
rows: rows,
|
|
342
|
+
rows: columnSort.rows,
|
|
328
343
|
loading: listed.status === 'loading',
|
|
329
344
|
/*
|
|
330
345
|
* The panel and the search are the grid's; every clause and
|
|
331
346
|
* the search word are on the list's query (AGL-3321). The grid
|
|
332
|
-
* neither filters nor sorts the page it is handed:
|
|
333
|
-
*
|
|
334
|
-
*
|
|
347
|
+
* neither filters nor sorts the page it is handed: every header
|
|
348
|
+
* orders the query (AGL-3680) — alphabetical by default, newest
|
|
349
|
+
* added while a date applies.
|
|
335
350
|
*/ filterMode: "server",
|
|
336
351
|
filterModel: gridFilter.filterModel,
|
|
337
352
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
338
353
|
quickFilter: true,
|
|
339
|
-
|
|
354
|
+
columnSort: columnSort,
|
|
340
355
|
noRowsLabel: "No domains match these filters",
|
|
341
356
|
/*
|
|
342
357
|
* The grid's own export writes the page on screen; with the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { pluginDocsHelp, useTransferLauncher } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY } from '../constants/transfer-resources'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomainList } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport { OUTREACH_DO_NOT_CONTACT_REASON_LABELS }\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n import: 'Import',\n export: 'Export',\n} as const\n\n/** What the export dialog calls the list's search and filters. */\nexport const DO_NOT_CONTACT_EXPORT_FILTER_LABEL = 'The domains matching this list’s search and filters'\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317), each field on the\n * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):\n * the whole domain, why it is listed, and when it was added. The search box\n * reads the domain, each of its labels and the words of its detail.\n */\nconst DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n addedAtMs: 'Added',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n\n/** The grid toolbar without its page-only CSV and print export. */\nconst GRID_EXPORT_OFF = {\n toolbar: { csvOptions: { disableToolbarButton: true }, printOptions: { disableToolbarButton: true } },\n}\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the domains, alphabetical, with every clause and the search\n * word on the query (AGL-3321) — never matched over the domains that\n * happen to be loaded.\n */\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n })\n const rows = listed.rows.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n }))\n /** Nothing listed at all — not a filter that matched nothing. */\n const none =\n listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0\n\n /*\n * Import adds domains and addresses from a file; Export downloads the\n * domains, narrowed to the list's search and filters when it has any (the\n * export reads them as the same Firestore query). Outside the console\n * shell there is no launcher, and no buttons.\n */\n const transfer = useTransferLauncher()\n const transferTarget = { resource: OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, scope: 'org' as const }\n // Import for those who may write the records, Export for those who may read them.\n const canImport = Boolean(transfer?.can('import', transferTarget))\n const canExport = Boolean(transfer?.can('export', transferTarget))\n const transferActions =\n transfer && (canImport || canExport) ? (\n <Stack direction=\"row\" spacing={1}>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={transferTarget} />\n {canImport && (\n <Button size=\"small\" onClick={() => transfer.openImport(transferTarget)}>\n {DO_NOT_CONTACT_DOMAIN_LABELS.import}\n </Button>\n )}\n {canExport && (\n <Button\n size=\"small\"\n onClick={() =>\n transfer.openExport({\n ...transferTarget,\n ...(filtering\n ? {\n filter: {\n label: DO_NOT_CONTACT_EXPORT_FILTER_LABEL,\n value: {\n clauses: gridFilter.clauses.map(({ field, op, value }) => ({ field, op, value })),\n search: gridFilter.searchWords,\n },\n },\n }\n : {}),\n })\n }\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.export}\n </Button>\n )}\n </Stack>\n ) : undefined\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n HeaderProps={{ action: transferActions }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried. Import adds ' +\n 'domains and email addresses from a file; Export downloads the domains.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' && !filtering ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : none ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(listed.plan.refused, {\n fields: DOMAIN_FILTER_FIELDS,\n headers: DOMAIN_FILTER_HEADERS,\n options: DOMAIN_FILTER_OPTIONS,\n })}\n notices={listed.plan.notices}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={rows}\n loading={listed.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and\n * the search word are on the list's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: the query's\n * order — alphabetical, or newest added while a date applies —\n * is the list's.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n disableColumnSorting\n noRowsLabel=\"No domains match these filters\"\n /*\n * The grid's own export writes the page on screen; with the\n * header's Export, which writes every matching domain, it\n * would be a second button by the same name doing less.\n */\n slotProps={transfer ? GRID_EXPORT_OFF : undefined}\n hideFooter\n />\n <ListPagination\n page={listed.page}\n pageSize={listed.pageSize}\n rowCount={rows.length}\n hasMore={listed.hasMore}\n onPageChange={listed.setPage}\n onPageSizeChange={listed.setPageSize}\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["TransferResumeImport","pluginDocsHelp","useTransferLauncher","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","listFilterGridColumns","useListGridFilter","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useState","OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomainList","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","import","export","DO_NOT_CONTACT_EXPORT_FILTER_LABEL","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","fields","DOMAIN_FILTER_HEADERS","reason","addedAtMs","DOMAIN_FILTER_OPTIONS","map","value","label","GRID_EXPORT_OFF","toolbar","csvOptions","disableToolbarButton","printOptions","addedOn","entry","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","listed","search","rows","$id","detail","none","status","page","transfer","transferTarget","resource","scope","canImport","Boolean","can","canExport","transferActions","direction","spacing","target","openImport","openExport","filter","op","undefined","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","HeaderProps","contentGutterX","contentGutterY","xs","sm","alignItems","onChange","event","onKeyDown","key","preventDefault","helperText","flexGrow","what","headers","setClauses","options","refused","plan","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","disableColumnSorting","noRowsLabel","slotProps","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/do-not-contact-domains.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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport { pluginDocsHelp, useTransferLauncher } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport type { ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Chip, IconButton, Stack, TextField, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport { OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY } from '../constants/transfer-resources'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport { OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY } from '../model/do-not-contact-domain-list-query'\nimport {\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomainList } from './use-outreach-data'\n\nexport interface OutreachDoNotContactDomainsCardProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** Why a domain is on the list, as the card says it. */\nexport { OUTREACH_DO_NOT_CONTACT_REASON_LABELS }\n\n/** Accessible names of the card's controls, spelled once for the specs. */\nexport const DO_NOT_CONTACT_DOMAIN_LABELS = {\n field: 'Domain',\n add: 'Add domain',\n remove: (domain: string) => `Remove ${domain}`,\n import: 'Import',\n export: 'Export',\n} as const\n\n/** What the export dialog calls the list's search and filters. */\nexport const DO_NOT_CONTACT_EXPORT_FILTER_LABEL = 'The domains matching this list’s search and filters'\n\n/** What the field says under a value that is not a domain. */\nexport const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.'\n\n/*\n * What the domains grid's Filters panel offers (AGL-3317), each field on the\n * list's Firestore query (AGL-3321, `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY`):\n * the whole domain, why it is listed, and when it was added. The search box\n * reads the domain, each of its labels and the words of its detail.\n */\nconst DOMAIN_FILTER_FIELDS = OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.fields\nconst DOMAIN_FILTER_HEADERS: Readonly<Record<string, string>> = {\n domain: 'Domain',\n reason: 'Why',\n addedAtMs: 'Added',\n}\nconst DOMAIN_FILTER_OPTIONS = {\n reason: OUTREACH_DO_NOT_CONTACT_REASONS.map((reason) => ({\n value: reason,\n label: OUTREACH_DO_NOT_CONTACT_REASON_LABELS[reason],\n })),\n}\n\n/** The grid toolbar without its page-only CSV and print export. */\nconst GRID_EXPORT_OFF = {\n toolbar: { csvOptions: { disableToolbarButton: true }, printOptions: { disableToolbarButton: true } },\n}\n\nfunction addedOn(entry: OutreachDoNotContactDomainEntry): string {\n if (!entry.addedAtMs) return ''\n return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(entry.addedAtMs)\n}\n\n/** The grid's columns; the remove button names its domain for a screen reader. */\nfunction domainColumns(\n busy: boolean,\n remove: (domain: string) => void,\n): NonNullable<ListTableProps['columns']> {\n return listFilterGridColumns(\n [\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }) => (\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {row.domain}\n </Typography>\n ),\n },\n {\n field: 'reason',\n headerName: 'Why',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={row.reason === 'gateway_block' ? 'warning' : 'default'}\n label={\n OUTREACH_DO_NOT_CONTACT_REASON_LABELS[row.reason as OutreachDoNotContactReason] ??\n row.reason\n }\n />\n ),\n },\n {\n field: 'addedAtMs',\n headerName: 'Added',\n width: 140,\n renderCell: ({ row }) => row.added,\n },\n { field: 'detail', headerName: 'Detail', flex: 1, minWidth: 160 },\n listActionsColumn(\n (row) => (\n <IconButton\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(row.domain)}\n disabled={busy}\n onClick={() => remove(row.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n ),\n { width: 72 },\n ),\n ],\n DOMAIN_FILTER_FIELDS,\n DOMAIN_FILTER_OPTIONS,\n DOMAIN_FILTER_HEADERS,\n )\n}\n\n/**\n * Sequences → Compliance → Do not contact domains (AGL-3244): the domains no\n * sequence emails anyone at, whoever enrolls them.\n *\n * A member adds one by hand — a company that asked, or one whose gateway is\n * known to block cold mail — and the sending runtime adds one when a hard\n * bounce reads as the domain's gateway refusing the sender. Either way it\n * is listed here with why, and a member takes it off by name.\n */\nexport function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [draft, setDraft] = useState('')\n const [busy, setBusy] = useState<string | null>(null)\n const normalized = normalizeOutreachDomain(draft)\n const invalid = draft.trim() !== '' && normalized === null\n const gridFilter = useListGridFilter({ selectFields: ['reason'] })\n const filtering =\n gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word) => word.trim())\n /*\n * One page of the domains, alphabetical, with every clause and the search\n * word on the query (AGL-3321) — never matched over the domains that\n * happen to be loaded.\n */\n // Every header orders the query (AGL-3680): `OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts`.\n const [askedSort, setAskedSort] = useState<ListQuerySort | null>(null)\n const listed = useOutreachDoNotContactDomainList(orgId, {\n clauses: gridFilter.clauses,\n search: gridFilter.searchWords,\n sort: askedSort,\n })\n const rows = listed.rows.map((entry) => ({\n $id: entry.domain,\n domain: entry.domain,\n reason: entry.reason,\n added: addedOn(entry),\n addedAtMs: entry.addedAtMs ?? 0,\n detail: entry.detail ?? '',\n }))\n const columnSort = useListColumnSort({\n sorts: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts,\n defaultSort: OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY.sorts[0],\n sort: askedSort,\n onSortChange: setAskedSort,\n orderBy: listed.plan.orderBy,\n rows,\n })\n /** Nothing listed at all — not a filter that matched nothing. */\n const none =\n listed.status === 'ready' && !listed.rows.length && !filtering && listed.page === 0\n\n /*\n * Import adds domains and addresses from a file; Export downloads the\n * domains, narrowed to the list's search and filters when it has any (the\n * export reads them as the same Firestore query). Outside the console\n * shell there is no launcher, and no buttons.\n */\n const transfer = useTransferLauncher()\n const transferTarget = { resource: OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY, scope: 'org' as const }\n // Import for those who may write the records, Export for those who may read them.\n const canImport = Boolean(transfer?.can('import', transferTarget))\n const canExport = Boolean(transfer?.can('export', transferTarget))\n const transferActions =\n transfer && (canImport || canExport) ? (\n <Stack direction=\"row\" spacing={1}>\n {/* An import left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport target={transferTarget} />\n {canImport && (\n <Button size=\"small\" onClick={() => transfer.openImport(transferTarget)}>\n {DO_NOT_CONTACT_DOMAIN_LABELS.import}\n </Button>\n )}\n {canExport && (\n <Button\n size=\"small\"\n onClick={() =>\n transfer.openExport({\n ...transferTarget,\n ...(filtering\n ? {\n filter: {\n label: DO_NOT_CONTACT_EXPORT_FILTER_LABEL,\n value: {\n clauses: gridFilter.clauses.map(({ field, op, value }) => ({ field, op, value })),\n search: gridFilter.searchWords,\n },\n },\n }\n : {}),\n })\n }\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.export}\n </Button>\n )}\n </Stack>\n ) : undefined\n\n const change = async (action: 'add' | 'remove', domain: string) => {\n setBusy(domain)\n try {\n const answer = await api.changeDoNotContactDomain(action, domain)\n if (action === 'add') setDraft('')\n enqueueSnackbar(\n answer.changed\n ? action === 'add'\n ? `${answer.domain} is on the do-not-contact list.`\n : `${answer.domain} is off the do-not-contact list.`\n : action === 'add'\n ? `${answer.domain} was already on the list.`\n : `${answer.domain} was not on the list.`,\n { variant: answer.changed ? 'success' : 'info' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(null)\n }\n }\n\n return (\n <CardDisplay\n header=\"Do not contact domains\"\n help={pluginDocsHelp('sequences', { anchor: '#do-not-contact-domains' })}\n HeaderProps={{ action: transferActions }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' +\n 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' +\n 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' +\n 'added here automatically, so the next person at that company is not tried. Import adds ' +\n 'domains and email addresses from a file; Export downloads the domains.'}\n </Typography>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'flex-start' } }}>\n <TextField\n label={DO_NOT_CONTACT_DOMAIN_LABELS.field}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && normalized && !busy) {\n event.preventDefault()\n void change('add', normalized)\n }\n }}\n error={invalid}\n helperText={invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT}\n size=\"small\"\n sx={{ flexGrow: 1 }}\n />\n <Button\n variant=\"outlined\"\n disabled={!normalized || busy !== null}\n onClick={() => normalized && void change('add', normalized)}\n >\n {DO_NOT_CONTACT_DOMAIN_LABELS.add}\n </Button>\n </Stack>\n {listed.status === 'loading' && !filtering ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : none ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <Stack spacing={1}>\n <ListFilterChips\n fields={DOMAIN_FILTER_FIELDS}\n headers={DOMAIN_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={DOMAIN_FILTER_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(listed.plan.refused, {\n fields: DOMAIN_FILTER_FIELDS,\n headers: DOMAIN_FILTER_HEADERS,\n options: DOMAIN_FILTER_OPTIONS,\n })}\n notices={[...listed.plan.notices, ...columnSort.notices]}\n />\n <ListTable\n aria-label=\"Do not contact domains\"\n columns={domainColumns(busy !== null, (domain) => void change('remove', domain))}\n rows={columnSort.rows}\n loading={listed.status === 'loading'}\n /*\n * The panel and the search are the grid's; every clause and\n * the search word are on the list's query (AGL-3321). The grid\n * neither filters nor sorts the page it is handed: every header\n * orders the query (AGL-3680) — alphabetical by default, newest\n * added while a date applies.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n columnSort={columnSort}\n noRowsLabel=\"No domains match these filters\"\n /*\n * The grid's own export writes the page on screen; with the\n * header's Export, which writes every matching domain, it\n * would be a second button by the same name doing less.\n */\n slotProps={transfer ? GRID_EXPORT_OFF : undefined}\n hideFooter\n />\n <ListPagination\n page={listed.page}\n pageSize={listed.pageSize}\n rowCount={rows.length}\n hasMore={listed.hasMore}\n onPageChange={listed.setPage}\n onPageSizeChange={listed.setPageSize}\n />\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["TransferResumeImport","pluginDocsHelp","useTransferLauncher","mdiTrashCanOutline","CardDisplay","MdiIcon","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","listFilterGridColumns","useListGridFilter","useListColumnSort","useSnackbar","Button","Chip","IconButton","Stack","TextField","Typography","useState","OUTREACH_DO_NOT_CONTACT_TRANSFER_KEY","normalizeOutreachDomain","OUTREACH_DO_NOT_CONTACT_DOMAIN_LIST_QUERY","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","OUTREACH_DO_NOT_CONTACT_REASONS","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomainList","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","import","export","DO_NOT_CONTACT_EXPORT_FILTER_LABEL","DO_NOT_CONTACT_DOMAIN_HINT","DOMAIN_FILTER_FIELDS","fields","DOMAIN_FILTER_HEADERS","reason","addedAtMs","DOMAIN_FILTER_OPTIONS","map","value","label","GRID_EXPORT_OFF","toolbar","csvOptions","disableToolbarButton","printOptions","addedOn","entry","Intl","DateTimeFormat","dateStyle","format","domainColumns","busy","headerName","flex","minWidth","renderCell","row","variant","component","sx","fontWeight","size","color","width","added","aria-label","disabled","onClick","path","fontSize","OutreachDoNotContactDomainsCard","props","orgId","api","enqueueSnackbar","draft","setDraft","setBusy","normalized","invalid","trim","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","askedSort","setAskedSort","listed","search","sort","rows","$id","detail","columnSort","sorts","defaultSort","onSortChange","orderBy","plan","none","status","page","transfer","transferTarget","resource","scope","canImport","Boolean","can","canExport","transferActions","direction","spacing","target","openImport","openExport","filter","op","undefined","change","action","answer","changeDoNotContactDomain","changed","error","message","allowDuplicate","header","help","anchor","HeaderProps","contentGutterX","contentGutterY","xs","sm","alignItems","onChange","event","onKeyDown","key","preventDefault","helperText","flexGrow","what","headers","setClauses","options","refused","notices","columns","loading","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","slotProps","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","displayName"],"mappings":"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"}
|
|
@@ -414,6 +414,9 @@ const STATUS_COLORS = {
|
|
|
414
414
|
value: tab,
|
|
415
415
|
onChange: (_event, next)=>setTab(next),
|
|
416
416
|
"aria-label": "Where the people come from",
|
|
417
|
+
variant: "scrollable",
|
|
418
|
+
scrollButtons: "auto",
|
|
419
|
+
allowScrollButtonsMobile: true,
|
|
417
420
|
children: [
|
|
418
421
|
/*#__PURE__*/ _jsx(Tab, {
|
|
419
422
|
value: "view",
|
|
@@ -507,6 +510,7 @@ const STATUS_COLORS = {
|
|
|
507
510
|
})
|
|
508
511
|
}) : null,
|
|
509
512
|
pickedLeads.size ? /*#__PURE__*/ _jsx(Stack, {
|
|
513
|
+
useFlexGap: true,
|
|
510
514
|
direction: "row",
|
|
511
515
|
spacing: 1,
|
|
512
516
|
sx: {
|
|
@@ -584,6 +588,7 @@ const STATUS_COLORS = {
|
|
|
584
588
|
})
|
|
585
589
|
}) : null,
|
|
586
590
|
picked.size ? /*#__PURE__*/ _jsx(Stack, {
|
|
591
|
+
useFlexGap: true,
|
|
587
592
|
direction: "row",
|
|
588
593
|
spacing: 1,
|
|
589
594
|
sx: {
|
|
@@ -903,6 +908,7 @@ function PersonRow(props) {
|
|
|
903
908
|
})
|
|
904
909
|
}) : null,
|
|
905
910
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
911
|
+
useFlexGap: true,
|
|
906
912
|
direction: "row",
|
|
907
913
|
spacing: 1,
|
|
908
914
|
sx: {
|