@aglyn/plugins-outreach 1.0.0-beta.151 → 1.0.0-beta.153

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 (64) hide show
  1. package/README.md +5 -1
  2. package/package.json +12 -12
  3. package/src/lib/components/compliance-section.js +1 -1
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.js +142 -32
  6. package/src/lib/components/enroll-dialog.js.map +1 -1
  7. package/src/lib/components/enrollments-table.js +37 -8
  8. package/src/lib/components/enrollments-table.js.map +1 -1
  9. package/src/lib/components/mailbox-card.d.ts +3 -0
  10. package/src/lib/components/mailbox-card.js +44 -8
  11. package/src/lib/components/mailbox-card.js.map +1 -1
  12. package/src/lib/components/use-outreach-crm.d.ts +20 -1
  13. package/src/lib/components/use-outreach-crm.js +78 -4
  14. package/src/lib/components/use-outreach-crm.js.map +1 -1
  15. package/src/lib/components/use-outreach-mailbox-api.d.ts +2 -1
  16. package/src/lib/components/use-outreach-mailbox-api.js +5 -3
  17. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
  18. package/src/lib/declarations.console-server.js +14 -0
  19. package/src/lib/declarations.console-server.js.map +1 -1
  20. package/src/lib/engine/compose.d.ts +7 -2
  21. package/src/lib/engine/compose.js +11 -5
  22. package/src/lib/engine/compose.js.map +1 -1
  23. package/src/lib/engine/enrollment-state.d.ts +6 -1
  24. package/src/lib/engine/enrollment-state.js +3 -1
  25. package/src/lib/engine/enrollment-state.js.map +1 -1
  26. package/src/lib/engine/opt-out-intent.js +1 -1
  27. package/src/lib/engine/opt-out-intent.js.map +1 -1
  28. package/src/lib/enrollment/enroll-people.d.ts +52 -7
  29. package/src/lib/enrollment/enroll-people.js +109 -12
  30. package/src/lib/enrollment/enroll-people.js.map +1 -1
  31. package/src/lib/enrollment/gate-lookups.d.ts +12 -3
  32. package/src/lib/enrollment/gate-lookups.js +17 -14
  33. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  34. package/src/lib/mailboxes/mailbox-api.d.ts +27 -2
  35. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  36. package/src/lib/mailboxes/mailbox-routes.js +52 -8
  37. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  38. package/src/lib/mailboxes/oauth-redirect.d.ts +15 -4
  39. package/src/lib/mailboxes/oauth-redirect.js +15 -6
  40. package/src/lib/mailboxes/oauth-redirect.js.map +1 -1
  41. package/src/lib/model/outreach-api.d.ts +39 -8
  42. package/src/lib/model/outreach-api.js.map +1 -1
  43. package/src/lib/model/outreach.types.d.ts +29 -4
  44. package/src/lib/model/outreach.types.js +9 -0
  45. package/src/lib/model/outreach.types.js.map +1 -1
  46. package/src/lib/model/stored-records.js +7 -0
  47. package/src/lib/model/stored-records.js.map +1 -1
  48. package/src/lib/routes/enroll-routes.js +112 -39
  49. package/src/lib/routes/enroll-routes.js.map +1 -1
  50. package/src/lib/routes/preview-routes.js +18 -5
  51. package/src/lib/routes/preview-routes.js.map +1 -1
  52. package/src/lib/runtime/lead-records.d.ts +26 -0
  53. package/src/lib/runtime/lead-records.js +84 -0
  54. package/src/lib/runtime/lead-records.js.map +1 -0
  55. package/src/lib/runtime/send-job.js +30 -12
  56. package/src/lib/runtime/send-job.js.map +1 -1
  57. package/src/lib/runtime/sync-job.js +11 -3
  58. package/src/lib/runtime/sync-job.js.map +1 -1
  59. package/src/lib/runtime/timeline.d.ts +6 -4
  60. package/src/lib/runtime/timeline.js +3 -7
  61. package/src/lib/runtime/timeline.js.map +1 -1
  62. package/src/lib/storage/outreach-records.d.ts +21 -2
  63. package/src/lib/storage/outreach-records.js +31 -2
  64. package/src/lib/storage/outreach-records.js.map +1 -1
package/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # @aglyn/plugins-outreach
2
2
 
3
3
  Sequences: one-to-one, multi-step email sequences a rep sends from
4
- their own connected mailbox, logged on the CRM's records.
4
+ their own connected mailbox, logged on the CRM's records. A sequence
5
+ enrolls a contact or a lead (AGL-3234): an enrollment names which record the
6
+ person is, a lead is judged and merged as the contact it would be, and when
7
+ the lead converts its enrollment follows it to the contact through the
8
+ core's lead-conversion seam.
5
9
 
