@aglyn/plugins-outreach 1.0.0-beta.159 → 1.0.0-beta.161
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/README.md +4 -0
- package/package.json +13 -13
- package/src/lib/components/compliance-section.d.ts +3 -2
- package/src/lib/components/compliance-section.js +7 -2
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/do-not-contact-domains.d.ts +29 -0
- package/src/lib/components/do-not-contact-domains.js +208 -0
- package/src/lib/components/do-not-contact-domains.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +12 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +8 -1
- package/src/lib/components/use-outreach-data.js +32 -0
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +5 -0
- package/src/lib/constants/api-routes.js +4 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/do-not-contact-domain.d.ts +39 -0
- package/src/lib/engine/do-not-contact-domain.js +51 -0
- package/src/lib/engine/do-not-contact-domain.js.map +1 -0
- package/src/lib/engine/gates.d.ts +6 -1
- package/src/lib/engine/gates.js +6 -0
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +31 -0
- package/src/lib/engine/gateway-block.js +54 -0
- package/src/lib/engine/gateway-block.js.map +1 -0
- package/src/lib/engine/index.d.ts +3 -0
- package/src/lib/engine/index.js +3 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/mailbox-health.d.ts +5 -0
- package/src/lib/engine/mailbox-health.js +12 -5
- package/src/lib/engine/mailbox-health.js.map +1 -1
- package/src/lib/engine/mailbox-notice.d.ts +46 -0
- package/src/lib/engine/mailbox-notice.js +64 -0
- package/src/lib/engine/mailbox-notice.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.js +7 -3
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +23 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +33 -1
- package/src/lib/model/outreach.types.js +11 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/do-not-contact-routes.d.ts +35 -0
- package/src/lib/routes/do-not-contact-routes.js +92 -0
- package/src/lib/routes/do-not-contact-routes.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +1 -0
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +4 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/runtime/mailbox-health-store.d.ts +1 -1
- package/src/lib/runtime/mailbox-health-store.js +17 -4
- package/src/lib/runtime/mailbox-health-store.js.map +1 -1
- package/src/lib/runtime/mailbox-notices.d.ts +39 -0
- package/src/lib/runtime/mailbox-notices.js +117 -0
- package/src/lib/runtime/mailbox-notices.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -0
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +25 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +21 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +6 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +34 -7
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/server.d.ts +1 -1
- package/src/lib/server.js +1 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +28 -1
- package/src/lib/storage/do-not-contact-store.js +117 -9
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,9 @@ pure functions with no I/O, exported from the package root:
|
|
|
81
81
|
| `thread-message` | the classifier's message shape, and the adapter from a Gmail API message |
|
|
82
82
|
| `delivery-status`, `opt-out-intent`, `thread-classification` | what came back: bounces, automatic replies, replies and opt-outs |
|
|
83
83
|
| `mailbox-health` | when a mailbox pauses itself |
|
|
84
|
+
| `mailbox-notice` | the email a mailbox's owner gets when it pauses itself or needs reconnecting |
|
|
85
|
+
| `gateway-block` | whether a hard bounce is the recipient domain's gateway refusing the sender, rather than one unknown address |
|
|
86
|
+
| `do-not-contact-domain` | how a domain on the do-not-contact list is spelled |
|
|
84
87
|
|
|
85
88
|
`engine/do-not-contact` derives the do-not-contact document id with
|
|
86
89
|
`node:crypto`, so it is not in the barrel: import
|
|
@@ -97,6 +100,7 @@ console alone — each run opens a rep's sealed grant:
|
|
|
97
100
|
| `runtime/sync-job` | `outreach-sync`: replies, out-of-office answers, opt-outs and bounces read with `format=full` and classified by the engine; the mailbox's health evidence and its automatic pause |
|
|
98
101
|
| `runtime/unsubscribe-link`, `runtime/unsubscribe-route` | the signed one-click link (`GET` and RFC 8058 `POST /api/outreach/unsubscribe`), a recipient link that outlives a paused rollout, and the `+unsubscribe` mailto |
|
|
99
102
|
| `runtime/enrollment-events` | status changes as the engine's events, and an opt-out recorded on every list |
|
|
103
|
+
| `runtime/mailbox-notices` | the owner told, once, when a mailbox pauses itself or needs reconnecting |
|
|
100
104
|
| `runtime/timeline` | the contact's timeline, through the core's record-timeline seam |
|
|
101
105
|
| `runtime/person-erasure` | the enrollments a person erasure deletes, and the do-not-contact entry it keeps |
|
|
102
106
|
|
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.161",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
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-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.161",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.161",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.161",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.161",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.161",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.161",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.161",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.161",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.161",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.161",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.161",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.161",
|
|
40
40
|
"@swc/helpers": "0.5.23"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
@@ -5,8 +5,9 @@ export interface OutreachComplianceSectionProps {
|
|
|
5
5
|
/** What CAN-SPAM accepts as the postal address, as the field says it. */
|
|
6
6
|
export declare const OUTREACH_POSTAL_ADDRESS_HELP: string;
|
|
7
7
|
/**
|
|
8
|
-
* Sequences → Compliance (AGL-2980): who every email says sent it,
|
|
9
|
-
* countries a sequence may send to at all
|
|
8
|
+
* Sequences → Compliance (AGL-2980): who every email says sent it, the
|
|
9
|
+
* countries a sequence may send to at all, and — below the settings, with
|
|
10
|
+
* no Save of its own — the domains no sequence emails (AGL-3244).
|
|
10
11
|
*
|
|
11
12
|
* The footer is previewed from what is typed, by the engine's own
|
|
12
13
|
* `composeOutreachFooter`, so the page shows the lines every email will end
|
|
@@ -24,6 +24,7 @@ import { Alert, Autocomplete, Button, Chip, Paper, Stack, TextField, Typography
|
|
|
24
24
|
import { useEffect, useMemo, useState } from "react";
|
|
25
25
|
import { composeOutreachFooter } from "../engine/compose.js";
|
|
26
26
|
import { outreachComplianceSettingsEqual, outreachCountryLabel, outreachCountryOptions, OUTREACH_BRAND_NAME_MAX, OUTREACH_LEGAL_NAME_MAX, OUTREACH_POSTAL_ADDRESS_MAX, validateOutreachComplianceSettings } from "../model/compliance-settings.js";
|
|
27
|
+
import { OutreachDoNotContactDomainsCard } from "./do-not-contact-domains.js";
|
|
27
28
|
import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
28
29
|
import { OutreachRouteError, useOutreachApi } from "./use-outreach-api.js";
|
|
29
30
|
import { useOutreachComplianceSettings } from "./use-outreach-settings.js";
|
|
@@ -37,8 +38,9 @@ const EMPTY = {
|
|
|
37
38
|
]
|
|
38
39
|
};
|
|
39
40
|
/**
|
|
40
|
-
* Sequences → Compliance (AGL-2980): who every email says sent it,
|
|
41
|
-
* countries a sequence may send to at all
|
|
41
|
+
* Sequences → Compliance (AGL-2980): who every email says sent it, the
|
|
42
|
+
* countries a sequence may send to at all, and — below the settings, with
|
|
43
|
+
* no Save of its own — the domains no sequence emails (AGL-3244).
|
|
42
44
|
*
|
|
43
45
|
* The footer is previewed from what is typed, by the engine's own
|
|
44
46
|
* `composeOutreachFooter`, so the page shows the lines every email will end
|
|
@@ -281,6 +283,9 @@ const EMPTY = {
|
|
|
281
283
|
children: saving ? 'Saving…' : 'Save'
|
|
282
284
|
})
|
|
283
285
|
]
|
|
286
|
+
}),
|
|
287
|
+
/*#__PURE__*/ _jsx(OutreachDoNotContactDomainsCard, {
|
|
288
|
+
orgId: orgId
|
|
284
289
|
})
|
|
285
290
|
]
|
|
286
291
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, and the\n * countries a sequence may send to at all.\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState(false)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n useEffect(() => {\n if (stored) setForm({ ...stored })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n const dirty = stored\n ? !outreachComplianceSettingsEqual(stored, normalized)\n : false\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues({})\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const save = async () => {\n setSaving(true)\n try {\n const answer = await api.saveSettings(normalized)\n setServerIssues({})\n setForm({ ...answer.settings })\n loaded.reload()\n enqueueSnackbar(\n answer.changed ? 'Compliance settings saved.' : 'Nothing changed.',\n {\n variant: answer.changed ? 'success' : 'info',\n },\n )\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n setServerIssues({})\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n variant=\"text\"\n disabled={!dirty || saving}\n onClick={() => {\n setServerIssues({})\n if (stored) setForm({ ...stored })\n }}\n >\n Discard changes\n </Button>\n <Button\n variant=\"contained\"\n disabled={!dirty || saving || issues.length > 0}\n onClick={() => void save()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","countries","stored","settings","normalized","issues","field","find","issue","message","dirty","footer","set","value","previous","save","answer","saveSettings","reload","changed","variant","error","length","Object","fromEntries","filter","map","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","spacing","header","help","anchor","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","next","renderValue","getItemProps","index","key","item","size","renderInput","params","direction","xs","sm","justifyContent","disabled","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState(false)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n useEffect(() => {\n if (stored) setForm({ ...stored })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n const dirty = stored\n ? !outreachComplianceSettingsEqual(stored, normalized)\n : false\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues({})\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const save = async () => {\n setSaving(true)\n try {\n const answer = await api.saveSettings(normalized)\n setServerIssues({})\n setForm({ ...answer.settings })\n loaded.reload()\n enqueueSnackbar(\n answer.changed ? 'Compliance settings saved.' : 'Nothing changed.',\n {\n variant: answer.changed ? 'success' : 'info',\n },\n )\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n setServerIssues({})\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n variant=\"text\"\n disabled={!dirty || saving}\n onClick={() => {\n setServerIssues({})\n if (stored) setForm({ ...stored })\n }}\n >\n Discard changes\n </Button>\n <Button\n variant=\"contained\"\n disabled={!dirty || saving || issues.length > 0}\n onClick={() => void save()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n\n {/* Its own list, saved as it is edited (AGL-3244): no Save above it. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","countries","stored","settings","normalized","issues","field","find","issue","message","dirty","footer","set","value","previous","save","answer","saveSettings","reload","changed","variant","error","length","Object","fromEntries","filter","map","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","spacing","header","help","anchor","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","next","renderValue","getItemProps","index","key","item","size","renderInput","params","direction","xs","sm","justifyContent","disabled","onClick","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OutreachDoNotContactReason } from '../model/outreach.types';
|
|
2
|
+
export interface OutreachDoNotContactDomainsCardProps {
|
|
3
|
+
/** The organization the hub is mounted under; `null` until the shell knows it. */
|
|
4
|
+
orgId: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** Why a domain is on the list, as the card says it. */
|
|
7
|
+
export declare const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string>;
|
|
8
|
+
/** Accessible names of the card's controls, spelled once for the specs. */
|
|
9
|
+
export declare const DO_NOT_CONTACT_DOMAIN_LABELS: {
|
|
10
|
+
readonly field: "Domain";
|
|
11
|
+
readonly add: "Add domain";
|
|
12
|
+
readonly remove: (domain: string) => string;
|
|
13
|
+
};
|
|
14
|
+
/** What the field says under a value that is not a domain. */
|
|
15
|
+
export declare const DO_NOT_CONTACT_DOMAIN_HINT = "A domain, such as example.com. Every address at it is refused.";
|
|
16
|
+
/**
|
|
17
|
+
* Sequences → Compliance → Do not contact domains (AGL-3244): the domains no
|
|
18
|
+
* sequence emails anyone at, whoever enrolls them.
|
|
19
|
+
*
|
|
20
|
+
* A member adds one by hand — a company that asked, or one whose gateway is
|
|
21
|
+
* known to block cold mail — and the sending runtime adds one when a hard
|
|
22
|
+
* bounce reads as the domain's gateway refusing the sender. Either way it
|
|
23
|
+
* is listed here with why, and a member takes it off by name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function OutreachDoNotContactDomainsCard(props: OutreachDoNotContactDomainsCardProps): import("react").JSX.Element;
|
|
26
|
+
export declare namespace OutreachDoNotContactDomainsCard {
|
|
27
|
+
var displayName: string;
|
|
28
|
+
}
|
|
29
|
+
export default OutreachDoNotContactDomainsCard;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
|
+
import { mdiTrashCanOutline } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
+
import { Button, Chip, IconButton, List, ListItem, ListItemText, Stack, TextField, Typography } from "@mui/material";
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
import { normalizeOutreachDomain } from "../engine/do-not-contact-domain.js";
|
|
25
|
+
import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
|
|
26
|
+
import { useOutreachApi } from "./use-outreach-api.js";
|
|
27
|
+
import { useOutreachDoNotContactDomains } from "./use-outreach-data.js";
|
|
28
|
+
/** Why a domain is on the list, as the card says it. */ export const OUTREACH_DO_NOT_CONTACT_REASON_LABELS = {
|
|
29
|
+
manual: 'Added by a member',
|
|
30
|
+
opt_out_reply: 'A reply asked not to be emailed',
|
|
31
|
+
unsubscribe: 'Unsubscribed',
|
|
32
|
+
hard_bounce: 'Mail bounced',
|
|
33
|
+
gateway_block: 'Its mail gateway blocked the sender'
|
|
34
|
+
};
|
|
35
|
+
/** Accessible names of the card's controls, spelled once for the specs. */ export const DO_NOT_CONTACT_DOMAIN_LABELS = {
|
|
36
|
+
field: 'Domain',
|
|
37
|
+
add: 'Add domain',
|
|
38
|
+
remove: (domain)=>`Remove ${domain}`
|
|
39
|
+
};
|
|
40
|
+
/** What the field says under a value that is not a domain. */ export const DO_NOT_CONTACT_DOMAIN_HINT = 'A domain, such as example.com. Every address at it is refused.';
|
|
41
|
+
function addedOn(entry) {
|
|
42
|
+
if (!entry.addedAtMs) return '';
|
|
43
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
44
|
+
dateStyle: 'medium'
|
|
45
|
+
}).format(entry.addedAtMs);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Sequences → Compliance → Do not contact domains (AGL-3244): the domains no
|
|
49
|
+
* sequence emails anyone at, whoever enrolls them.
|
|
50
|
+
*
|
|
51
|
+
* A member adds one by hand — a company that asked, or one whose gateway is
|
|
52
|
+
* known to block cold mail — and the sending runtime adds one when a hard
|
|
53
|
+
* bounce reads as the domain's gateway refusing the sender. Either way it
|
|
54
|
+
* is listed here with why, and a member takes it off by name.
|
|
55
|
+
*/ export function OutreachDoNotContactDomainsCard(props) {
|
|
56
|
+
const { orgId } = props;
|
|
57
|
+
const api = useOutreachApi(orgId);
|
|
58
|
+
const listed = useOutreachDoNotContactDomains(orgId);
|
|
59
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
60
|
+
const [draft, setDraft] = useState('');
|
|
61
|
+
const [busy, setBusy] = useState(null);
|
|
62
|
+
const normalized = normalizeOutreachDomain(draft);
|
|
63
|
+
const invalid = draft.trim() !== '' && normalized === null;
|
|
64
|
+
const change = async (action, domain)=>{
|
|
65
|
+
setBusy(domain);
|
|
66
|
+
try {
|
|
67
|
+
const answer = await api.changeDoNotContactDomain(action, domain);
|
|
68
|
+
if (action === 'add') setDraft('');
|
|
69
|
+
enqueueSnackbar(answer.changed ? action === 'add' ? `${answer.domain} is on the do-not-contact list.` : `${answer.domain} is off the do-not-contact list.` : action === 'add' ? `${answer.domain} was already on the list.` : `${answer.domain} was not on the list.`, {
|
|
70
|
+
variant: answer.changed ? 'success' : 'info'
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
enqueueSnackbar(error.message, {
|
|
74
|
+
variant: 'error',
|
|
75
|
+
allowDuplicate: true
|
|
76
|
+
});
|
|
77
|
+
} finally{
|
|
78
|
+
setBusy(null);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
82
|
+
header: "Do not contact domains",
|
|
83
|
+
help: pluginDocsHelp('sequences', {
|
|
84
|
+
anchor: '#do-not-contact-domains'
|
|
85
|
+
}),
|
|
86
|
+
contentGutterX: true,
|
|
87
|
+
contentGutterY: true,
|
|
88
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
89
|
+
spacing: 2,
|
|
90
|
+
children: [
|
|
91
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
92
|
+
variant: "body2",
|
|
93
|
+
color: "text.secondary",
|
|
94
|
+
children: 'No sequence emails anyone at these domains, whoever enrolls them. Add a company that asked not ' + 'to hear from you, or one whose mail gateway blocks you. When an email bounces because the ' + 'recipient’s gateway refused it — rather than because the address is unknown — the domain is ' + 'added here automatically, so the next person at that company is not tried.'
|
|
95
|
+
}),
|
|
96
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
97
|
+
direction: {
|
|
98
|
+
xs: 'column',
|
|
99
|
+
sm: 'row'
|
|
100
|
+
},
|
|
101
|
+
spacing: 1,
|
|
102
|
+
sx: {
|
|
103
|
+
alignItems: {
|
|
104
|
+
sm: 'flex-start'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
children: [
|
|
108
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
109
|
+
label: DO_NOT_CONTACT_DOMAIN_LABELS.field,
|
|
110
|
+
value: draft,
|
|
111
|
+
onChange: (event)=>setDraft(event.target.value),
|
|
112
|
+
onKeyDown: (event)=>{
|
|
113
|
+
if (event.key === 'Enter' && normalized && !busy) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
void change('add', normalized);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
error: invalid,
|
|
119
|
+
helperText: invalid ? 'That is not a domain.' : DO_NOT_CONTACT_DOMAIN_HINT,
|
|
120
|
+
size: "small",
|
|
121
|
+
sx: {
|
|
122
|
+
flexGrow: 1
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
126
|
+
variant: "outlined",
|
|
127
|
+
disabled: !normalized || busy !== null,
|
|
128
|
+
onClick: ()=>normalized && void change('add', normalized),
|
|
129
|
+
children: DO_NOT_CONTACT_DOMAIN_LABELS.add
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
}),
|
|
133
|
+
listed.status === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
|
|
134
|
+
label: "Loading domains…"
|
|
135
|
+
}) : listed.status === 'error' || listed.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
|
|
136
|
+
status: listed.status,
|
|
137
|
+
what: "the do-not-contact domains"
|
|
138
|
+
}) : listed.data.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
139
|
+
variant: "body2",
|
|
140
|
+
color: "text.secondary",
|
|
141
|
+
children: "No domains yet."
|
|
142
|
+
}) : /*#__PURE__*/ _jsx(List, {
|
|
143
|
+
dense: true,
|
|
144
|
+
disablePadding: true,
|
|
145
|
+
"aria-label": "Do not contact domains",
|
|
146
|
+
children: listed.data.map((entry)=>{
|
|
147
|
+
var _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason;
|
|
148
|
+
return /*#__PURE__*/ _jsx(ListItem, {
|
|
149
|
+
disableGutters: true,
|
|
150
|
+
secondaryAction: /*#__PURE__*/ _jsx(IconButton, {
|
|
151
|
+
edge: "end",
|
|
152
|
+
size: "small",
|
|
153
|
+
"aria-label": DO_NOT_CONTACT_DOMAIN_LABELS.remove(entry.domain),
|
|
154
|
+
disabled: busy !== null,
|
|
155
|
+
onClick: ()=>void change('remove', entry.domain),
|
|
156
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
157
|
+
path: mdiTrashCanOutline.path,
|
|
158
|
+
fontSize: "small"
|
|
159
|
+
})
|
|
160
|
+
}),
|
|
161
|
+
children: /*#__PURE__*/ _jsx(ListItemText, {
|
|
162
|
+
primary: /*#__PURE__*/ _jsxs(Stack, {
|
|
163
|
+
direction: "row",
|
|
164
|
+
spacing: 1,
|
|
165
|
+
sx: {
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
flexWrap: 'wrap'
|
|
168
|
+
},
|
|
169
|
+
children: [
|
|
170
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
171
|
+
variant: "body2",
|
|
172
|
+
component: "span",
|
|
173
|
+
sx: {
|
|
174
|
+
fontWeight: 500
|
|
175
|
+
},
|
|
176
|
+
children: entry.domain
|
|
177
|
+
}),
|
|
178
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
179
|
+
size: "small",
|
|
180
|
+
variant: "outlined",
|
|
181
|
+
color: entry.reason === 'gateway_block' ? 'warning' : 'default',
|
|
182
|
+
label: (_OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason = OUTREACH_DO_NOT_CONTACT_REASON_LABELS[entry.reason]) != null ? _OUTREACH_DO_NOT_CONTACT_REASON_LABELS_entry_reason : entry.reason
|
|
183
|
+
})
|
|
184
|
+
]
|
|
185
|
+
}),
|
|
186
|
+
secondary: [
|
|
187
|
+
addedOn(entry) && `Added ${addedOn(entry)}`,
|
|
188
|
+
entry.detail
|
|
189
|
+
].filter(Boolean).join(' · '),
|
|
190
|
+
slotProps: {
|
|
191
|
+
secondary: {
|
|
192
|
+
sx: {
|
|
193
|
+
wordBreak: 'break-word'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}, entry.domain);
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
]
|
|
202
|
+
})
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
OutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard';
|
|
206
|
+
export default OutreachDoNotContactDomainsCard;
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=do-not-contact-domains.js.map
|
|
@@ -0,0 +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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiTrashCanOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n IconButton,\n List,\n ListItem,\n ListItemText,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useState } from 'react'\nimport { normalizeOutreachDomain } from '../engine/do-not-contact-domain'\nimport type {\n OutreachDoNotContactDomainEntry,\n OutreachDoNotContactReason,\n} from '../model/outreach.types'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { useOutreachApi } from './use-outreach-api'\nimport { useOutreachDoNotContactDomains } 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 const OUTREACH_DO_NOT_CONTACT_REASON_LABELS: Record<OutreachDoNotContactReason, string> = {\n manual: 'Added by a member',\n opt_out_reply: 'A reply asked not to be emailed',\n unsubscribe: 'Unsubscribed',\n hard_bounce: 'Mail bounced',\n gateway_block: 'Its mail gateway blocked the sender',\n}\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} as const\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\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/**\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 listed = useOutreachDoNotContactDomains(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\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 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.'}\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' ? (\n <OutreachLoading label=\"Loading domains…\" />\n ) : listed.status === 'error' || listed.status === 'refused' ? (\n <OutreachLoadProblem status={listed.status} what=\"the do-not-contact domains\" />\n ) : listed.data.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n No domains yet.\n </Typography>\n ) : (\n <List dense disablePadding aria-label=\"Do not contact domains\">\n {listed.data.map((entry) => (\n <ListItem\n key={entry.domain}\n disableGutters\n secondaryAction={\n <IconButton\n edge=\"end\"\n size=\"small\"\n aria-label={DO_NOT_CONTACT_DOMAIN_LABELS.remove(entry.domain)}\n disabled={busy !== null}\n onClick={() => void change('remove', entry.domain)}\n >\n <MdiIcon path={mdiTrashCanOutline.path} fontSize=\"small\" />\n </IconButton>\n }\n >\n <ListItemText\n primary={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>\n <Typography variant=\"body2\" component=\"span\" sx={{ fontWeight: 500 }}>\n {entry.domain}\n </Typography>\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={entry.reason === 'gateway_block' ? 'warning' : 'default'}\n label={OUTREACH_DO_NOT_CONTACT_REASON_LABELS[entry.reason] ?? entry.reason}\n />\n </Stack>\n }\n secondary={[addedOn(entry) && `Added ${addedOn(entry)}`, entry.detail].filter(Boolean).join(' · ')}\n slotProps={{ secondary: { sx: { wordBreak: 'break-word' } } }}\n />\n </ListItem>\n ))}\n </List>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nOutreachDoNotContactDomainsCard.displayName = 'OutreachDoNotContactDomainsCard'\n\nexport default OutreachDoNotContactDomainsCard\n"],"names":["pluginDocsHelp","mdiTrashCanOutline","CardDisplay","MdiIcon","useSnackbar","Button","Chip","IconButton","List","ListItem","ListItemText","Stack","TextField","Typography","useState","normalizeOutreachDomain","OutreachLoading","OutreachLoadProblem","useOutreachApi","useOutreachDoNotContactDomains","OUTREACH_DO_NOT_CONTACT_REASON_LABELS","manual","opt_out_reply","unsubscribe","hard_bounce","gateway_block","DO_NOT_CONTACT_DOMAIN_LABELS","field","add","remove","domain","DO_NOT_CONTACT_DOMAIN_HINT","addedOn","entry","addedAtMs","Intl","DateTimeFormat","dateStyle","format","OutreachDoNotContactDomainsCard","props","orgId","api","listed","enqueueSnackbar","draft","setDraft","busy","setBusy","normalized","invalid","trim","change","action","answer","changeDoNotContactDomain","changed","variant","error","message","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","color","direction","xs","sm","sx","alignItems","label","value","onChange","event","target","onKeyDown","key","preventDefault","helperText","size","flexGrow","disabled","onClick","status","what","data","length","dense","disablePadding","aria-label","map","disableGutters","secondaryAction","edge","path","fontSize","primary","flexWrap","component","fontWeight","reason","secondary","detail","filter","Boolean","join","slotProps","wordBreak","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,MAAM,EACNC,IAAI,EACJC,UAAU,EACVC,IAAI,EACJC,QAAQ,EACRC,YAAY,EACZC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,QAAQ,QAAQ,QAAO;AAChC,SAASC,uBAAuB,QAAQ,qCAAiC;AAKzE,SAASC,eAAe,EAAEC,mBAAmB,QAAQ,mBAAe;AACpE,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,8BAA8B,QAAQ,yBAAqB;AAOpE,sDAAsD,GACtD,OAAO,MAAMC,wCAAoF;IAC/FC,QAAQ;IACRC,eAAe;IACfC,aAAa;IACbC,aAAa;IACbC,eAAe;AACjB,EAAC;AAED,yEAAyE,GACzE,OAAO,MAAMC,+BAA+B;IAC1CC,OAAO;IACPC,KAAK;IACLC,QAAQ,CAACC,SAAmB,CAAC,OAAO,EAAEA,QAAQ;AAChD,EAAU;AAEV,4DAA4D,GAC5D,OAAO,MAAMC,6BAA6B,iEAAgE;AAE1G,SAASC,QAAQC,KAAsC;IACrD,IAAI,CAACA,MAAMC,SAAS,EAAE,OAAO;IAC7B,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS;QAAEC,WAAW;IAAS,GAAGC,MAAM,CAACL,MAAMC,SAAS;AACzF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK,gCAAgCC,KAA2C;IACzF,MAAM,EAAEC,KAAK,EAAE,GAAGD;IAClB,MAAME,MAAMxB,eAAeuB;IAC3B,MAAME,SAASxB,+BAA+BsB;IAC9C,MAAM,EAAEG,eAAe,EAAE,GAAGxC;IAC5B,MAAM,CAACyC,OAAOC,SAAS,GAAGhC,SAAS;IACnC,MAAM,CAACiC,MAAMC,QAAQ,GAAGlC,SAAwB;IAChD,MAAMmC,aAAalC,wBAAwB8B;IAC3C,MAAMK,UAAUL,MAAMM,IAAI,OAAO,MAAMF,eAAe;IAEtD,MAAMG,SAAS,OAAOC,QAA0BvB;QAC9CkB,QAAQlB;QACR,IAAI;YACF,MAAMwB,SAAS,MAAMZ,IAAIa,wBAAwB,CAACF,QAAQvB;YAC1D,IAAIuB,WAAW,OAAOP,SAAS;YAC/BF,gBACEU,OAAOE,OAAO,GACVH,WAAW,QACT,GAAGC,OAAOxB,MAAM,CAAC,+BAA+B,CAAC,GACjD,GAAGwB,OAAOxB,MAAM,CAAC,gCAAgC,CAAC,GACpDuB,WAAW,QACT,GAAGC,OAAOxB,MAAM,CAAC,yBAAyB,CAAC,GAC3C,GAAGwB,OAAOxB,MAAM,CAAC,qBAAqB,CAAC,EAC7C;gBAAE2B,SAASH,OAAOE,OAAO,GAAG,YAAY;YAAO;QAEnD,EAAE,OAAOE,OAAO;YACdd,gBAAgB,AAACc,MAAgBC,OAAO,EAAE;gBAAEF,SAAS;gBAASG,gBAAgB;YAAK;QACrF,SAAU;YACRZ,QAAQ;QACV;IACF;IAEA,qBACE,KAAC9C;QACC2D,QAAO;QACPC,MAAM9D,eAAe,aAAa;YAAE+D,QAAQ;QAA0B;QACtEC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAACtD;YAAMuD,SAAS;;8BACd,KAACrD;oBAAW4C,SAAQ;oBAAQU,OAAM;8BAC/B,oGACC,+FACA,iGACA;;8BAEJ,MAACxD;oBAAMyD,WAAW;wBAAEC,IAAI;wBAAUC,IAAI;oBAAM;oBAAGJ,SAAS;oBAAGK,IAAI;wBAAEC,YAAY;4BAAEF,IAAI;wBAAa;oBAAE;;sCAChG,KAAC1D;4BACC6D,OAAO/C,6BAA6BC,KAAK;4BACzC+C,OAAO7B;4BACP8B,UAAU,CAACC,QAAU9B,SAAS8B,MAAMC,MAAM,CAACH,KAAK;4BAChDI,WAAW,CAACF;gCACV,IAAIA,MAAMG,GAAG,KAAK,WAAW9B,cAAc,CAACF,MAAM;oCAChD6B,MAAMI,cAAc;oCACpB,KAAK5B,OAAO,OAAOH;gCACrB;4BACF;4BACAS,OAAOR;4BACP+B,YAAY/B,UAAU,0BAA0BnB;4BAChDmD,MAAK;4BACLX,IAAI;gCAAEY,UAAU;4BAAE;;sCAEpB,KAAC9E;4BACCoD,SAAQ;4BACR2B,UAAU,CAACnC,cAAcF,SAAS;4BAClCsC,SAAS,IAAMpC,cAAc,KAAKG,OAAO,OAAOH;sCAE/CvB,6BAA6BE,GAAG;;;;gBAGpCe,OAAO2C,MAAM,KAAK,0BACjB,KAACtE;oBAAgByD,OAAM;qBACrB9B,OAAO2C,MAAM,KAAK,WAAW3C,OAAO2C,MAAM,KAAK,0BACjD,KAACrE;oBAAoBqE,QAAQ3C,OAAO2C,MAAM;oBAAEC,MAAK;qBAC/C5C,OAAO6C,IAAI,CAACC,MAAM,KAAK,kBACzB,KAAC5E;oBAAW4C,SAAQ;oBAAQU,OAAM;8BAAiB;mCAInD,KAAC3D;oBAAKkF,KAAK;oBAACC,cAAc;oBAACC,cAAW;8BACnCjD,OAAO6C,IAAI,CAACK,GAAG,CAAC,CAAC5D;4BA0BCb;6CAzBjB,KAACX;4BAECqF,cAAc;4BACdC,+BACE,KAACxF;gCACCyF,MAAK;gCACLd,MAAK;gCACLU,cAAYlE,6BAA6BG,MAAM,CAACI,MAAMH,MAAM;gCAC5DsD,UAAUrC,SAAS;gCACnBsC,SAAS,IAAM,KAAKjC,OAAO,UAAUnB,MAAMH,MAAM;0CAEjD,cAAA,KAAC3B;oCAAQ8F,MAAMhG,mBAAmBgG,IAAI;oCAAEC,UAAS;;;sCAIrD,cAAA,KAACxF;gCACCyF,uBACE,MAACxF;oCAAMyD,WAAU;oCAAMF,SAAS;oCAAGK,IAAI;wCAAEC,YAAY;wCAAU4B,UAAU;oCAAO;;sDAC9E,KAACvF;4CAAW4C,SAAQ;4CAAQ4C,WAAU;4CAAO9B,IAAI;gDAAE+B,YAAY;4CAAI;sDAChErE,MAAMH,MAAM;;sDAEf,KAACxB;4CACC4E,MAAK;4CACLzB,SAAQ;4CACRU,OAAOlC,MAAMsE,MAAM,KAAK,kBAAkB,YAAY;4CACtD9B,KAAK,GAAErD,sDAAAA,qCAAqC,CAACa,MAAMsE,MAAM,CAAC,YAAnDnF,sDAAuDa,MAAMsE,MAAM;;;;gCAIhFC,WAAW;oCAACxE,QAAQC,UAAU,CAAC,MAAM,EAAED,QAAQC,QAAQ;oCAAEA,MAAMwE,MAAM;iCAAC,CAACC,MAAM,CAACC,SAASC,IAAI,CAAC;gCAC5FC,WAAW;oCAAEL,WAAW;wCAAEjC,IAAI;4CAAEuC,WAAW;wCAAa;oCAAE;gCAAE;;2BA7BzD7E,MAAMH,MAAM;;;;;;AAsCjC;AACAS,gCAAgCwE,WAAW,GAAG;AAE9C,eAAexE,gCAA+B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OutreachComplianceIssue } from '../model/compliance-settings';
|
|
2
|
-
import type { OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse } from '../model/outreach-api';
|
|
2
|
+
import type { OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse } from '../model/outreach-api';
|
|
3
3
|
import type { OutreachSequenceDraft, OutreachSequenceIssue } from '../model/sequence-draft';
|
|
4
4
|
/**
|
|
5
5
|
* A route's refusal, carrying its sentence, its stable reason and any
|
|
@@ -27,6 +27,10 @@ export interface OutreachApi {
|
|
|
27
27
|
enroll(sequenceId: string, people: OutreachEnrollPersonRequest[]): Promise<OutreachEnrollResponse>;
|
|
28
28
|
actOnEnrollment(enrollmentId: string, action: OutreachEnrollmentAction, detail?: string): Promise<OutreachEnrollmentActionResponse>;
|
|
29
29
|
previewEmail(input: Omit<OutreachPreviewRequest, 'orgId'>): Promise<OutreachPreviewResponse>;
|
|
30
|
+
/** The domains on the do-not-contact list (AGL-3244). */
|
|
31
|
+
readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>;
|
|
32
|
+
/** Puts a domain on the list, or takes one off. */
|
|
33
|
+
changeDoNotContactDomain(action: OutreachDoNotContactDomainRequest['action'], domain: string, detail?: string): Promise<OutreachDoNotContactDomainResponse>;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* The Outreach pages' one door to the settings, sequence and enrollment
|
|
@@ -130,6 +130,18 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
|
130
130
|
previewEmail: (input)=>call(OUTREACH_API_ROUTES.preview, {
|
|
131
131
|
method: 'POST',
|
|
132
132
|
body: _extends({}, input)
|
|
133
|
+
}),
|
|
134
|
+
readDoNotContactDomains: ()=>call(OUTREACH_API_ROUTES.doNotContactDomains, {
|
|
135
|
+
method: 'GET'
|
|
136
|
+
}),
|
|
137
|
+
changeDoNotContactDomain: (action, domain, detail)=>call(OUTREACH_API_ROUTES.doNotContactDomains, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
body: _extends({
|
|
140
|
+
action,
|
|
141
|
+
domain
|
|
142
|
+
}, detail ? {
|
|
143
|
+
detail
|
|
144
|
+
} : {})
|
|
133
145
|
})
|
|
134
146
|
}), [
|
|
135
147
|
call
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","previewEmail","preview"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","previewEmail","preview","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CampaignLinkRollup } from '@aglyn/shared-ui-email-campaigns/model';
|
|
2
|
-
import { type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
|
|
2
|
+
import { type OutreachDoNotContactDomainEntry, type OutreachEnrollment, type OutreachSequence } from '../model/outreach.types';
|
|
3
3
|
/**
|
|
4
4
|
* The Outreach documents the console reads LIVE (AGL-2980): sequences,
|
|
5
5
|
* enrollments, and the mailboxes they send from.
|
|
@@ -34,6 +34,13 @@ export declare function useOutreachSequence(orgId: string | null, sequenceId: st
|
|
|
34
34
|
*/
|
|
35
35
|
export type OutreachSequenceLinksLoad = OutreachLoad<CampaignLinkRollup | null>;
|
|
36
36
|
export declare function useOutreachSequenceLinks(orgId: string | null, sequenceId: string | null): OutreachSequenceLinksLoad;
|
|
37
|
+
/**
|
|
38
|
+
* The domains on the organization's do-not-contact list (AGL-3244),
|
|
39
|
+
* alphabetically, live: an add or a remove through the route shows up here
|
|
40
|
+
* without a reload, and so does a domain the sending runtime files after a
|
|
41
|
+
* gateway block.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useOutreachDoNotContactDomains(orgId: string | null): OutreachLoad<OutreachDoNotContactDomainEntry[]>;
|
|
37
44
|
/** How many enrollments one page of the table reads; "Show more" reads another. */
|
|
38
45
|
export declare const OUTREACH_ENROLLMENTS_PAGE = 200;
|
|
39
46
|
export interface OutreachEnrollmentsLoad extends OutreachLoad<OutreachEnrollment[]> {
|