@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.
Files changed (27) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/curated-step-editor.js +1 -0
  3. package/src/lib/components/curated-step-editor.js.map +1 -1
  4. package/src/lib/components/do-not-contact-domains.js +23 -8
  5. package/src/lib/components/do-not-contact-domains.js.map +1 -1
  6. package/src/lib/components/enroll-dialog.js +6 -0
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/enrollment-detail.js +3 -0
  9. package/src/lib/components/enrollment-detail.js.map +1 -1
  10. package/src/lib/components/enrollments-table.js +94 -48
  11. package/src/lib/components/enrollments-table.js.map +1 -1
  12. package/src/lib/components/link-domains-card.js +2 -0
  13. package/src/lib/components/link-domains-card.js.map +1 -1
  14. package/src/lib/components/mailboxes-section.js +1 -0
  15. package/src/lib/components/mailboxes-section.js.map +1 -1
  16. package/src/lib/components/sequence-detail.js +1 -0
  17. package/src/lib/components/sequence-detail.js.map +1 -1
  18. package/src/lib/components/sequence-report-card.js +1 -0
  19. package/src/lib/components/sequence-report-card.js.map +1 -1
  20. package/src/lib/components/sequences-section.js +47 -15
  21. package/src/lib/components/sequences-section.js.map +1 -1
  22. package/src/lib/model/do-not-contact-domain-list-query.js +60 -2
  23. package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -1
  24. package/src/lib/model/enrollment-list-query.js +55 -2
  25. package/src/lib/model/enrollment-list-query.js.map +1 -1
  26. package/src/lib/model/sequence-list-query.js +36 -2
  27. 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.231",
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.231",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.231",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.231",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.231",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
34
- "@aglyn/shared-util-email": "1.0.0-beta.231",
35
- "@aglyn/shared-util-http": "1.0.0-beta.231",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.231",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.231",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.231",
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
- */ const listed = useOutreachDoNotContactDomainList(orgId, {
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: listed.plan.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: the query's
333
- * order — alphabetical, or newest added while a date applies —
334
- * is the list's.
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
- disableColumnSorting: true,
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: {