6
10
  > Beta. Published from the Aglyn monorepo under the `beta` dist-tag; APIs can change between beta releases.
7
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.151",
3
+ "version": "1.0.0-beta.153",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,17 +25,17 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.151",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.151",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.151",
31
- "@aglyn/shared-ui-next": "1.0.0-beta.151",
32
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.151",
33
- "@aglyn/shared-util-http": "1.0.0-beta.151",
34
- "@aglyn/shared-util-timestamp": "1.0.0-beta.151",
35
- "@aglyn/shared-util-tools": "1.0.0-beta.151",
36
- "@aglyn/tenant-data-admin": "1.0.0-beta.151",
37
- "@aglyn/tenant-feature-instance": "1.0.0-beta.151",
38
- "@aglyn/tenant-runtime": "1.0.0-beta.151",
28
+ "@aglyn/aglyn": "1.0.0-beta.153",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.153",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.153",
31
+ "@aglyn/shared-ui-next": "1.0.0-beta.153",
32
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.153",
33
+ "@aglyn/shared-util-http": "1.0.0-beta.153",
34
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.153",
35
+ "@aglyn/shared-util-tools": "1.0.0-beta.153",
36
+ "@aglyn/tenant-data-admin": "1.0.0-beta.153",
37
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.153",
38
+ "@aglyn/tenant-runtime": "1.0.0-beta.153",
39
39
  "@swc/helpers": "0.5.23"
40
40
  },
41
41
  "peerDependencies": {
@@ -135,7 +135,7 @@ const EMPTY = {
135
135
  /*#__PURE__*/ _jsx(Typography, {
136
136
  variant: "body2",
137
137
  color: "text.secondary",
138
- children: 'Every email a sequence sends ends with a footer naming your organization, its postal ' + 'address, that the email is a business solicitation, and how to stop more of them. The law ' + 'requires these on commercial email, so every email gets the footer automatically.'
138
+ children: 'Every email a sequence sends ends with a footer naming your organization, its postal ' + 'address, that the email is a sales email, and how to stop more of them. The law ' + 'requires these on commercial email, so every email gets the footer automatically.'
139
139
  }),
140
140
  /*#__PURE__*/ _jsx(TextField, {
141
141
  label: "Legal name",
@@ -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 business solicitation, 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 { 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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"}
@@ -23,7 +23,7 @@ import { useMemo, useState } from "react";
23
23
  import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
24
24
  import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
25
25
  import { OutreachLoading, OutreachLoadProblem } from "./outreach-ui.js";
26
- import { useOutreachContactSearch, useOutreachSavedViews } from "./use-outreach-crm.js";
26
+ import { useOutreachContactSearch, useOutreachLeadSearch, useOutreachSavedViews } from "./use-outreach-crm.js";
27
27
  const STATUS_LABELS = {
28
28
  eligible: 'Eligible',
29
29
  needs_confirmation: 'Needs you',
@@ -59,6 +59,10 @@ const STATUS_COLORS = {
59
59
  const [viewId, setViewId] = useState('');
60
60
  const [text, setText] = useState('');
61
61
  const [picked, setPicked] = useState(new Map());
62
+ // The leads picked from the sequence's site (AGL-3234), kept apart from
63
+ // the contacts: the two are different records and the source names which.
64
+ const [leadText, setLeadText] = useState('');
65
+ const [pickedLeads, setPickedLeads] = useState(new Map());
62
66
  const [stage, setStage] = useState({
63
67
  kind: 'source'
64
68
  });
@@ -72,6 +76,11 @@ const STATUS_COLORS = {
72
76
  contactGroupId: props.contactGroupId,
73
77
  text
74
78
  });
79
+ const leadSearch = useOutreachLeadSearch({
80
+ hostId: sequence.hostId,
81
+ text: leadText,
82
+ enabled: props.open && tab === 'leads'
83
+ });
75
84
  const reset = ()=>{
76
85
  setStage({
77
86
  kind: 'source'
@@ -83,7 +92,9 @@ const STATUS_COLORS = {
83
92
  const close = ()=>{
84
93
  reset();
85
94
  setPicked(new Map());
95
+ setPickedLeads(new Map());
86
96
  setText('');
97
+ setLeadText('');
87
98
  setViewId('');
88
99
  props.onClose();
89
100
  };
@@ -95,7 +106,7 @@ const STATUS_COLORS = {
95
106
  try {
96
107
  const answer = await api.previewEnrollment(sequence.id, source);
97
108
  setSupplied(Object.fromEntries(answer.people.map((person)=>[
98
- person.contactId,
109
+ person.personId,
99
110
  {
100
111
  include: person.status !== 'blocked',
101
112
  personalLine: '',
@@ -114,13 +125,13 @@ const STATUS_COLORS = {
114
125
  }
115
126
  };
116
127
  const answer = stage.kind === 'preview' || stage.kind === 'enrolling' ? stage.answer : null;
117
- const ready = useMemo(()=>answer ? answer.people.filter((person)=>outreachPersonReady(person, supplied[person.contactId])) : [], [
128
+ const ready = useMemo(()=>answer ? answer.people.filter((person)=>outreachPersonReady(person, supplied[person.personId])) : [], [
118
129
  answer,
119
130
  supplied
120
131
  ]);
121
132
  const waiting = answer ? answer.people.filter((person)=>{
122
- var _supplied_person_contactId;
123
- return person.status === 'needs_confirmation' && ((_supplied_person_contactId = supplied[person.contactId]) == null ? void 0 : _supplied_person_contactId.include) && !outreachPersonReady(person, supplied[person.contactId]);
133
+ var _supplied_person_personId;
134
+ return person.status === 'needs_confirmation' && ((_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.include) && !outreachPersonReady(person, supplied[person.personId]);
124
135
  }).length : 0;
125
136
  const enroll = async ()=>{
126
137
  if (!answer || !ready.length) return;
@@ -132,12 +143,15 @@ const STATUS_COLORS = {
132
143
  try {
133
144
  const result = await api.enroll(sequence.id, ready.map((person)=>{
134
145
  var _ref, _ref1;
135
- var _supplied_person_contactId, _supplied_person_contactId1;
136
- return {
137
- contactId: person.contactId,
138
- personalLine: (_ref = (_supplied_person_contactId = supplied[person.contactId]) == null ? void 0 : _supplied_person_contactId.personalLine.trim()) != null ? _ref : '',
139
- attestations: (_ref1 = (_supplied_person_contactId1 = supplied[person.contactId]) == null ? void 0 : _supplied_person_contactId1.attestations) != null ? _ref1 : []
140
- };
146
+ var _supplied_person_personId, _supplied_person_personId1;
147
+ return _extends({}, person.target === 'lead' && person.leadId ? {
148
+ leadId: person.leadId
149
+ } : {
150
+ contactId: person.contactId
151
+ }, {
152
+ personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : '',
153
+ attestations: (_ref1 = (_supplied_person_personId1 = supplied[person.personId]) == null ? void 0 : _supplied_person_personId1.attestations) != null ? _ref1 : []
154
+ });
141
155
  }));
142
156
  setStage({
143
157
  kind: 'done',
@@ -152,32 +166,36 @@ const STATUS_COLORS = {
152
166
  });
153
167
  }
154
168
  };
155
- const supply = (contactId, next)=>setSupplied((previous)=>_extends({}, previous, {
156
- [contactId]: _extends({}, previous[contactId], next)
169
+ const supply = (personId, next)=>setSupplied((previous)=>_extends({}, previous, {
170
+ [personId]: _extends({}, previous[personId], next)
157
171
  }));
158
172
  const previewEmail = async (person)=>{
159
173
  setEmailPreview((previous)=>_extends({}, previous, {
160
- [person.contactId]: {
174
+ [person.personId]: {
161
175
  status: 'loading'
162
176
  }
163
177
  }));
164
178
  try {
165
179
  var _ref;
166
- var _supplied_person_contactId;
167
- const result = await api.previewEmail({
168
- sequenceId: sequence.id,
169
- contactId: person.contactId,
170
- personalLine: (_ref = (_supplied_person_contactId = supplied[person.contactId]) == null ? void 0 : _supplied_person_contactId.personalLine.trim()) != null ? _ref : ''
171
- });
180
+ var _supplied_person_personId;
181
+ const result = await api.previewEmail(_extends({
182
+ sequenceId: sequence.id
183
+ }, person.target === 'lead' && person.leadId ? {
184
+ leadId: person.leadId
185
+ } : {
186
+ contactId: person.contactId
187
+ }, {
188
+ personalLine: (_ref = (_supplied_person_personId = supplied[person.personId]) == null ? void 0 : _supplied_person_personId.personalLine.trim()) != null ? _ref : ''
189
+ }));
172
190
  setEmailPreview((previous)=>_extends({}, previous, {
173
- [person.contactId]: {
191
+ [person.personId]: {
174
192
  status: 'ready',
175
193
  answer: result
176
194
  }
177
195
  }));
178
196
  } catch (error) {
179
197
  setEmailPreview((previous)=>_extends({}, previous, {
180
- [person.contactId]: {
198
+ [person.personId]: {
181
199
  status: 'failed',
182
200
  message: error.message
183
201
  }
@@ -233,6 +251,10 @@ const STATUS_COLORS = {
233
251
  /*#__PURE__*/ _jsx(Tab, {
234
252
  value: "search",
235
253
  label: "Search"
254
+ }),
255
+ /*#__PURE__*/ _jsx(Tab, {
256
+ value: "leads",
257
+ label: "Leads"
236
258
  })
237
259
  ]
238
260
  }),
@@ -247,15 +269,93 @@ const STATUS_COLORS = {
247
269
  label: "Saved Contacts view",
248
270
  value: viewId,
249
271
  onChange: (event)=>setViewId(event.target.value),
250
- helperText: `Enrolls up to ${OUTREACH_ENROLL_BATCH_MAX} of the people the view shows at this sequence’s site.`,
272
+ helperText: `Enrolls up to ${OUTREACH_ENROLL_BATCH_MAX} of the people the view shows at this sequence’s site — a Contacts view, or a Leads view of the site’s leads.`,
251
273
  fullWidth: true,
252
274
  children: views.data.map((view)=>/*#__PURE__*/ _jsx(MenuItem, {
253
275
  value: view.id,
254
- children: view.name
276
+ children: view.section === 'leads' ? `${view.name} (Leads)` : view.name
255
277
  }, view.id))
256
278
  }) : /*#__PURE__*/ _jsx(Alert, {
257
279
  severity: "info",
258
280
  children: "You have no saved Contacts views to enroll from. Save one in the CRM, or search instead."
281
+ }) : tab === 'leads' ? /*#__PURE__*/ _jsxs(Stack, {
282
+ spacing: 1.5,
283
+ children: [
284
+ /*#__PURE__*/ _jsx(TextField, {
285
+ label: "Search leads",
286
+ placeholder: "A name, or an email address",
287
+ value: leadText,
288
+ onChange: (event)=>setLeadText(event.target.value),
289
+ helperText: "The open leads at this sequence’s site. A lead is enrolled as it is, and follows itself to the contact it becomes.",
290
+ fullWidth: true,
291
+ autoFocus: true
292
+ }),
293
+ leadSearch.status === 'loading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
294
+ label: "Reading leads…"
295
+ }) : null,
296
+ leadSearch.status === 'error' || leadSearch.status === 'refused' ? /*#__PURE__*/ _jsx(OutreachLoadProblem, {
297
+ status: leadSearch.status,
298
+ what: "leads",
299
+ message: leadSearch.status === 'refused' ? 'Enrolling reads your CRM’s leads, and your role does not include Manage data.' : 'The leads could not be read. Try again.'
300
+ }) : null,
301
+ leadSearch.status === 'ready' && !leadSearch.idle && !leadSearch.data.length ? /*#__PURE__*/ _jsx(Typography, {
302
+ variant: "body2",
303
+ color: "text.secondary",
304
+ children: "No open leads at this sequence’s site match."
305
+ }) : null,
306
+ leadSearch.data.length ? /*#__PURE__*/ _jsx(List, {
307
+ dense: true,
308
+ "aria-label": "Lead results",
309
+ children: leadSearch.data.map((lead)=>{
310
+ const checked = pickedLeads.has(lead.id);
311
+ return /*#__PURE__*/ _jsx(ListItem, {
312
+ disablePadding: true,
313
+ children: /*#__PURE__*/ _jsxs(ListItemButton, {
314
+ onClick: ()=>setPickedLeads((previous)=>{
315
+ const next = new Map(previous);
316
+ if (checked) next.delete(lead.id);
317
+ else if (next.size < OUTREACH_ENROLL_BATCH_MAX) next.set(lead.id, lead);
318
+ return next;
319
+ }),
320
+ children: [
321
+ /*#__PURE__*/ _jsx(ListItemIcon, {
322
+ children: /*#__PURE__*/ _jsx(Checkbox, {
323
+ edge: "start",
324
+ checked: checked,
325
+ tabIndex: -1,
326
+ disableRipple: true
327
+ })
328
+ }),
329
+ /*#__PURE__*/ _jsx(ListItemText, {
330
+ primary: lead.name || lead.email || lead.id,
331
+ secondary: lead.email
332
+ })
333
+ ]
334
+ })
335
+ }, lead.id);
336
+ })
337
+ }) : null,
338
+ pickedLeads.size ? /*#__PURE__*/ _jsx(Stack, {
339
+ direction: "row",
340
+ spacing: 1,
341
+ sx: {
342
+ flexWrap: 'wrap',
343
+ rowGap: 1
344
+ },
345
+ "aria-label": "Picked leads",
346
+ children: [
347
+ ...pickedLeads.values()
348
+ ].map((lead)=>/*#__PURE__*/ _jsx(Chip, {
349
+ size: "small",
350
+ label: lead.name || lead.email || lead.id,
351
+ onDelete: ()=>setPickedLeads((previous)=>{
352
+ const next = new Map(previous);
353
+ next.delete(lead.id);
354
+ return next;
355
+ })
356
+ }, lead.id))
357
+ }) : null
358
+ ]
259
359
  }) : /*#__PURE__*/ _jsxs(Stack, {
260
360
  spacing: 1.5,
261
361
  children: [
@@ -362,12 +462,12 @@ const STATUS_COLORS = {
362
462
  "aria-label": "People",
363
463
  children: answer.people.map((person)=>/*#__PURE__*/ _jsx(PersonRow, {
364
464
  person: person,
365
- supplied: supplied[person.contactId],
366
- onSupply: (next)=>supply(person.contactId, next),
367
- emailPreview: emailPreview[person.contactId],
465
+ supplied: supplied[person.personId],
466
+ onSupply: (next)=>supply(person.personId, next),
467
+ emailPreview: emailPreview[person.personId],
368
468
  onPreviewEmail: ()=>void previewEmail(person),
369
469
  disabled: stage.kind === 'enrolling'
370
- }, person.contactId))
470
+ }, person.personId))
371
471
  }),
372
472
  waiting ? /*#__PURE__*/ _jsx(Typography, {
373
473
  variant: "body2",
@@ -410,10 +510,15 @@ const STATUS_COLORS = {
410
510
  ]
411
511
  }) : /*#__PURE__*/ _jsx(Button, {
412
512
  variant: "contained",
413
- disabled: stage.kind === 'previewing' || (tab === 'view' ? !viewId : !picked.size),
513
+ disabled: stage.kind === 'previewing' || (tab === 'view' ? !viewId : tab === 'leads' ? !pickedLeads.size : !picked.size),
414
514
  onClick: ()=>void preview(tab === 'view' ? {
415
515
  kind: 'view',
416
516
  viewId
517
+ } : tab === 'leads' ? {
518
+ kind: 'leads',
519
+ leadIds: [
520
+ ...pickedLeads.keys()
521
+ ]
417
522
  } : {
418
523
  kind: 'contacts',
419
524
  contactIds: [
@@ -437,7 +542,7 @@ function PersonRow(props) {
437
542
  var _ref, _ref1;
438
543
  var _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
439
544
  const { person, supplied, disabled } = props;
440
- const name = person.name || person.email || person.contactId;
545
+ const name = person.name || person.email || person.personId;
441
546
  const line = (_ref = supplied == null ? void 0 : supplied.personalLine) != null ? _ref : '';
442
547
  const tooLong = line.trim().length > OUTREACH_PERSONAL_LINE_MAX;
443
548
  return /*#__PURE__*/ _jsx(Paper, {
@@ -486,6 +591,11 @@ function PersonRow(props) {
486
591
  alignItems: 'center'
487
592
  },
488
593
  children: [
594
+ person.target === 'lead' ? /*#__PURE__*/ _jsx(Chip, {
595
+ size: "small",
596
+ variant: "outlined",
597
+ label: "Lead"
598
+ }) : null,
489
599
  person.cold ? /*#__PURE__*/ _jsx(Chip, {
490
600
  size: "small",
491
601
  variant: "outlined",
@@ -636,7 +746,7 @@ function DoneSummary(props) {
636
746
  children: [
637
747
  /*#__PURE__*/ _jsx(Typography, {
638
748
  variant: "body2",
639
- children: (_result_email = result.email) != null ? _result_email : result.contactId
749
+ children: (_result_email = result.email) != null ? _result_email : result.personId
640
750
  }),
641
751
  result.blocks.map((block)=>/*#__PURE__*/ _jsx(Typography, {
642
752
  variant: "body2",
@@ -644,7 +754,7 @@ function DoneSummary(props) {
644
754
  children: block.reason
645
755
  }, block.code))
646
756
  ]
647
- }, result.contactId);
757
+ }, result.personId);
648
758
  })
649
759
  ]
650
760
  }) : null