@aglyn/plugins-outreach 1.0.0-beta.195 → 1.0.0-beta.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/curate-step-dialog.d.ts +50 -0
- package/src/lib/components/curate-step-dialog.js +235 -0
- package/src/lib/components/curate-step-dialog.js.map +1 -0
- package/src/lib/components/curated-step-editor.d.ts +80 -0
- package/src/lib/components/curated-step-editor.js +255 -0
- package/src/lib/components/curated-step-editor.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +15 -0
- package/src/lib/components/enroll-dialog.js +214 -14
- package/src/lib/components/enroll-dialog.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +2 -0
- package/src/lib/components/enrollments-table.js +62 -8
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/mailbox-card.js +11 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -0
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.d.ts +2 -0
- package/src/lib/components/sequence-editor.js +26 -2
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-step-card.d.ts +7 -0
- package/src/lib/components/sequence-step-card.js +37 -14
- package/src/lib/components/sequence-step-card.js.map +1 -1
- package/src/lib/components/sequences-section.js +12 -3
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/step-test-dialog.d.ts +31 -0
- package/src/lib/components/step-test-dialog.js +247 -0
- package/src/lib/components/step-test-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +15 -1
- package/src/lib/components/use-outreach-api.js +16 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +6 -1
- package/src/lib/components/use-outreach-data.js +6 -3
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +15 -0
- package/src/lib/constants/api-routes.js +12 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/compose.d.ts +12 -2
- package/src/lib/engine/compose.js +32 -6
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/curation-prompt.d.ts +73 -0
- package/src/lib/engine/curation-prompt.js +151 -0
- package/src/lib/engine/curation-prompt.js.map +1 -0
- package/src/lib/engine/enrollment-activity.d.ts +24 -0
- package/src/lib/engine/enrollment-activity.js +25 -0
- package/src/lib/engine/enrollment-activity.js.map +1 -1
- package/src/lib/engine/sequence-validation.d.ts +10 -1
- package/src/lib/engine/sequence-validation.js +54 -0
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/enrollment/step-overrides.d.ts +53 -0
- package/src/lib/enrollment/step-overrides.js +140 -0
- package/src/lib/enrollment/step-overrides.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +89 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +42 -0
- package/src/lib/model/outreach.types.js +18 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +10 -2
- package/src/lib/model/step-preview.js +9 -5
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +9 -1
- package/src/lib/model/stored-records.js +47 -2
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/curate-routes.d.ts +63 -0
- package/src/lib/routes/curate-routes.js +424 -0
- package/src/lib/routes/curate-routes.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +7 -0
- package/src/lib/routes/enroll-routes.js +58 -4
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -2
- package/src/lib/routes/platform-deps.js +2 -2
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.d.ts +24 -0
- package/src/lib/routes/preview-routes.js +103 -85
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +41 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/step-test-routes.d.ts +88 -0
- package/src/lib/routes/step-test-routes.js +275 -0
- package/src/lib/routes/step-test-routes.js.map +1 -0
- package/src/lib/runtime/click-events.js +10 -0
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +11 -0
- package/src/lib/runtime/click-link.js +9 -4
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.js +3 -0
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/send-job.js +2 -0
- package/src/lib/runtime/send-job.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-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, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n OUTREACH_SEQUENCE_STATUS_LABELS,\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequences,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/*\n * What the sequences grid's Filters panel offers (AGL-3317). The list holds\n * every sequence it read, so each clause and the quick search are answered\n * over that whole set before it is paged, never over the page on screen.\n */\nconst SEQUENCE_FILTER_FIELDS = [\n inMemoryListField('name', 'text'),\n inMemoryListField('mailbox', 'text'),\n inMemoryListField('status', 'select'),\n]\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n}\nconst SEQUENCE_FILTER_OPTIONS = {\n status: OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n })),\n}\n/** What the quick search reads on a sequence row. */\nconst SEQUENCE_SEARCH_FIELDS = ['name', 'mailbox'] as const\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const sequences = useOutreachSequences(orgId)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status'] })\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailbox.sendAs || mailbox.email\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n /*\n * Filtered BEFORE the page is cut: the list read every sequence (under\n * `OUTREACH_SEQUENCES_LIMIT`), so a clause or a search word answers over\n * all of them, and the footer counts the matches.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const matching = useMemo(\n () =>\n filterListRows(\n sequences.data.map((sequence) => ({\n sequence,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n })),\n SEQUENCE_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: SEQUENCE_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.sequence),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [sequences.data, props.mailboxes, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n const pageRows = matching.slice(page * pageSize, (page + 1) * pageSize)\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={matching.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n const chips = (\n <ListFilterChips\n fields={SEQUENCE_FILTER_FIELDS}\n headers={SEQUENCE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SEQUENCE_FILTER_OPTIONS}\n />\n )\n let body\n if (sequences.status === 'loading')\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n } else if (!sequences.data.length) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n {chips}\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n {footer}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = listFilterGridColumns([\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ], SEQUENCE_FILTER_FIELDS, SEQUENCE_FILTER_OPTIONS, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n /*\n * The panel and the search are the grid's; the list answers them\n * over every sequence it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No sequences match these filters\"\n hideFooter\n />\n {footer}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action:\n sequences.status === 'ready' && sequences.data.length\n ? newButton\n : undefined,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListFilterChips","ListPagination","ListTable","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequences","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","org","sequenceId","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","SEQUENCE_FILTER_FIELDS","SEQUENCE_FILTER_HEADERS","name","mailbox","status","SEQUENCE_FILTER_OPTIONS","map","value","SEQUENCE_SEARCH_FIELDS","theme","narrow","breakpoints","down","sequences","page","setPage","pageSize","setPageSize","gridFilter","selectFields","mailboxLabel","find","entry","mailboxId","sendAs","email","searchKey","searchWords","join","matching","data","clauses","paths","words","row","pageRows","slice","counts","footer","rowCount","length","count","onPageChange","onPageSizeChange","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","key","String","chips","fields","headers","onChange","setClauses","options","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","align","headerAlign","rows","$id","Object","fromEntries","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequences-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, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListTable,\n type ListTableProps,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n filterListRows,\n inMemoryListField,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport {\n Alert,\n Button,\n Card,\n CardContent,\n Chip,\n Stack,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n OUTREACH_SEQUENCE_STATUSES,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport {\n OUTREACH_SEQUENCE_STATUS_LABELS,\n OutreachLink,\n OutreachLoading,\n OutreachLoadProblem,\n OutreachSequenceStatusChip,\n useOutreachNavigate,\n} from './outreach-ui'\nimport {\n OutreachSequenceDetail,\n type OutreachSequenceTab,\n} from './sequence-detail'\nimport { OutreachSequenceEditor } from './sequence-editor'\nimport { useOutreachApi } from './use-outreach-api'\nimport {\n type OutreachSequenceCounts,\n useOutreachSequenceCounts,\n useOutreachSequences,\n} from './use-outreach-data'\nimport { useOutreachMailboxes } from './use-outreach-mailboxes'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachSequencesSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n orgMount?: ConsolePluginOrgMount\n org?: Record<string, unknown> | null\n /** The section's own path: `/[orgSlug]/outreach/sequences`. */\n sectionPath: string\n /** The path below the section: `[]`, `['new']`, `[id]` or `[id, 'enrollments']`. */\n subpath: readonly string[]\n /** The Mailboxes section: `/[orgSlug]/outreach/mailboxes`. */\n mailboxesPath: string\n /** The Compliance section: `/[orgSlug]/outreach/compliance`. */\n compliancePath: string\n}\n\n/** The new-sequence page's segment below the section. */\nexport const OUTREACH_NEW_SEQUENCE_SEGMENT = 'new'\n\n/**\n * The Sequences section (AGL-2980), and the pages below it: the list, a new\n * sequence, and one sequence's steps and enrollments. Each is a real URL, so\n * a sequence is a link a rep can keep and the back button walks them.\n */\nexport function OutreachSequencesSection(props: OutreachSequencesSectionProps) {\n const { orgId, sectionPath, subpath } = props\n const api = useOutreachApi(orgId)\n const settings = useOutreachComplianceSettings(api, orgId)\n const mailboxes = useOutreachMailboxes(orgId)\n const navigate = useOutreachNavigate()\n\n if (!orgId) return <OutreachLoading label=\"Loading sequences…\" />\n const [first, second] = subpath\n if (first === OUTREACH_NEW_SEQUENCE_SEGMENT) {\n return (\n <Stack spacing={2}>\n <OutreachLink href={sectionPath}>Sequences</OutreachLink>\n <Typography variant=\"h5\" component=\"h2\">\n New sequence\n </Typography>\n <OutreachSequenceEditor\n orgId={orgId}\n orgMount={props.orgMount}\n sequence={null}\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n onSaved={(sequence) => navigate(`${sectionPath}/${sequence.id}`)}\n onCancel={() => navigate(sectionPath)}\n />\n </Stack>\n )\n }\n if (first) {\n return (\n <OutreachSequenceDetail\n orgId={orgId}\n orgMount={props.orgMount}\n org={props.org}\n sectionPath={sectionPath}\n sequenceId={first}\n tab={\n (second === 'enrollments'\n ? 'enrollments'\n : 'steps') as OutreachSequenceTab\n }\n settings={settings}\n mailboxes={mailboxes}\n mailboxesPath={props.mailboxesPath}\n compliancePath={props.compliancePath}\n />\n )\n }\n return (\n <OutreachSequenceList\n orgId={orgId}\n sectionPath={sectionPath}\n mailboxes={mailboxes.mailboxes}\n />\n )\n}\nOutreachSequencesSection.displayName = 'OutreachSequencesSection'\n\nconst COUNT_COLUMNS: ReadonlyArray<[keyof OutreachSequenceCounts, string]> = [\n ['enrolled', 'Enrolled'],\n ['active', 'Active'],\n ['replied', 'Replied'],\n ['bounced', 'Bounced'],\n ['optedOut', 'Opted out'],\n]\n\n/*\n * What the sequences grid's Filters panel offers (AGL-3317). The list holds\n * every sequence it read, so each clause and the quick search are answered\n * over that whole set before it is paged, never over the page on screen.\n */\nconst SEQUENCE_FILTER_FIELDS = [\n inMemoryListField('name', 'text'),\n inMemoryListField('mailbox', 'text'),\n inMemoryListField('status', 'select'),\n]\nconst SEQUENCE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Name',\n mailbox: 'Mailbox',\n status: 'Status',\n}\nconst SEQUENCE_FILTER_OPTIONS = {\n status: OUTREACH_SEQUENCE_STATUSES.map((status) => ({\n value: status,\n label: OUTREACH_SEQUENCE_STATUS_LABELS[status],\n })),\n}\n/** What the quick search reads on a sequence row. */\nconst SEQUENCE_SEARCH_FIELDS = ['name', 'mailbox'] as const\n\n/**\n * Every sequence: its mailbox, its status, and how many people it enrolled\n * and where they stand — a table on wider screens, cards on a phone.\n */\nexport function OutreachSequenceList(props: {\n orgId: string\n sectionPath: string\n mailboxes: readonly OutreachMailbox[]\n}) {\n const { orgId, sectionPath } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('md'))\n const navigate = useOutreachNavigate()\n const sequences = useOutreachSequences(orgId)\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const gridFilter = useListGridFilter({ selectFields: ['status'] })\n const mailboxLabel = (sequence: OutreachSequence) => {\n const mailbox = props.mailboxes.find(\n (entry) => entry.id === sequence.mailboxId,\n )\n return mailbox\n ? mailbox.sendAs || mailbox.email\n : sequence.mailboxId\n ? 'Mailbox removed'\n : 'No mailbox yet'\n }\n /*\n * Filtered BEFORE the page is cut: the list read every sequence (under\n * `OUTREACH_SEQUENCES_LIMIT`), so a clause or a search word answers over\n * all of them, and the footer counts the matches.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const matching = useMemo(\n () =>\n filterListRows(\n sequences.data.map((sequence) => ({\n sequence,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n })),\n SEQUENCE_FILTER_FIELDS,\n gridFilter.clauses,\n { paths: SEQUENCE_SEARCH_FIELDS, words: gridFilter.searchWords },\n ).map((row) => row.sequence),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [sequences.data, props.mailboxes, gridFilter.clauses, searchKey],\n )\n // A narrowed list starts again at its first page.\n useEffect(() => setPage(0), [gridFilter.clauses, searchKey])\n const pageRows = matching.slice(page * pageSize, (page + 1) * pageSize)\n // Counted for the page on screen only: five aggregations a sequence.\n const counts = useOutreachSequenceCounts(\n orgId,\n pageRows.map((sequence) => sequence.id),\n )\n const footer = (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={matching.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )\n const newHref = `${sectionPath}/${OUTREACH_NEW_SEQUENCE_SEGMENT}`\n const newButton = (\n <Button\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n href={newHref}\n onClick={(event) => {\n event.preventDefault()\n navigate(newHref)\n }}\n >\n New sequence\n </Button>\n )\n const count = (\n sequence: OutreachSequence,\n key: keyof OutreachSequenceCounts,\n ) => (counts[sequence.id] ? String(counts[sequence.id][key]) : '—')\n\n /*\n * The read stops at `OUTREACH_SEQUENCES_LIMIT`, newest first, and probes one\n * past it, so this shows only when older sequences really exist.\n */\n const truncatedNotice = sequences.truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${sequences.data.length.toLocaleString()} newest sequences. ` +\n 'The filters and the search narrow these only; older sequences are ' +\n 'not listed here.'}\n </Alert>\n ) : null\n\n const chips = (\n <ListFilterChips\n fields={SEQUENCE_FILTER_FIELDS}\n headers={SEQUENCE_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={SEQUENCE_FILTER_OPTIONS}\n />\n )\n let body\n if (sequences.status === 'loading')\n body = <OutreachLoading label=\"Loading sequences…\" />\n else if (sequences.status === 'error' || sequences.status === 'refused') {\n body = <OutreachLoadProblem status={sequences.status} what=\"sequences\" />\n } else if (!sequences.data.length) {\n body = (\n <Card variant=\"outlined\">\n <EmptyStateComponent\n label=\"No sequences yet\"\n description=\"A sequence is the emails, and the tasks between them, one person gets from one rep.\"\n action={newButton}\n />\n </Card>\n )\n } else if (narrow) {\n body = (\n <Stack spacing={1}>\n {chips}\n <Stack\n spacing={1}\n component=\"ul\"\n sx={{ listStyle: 'none', p: 0, m: 0 }}\n aria-label=\"Sequences\"\n >\n {pageRows.map((sequence) => (\n <Card key={sequence.id} variant=\"outlined\" component=\"li\">\n <CardContent>\n <Stack spacing={1}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <OutreachLink href={`${sectionPath}/${sequence.id}`}>\n {sequence.name || 'Untitled'}\n </OutreachLink>\n <OutreachSequenceStatusChip status={sequence.status} />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mailboxLabel(sequence)}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {COUNT_COLUMNS.map(([key, label]) => (\n <Chip\n key={key}\n size=\"small\"\n variant=\"outlined\"\n label={`${label} ${count(sequence, key)}`}\n />\n ))}\n </Stack>\n </Stack>\n </CardContent>\n </Card>\n ))}\n </Stack>\n {footer}\n {truncatedNotice}\n </Stack>\n )\n } else {\n const columns: NonNullable<ListTableProps['columns']> = listFilterGridColumns([\n { field: 'name', headerName: 'Name', flex: 1, minWidth: 180 },\n { field: 'mailbox', headerName: 'Mailbox', flex: 1, minWidth: 180 },\n {\n field: 'status',\n headerName: 'Status',\n width: 110,\n renderCell: ({ row }) => (\n <OutreachSequenceStatusChip status={row.status} />\n ),\n },\n ...COUNT_COLUMNS.map(([key, label]) => ({\n field: key,\n headerName: label,\n width: 96,\n align: 'right' as const,\n headerAlign: 'right' as const,\n })),\n ], SEQUENCE_FILTER_FIELDS, SEQUENCE_FILTER_OPTIONS, SEQUENCE_FILTER_HEADERS)\n body = (\n <Stack spacing={1}>\n {chips}\n <ListTable\n aria-label=\"Sequences\"\n columns={columns}\n rows={pageRows.map((sequence) => ({\n $id: sequence.id,\n name: sequence.name || 'Untitled',\n mailbox: mailboxLabel(sequence),\n status: sequence.status,\n ...Object.fromEntries(\n COUNT_COLUMNS.map(([key]) => [key, count(sequence, key)]),\n ),\n }))}\n onOpen={(id) => navigate(`${sectionPath}/${id}`)}\n /*\n * The panel and the search are the grid's; the list answers them\n * over every sequence it read (AGL-3317).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel=\"No sequences match these filters\"\n hideFooter\n />\n {footer}\n {truncatedNotice}\n </Stack>\n )\n }\n\n return (\n <CardDisplay\n header=\"Sequences\"\n help={pluginDocsHelp('sequences', { anchor: '#sequences' })}\n HeaderProps={{\n action:\n sequences.status === 'ready' && sequences.data.length\n ? newButton\n : undefined,\n }}\n contentGutterX\n contentGutterY\n >\n {body}\n </CardDisplay>\n )\n}\nOutreachSequenceList.displayName = 'OutreachSequenceList'\n\nexport default OutreachSequencesSection\n"],"names":["pluginDocsHelp","mdiPlus","CardDisplay","MdiIcon","EmptyStateComponent","ListFilterChips","ListPagination","ListTable","filterListRows","inMemoryListField","listFilterGridColumns","TABLE_PAGE_SIZE_DEFAULT","useListGridFilter","Alert","Button","Card","CardContent","Chip","Stack","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","OUTREACH_SEQUENCE_STATUSES","OUTREACH_SEQUENCE_STATUS_LABELS","OutreachLink","OutreachLoading","OutreachLoadProblem","OutreachSequenceStatusChip","useOutreachNavigate","OutreachSequenceDetail","OutreachSequenceEditor","useOutreachApi","useOutreachSequenceCounts","useOutreachSequences","useOutreachMailboxes","useOutreachComplianceSettings","OUTREACH_NEW_SEQUENCE_SEGMENT","OutreachSequencesSection","props","orgId","sectionPath","subpath","api","settings","mailboxes","navigate","label","first","second","spacing","href","variant","component","orgMount","sequence","mailboxesPath","onSaved","id","onCancel","org","sequenceId","tab","compliancePath","OutreachSequenceList","displayName","COUNT_COLUMNS","SEQUENCE_FILTER_FIELDS","SEQUENCE_FILTER_HEADERS","name","mailbox","status","SEQUENCE_FILTER_OPTIONS","map","value","SEQUENCE_SEARCH_FIELDS","theme","narrow","breakpoints","down","sequences","page","setPage","pageSize","setPageSize","gridFilter","selectFields","mailboxLabel","find","entry","mailboxId","sendAs","email","searchKey","searchWords","join","matching","data","clauses","paths","words","row","pageRows","slice","counts","footer","rowCount","length","count","onPageChange","onPageSizeChange","newHref","newButton","startIcon","path","fontSize","onClick","event","preventDefault","key","String","truncatedNotice","truncated","severity","toLocaleString","chips","fields","headers","onChange","setClauses","options","body","what","description","action","sx","listStyle","p","m","aria-label","direction","alignItems","justifyContent","color","flexWrap","rowGap","size","columns","field","headerName","flex","minWidth","width","renderCell","align","headerAlign","rows","$id","Object","fromEntries","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","hideFooter","header","help","anchor","HeaderProps","undefined","contentGutterX","contentGutterY"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,cAAc,QAAoC,eAAc;AACzE,SAASC,OAAO,QAAQ,yBAAwB;AAChD,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,OAAOC,yBAAyB,wDAAuD;AACvF,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,SAAS,QAEJ,uDAAsD;AAC7D,SACEC,cAAc,EACdC,iBAAiB,EACjBC,qBAAqB,QAChB,8CAA6C;AACpD,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,WAAW,EACXC,IAAI,EACJC,KAAK,EACLC,UAAU,EACVC,aAAa,EACbC,QAAQ,QACH,gBAAe;AACtB,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SACEC,0BAA0B,QAGrB,6BAAyB;AAChC,SACEC,+BAA+B,EAC/BC,YAAY,EACZC,eAAe,EACfC,mBAAmB,EACnBC,0BAA0B,EAC1BC,mBAAmB,QACd,mBAAe;AACtB,SACEC,sBAAsB,QAEjB,uBAAmB;AAC1B,SAASC,sBAAsB,QAAQ,uBAAmB;AAC1D,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAEEC,yBAAyB,EACzBC,oBAAoB,QACf,yBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,8BAA0B;AAC/D,SAASC,6BAA6B,QAAQ,6BAAyB;AAiBvE,uDAAuD,GACvD,OAAO,MAAMC,gCAAgC,MAAK;AAElD;;;;CAIC,GACD,OAAO,SAASC,yBAAyBC,KAAoC;IAC3E,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAEC,OAAO,EAAE,GAAGH;IACxC,MAAMI,MAAMX,eAAeQ;IAC3B,MAAMI,WAAWR,8BAA8BO,KAAKH;IACpD,MAAMK,YAAYV,qBAAqBK;IACvC,MAAMM,WAAWjB;IAEjB,IAAI,CAACW,OAAO,qBAAO,KAACd;QAAgBqB,OAAM;;IAC1C,MAAM,CAACC,OAAOC,OAAO,GAAGP;IACxB,IAAIM,UAAUX,+BAA+B;QAC3C,qBACE,MAACrB;YAAMkC,SAAS;;8BACd,KAACzB;oBAAa0B,MAAMV;8BAAa;;8BACjC,KAACxB;oBAAWmC,SAAQ;oBAAKC,WAAU;8BAAK;;8BAGxC,KAACtB;oBACCS,OAAOA;oBACPc,UAAUf,MAAMe,QAAQ;oBACxBC,UAAU;oBACVX,UAAUA;oBACVC,WAAWA;oBACXW,eAAejB,MAAMiB,aAAa;oBAClCC,SAAS,CAACF,WAAaT,SAAS,GAAGL,YAAY,CAAC,EAAEc,SAASG,EAAE,EAAE;oBAC/DC,UAAU,IAAMb,SAASL;;;;IAIjC;IACA,IAAIO,OAAO;QACT,qBACE,KAAClB;YACCU,OAAOA;YACPc,UAAUf,MAAMe,QAAQ;YACxBM,KAAKrB,MAAMqB,GAAG;YACdnB,aAAaA;YACboB,YAAYb;YACZc,KACGb,WAAW,gBACR,gBACA;YAENL,UAAUA;YACVC,WAAWA;YACXW,eAAejB,MAAMiB,aAAa;YAClCO,gBAAgBxB,MAAMwB,cAAc;;IAG1C;IACA,qBACE,KAACC;QACCxB,OAAOA;QACPC,aAAaA;QACbI,WAAWA,UAAUA,SAAS;;AAGpC;AACAP,yBAAyB2B,WAAW,GAAG;AAEvC,MAAMC,gBAAuE;IAC3E;QAAC;QAAY;KAAW;IACxB;QAAC;QAAU;KAAS;IACpB;QAAC;QAAW;KAAU;IACtB;QAAC;QAAW;KAAU;IACtB;QAAC;QAAY;KAAY;CAC1B;AAED;;;;CAIC,GACD,MAAMC,yBAAyB;IAC7B5D,kBAAkB,QAAQ;IAC1BA,kBAAkB,WAAW;IAC7BA,kBAAkB,UAAU;CAC7B;AACD,MAAM6D,0BAA4D;IAChEC,MAAM;IACNC,SAAS;IACTC,QAAQ;AACV;AACA,MAAMC,0BAA0B;IAC9BD,QAAQhD,2BAA2BkD,GAAG,CAAC,CAACF,SAAY,CAAA;YAClDG,OAAOH;YACPxB,OAAOvB,+BAA+B,CAAC+C,OAAO;QAChD,CAAA;AACF;AACA,mDAAmD,GACnD,MAAMI,yBAAyB;IAAC;IAAQ;CAAU;AAElD;;;CAGC,GACD,OAAO,SAASX,qBAAqBzB,KAIpC;IACC,MAAM,EAAEC,KAAK,EAAEC,WAAW,EAAE,GAAGF;IAC/B,MAAMqC,QAAQzD;IACd,MAAM0D,SAAS3D,cAAc0D,MAAME,WAAW,CAACC,IAAI,CAAC;IACpD,MAAMjC,WAAWjB;IACjB,MAAMmD,YAAY9C,qBAAqBM;IACvC,MAAM,CAACyC,MAAMC,QAAQ,GAAG5D,SAAS;IACjC,MAAM,CAAC6D,UAAUC,YAAY,GAAG9D,SAASb;IACzC,MAAM4E,aAAa3E,kBAAkB;QAAE4E,cAAc;YAAC;SAAS;IAAC;IAChE,MAAMC,eAAe,CAAChC;QACpB,MAAMe,UAAU/B,MAAMM,SAAS,CAAC2C,IAAI,CAClC,CAACC,QAAUA,MAAM/B,EAAE,KAAKH,SAASmC,SAAS;QAE5C,OAAOpB,UACHA,QAAQqB,MAAM,IAAIrB,QAAQsB,KAAK,GAC/BrC,SAASmC,SAAS,GAChB,oBACA;IACR;IACA;;;;GAIC,GACD,MAAMG,YAAYR,WAAWS,WAAW,CAACC,IAAI,CAAC;IAC9C,MAAMC,WAAW3E,QACf,IACEf,eACE0E,UAAUiB,IAAI,CAACxB,GAAG,CAAC,CAAClB,WAAc,CAAA;gBAChCA;gBACAc,MAAMd,SAASc,IAAI,IAAI;gBACvBC,SAASiB,aAAahC;gBACtBgB,QAAQhB,SAASgB,MAAM;YACzB,CAAA,IACAJ,wBACAkB,WAAWa,OAAO,EAClB;YAAEC,OAAOxB;YAAwByB,OAAOf,WAAWS,WAAW;QAAC,GAC/DrB,GAAG,CAAC,CAAC4B,MAAQA,IAAI9C,QAAQ,GAC7B,uEAAuE;IACvE,uDAAuD;IACvD;QAACyB,UAAUiB,IAAI;QAAE1D,MAAMM,SAAS;QAAEwC,WAAWa,OAAO;QAAEL;KAAU;IAElE,kDAAkD;IAClDzE,UAAU,IAAM8D,QAAQ,IAAI;QAACG,WAAWa,OAAO;QAAEL;KAAU;IAC3D,MAAMS,WAAWN,SAASO,KAAK,CAACtB,OAAOE,UAAU,AAACF,CAAAA,OAAO,CAAA,IAAKE;IAC9D,qEAAqE;IACrE,MAAMqB,SAASvE,0BACbO,OACA8D,SAAS7B,GAAG,CAAC,CAAClB,WAAaA,SAASG,EAAE;IAExC,MAAM+C,uBACJ,KAACrG;QACC6E,MAAMA;QACNE,UAAUA;QACVuB,UAAUJ,SAASK,MAAM;QACzBC,OAAOZ,SAASW,MAAM;QACtBE,cAAc3B;QACd4B,kBAAkB1B;;IAGtB,MAAM2B,UAAU,GAAGtE,YAAY,CAAC,EAAEJ,+BAA+B;IACjE,MAAM2E,0BACJ,KAACpG;QACCwC,SAAQ;QACR6D,yBAAW,KAAChH;YAAQiH,MAAMnH,QAAQmH,IAAI;YAAEC,UAAS;;QACjDhE,MAAM4D;QACNK,SAAS,CAACC;YACRA,MAAMC,cAAc;YACpBxE,SAASiE;QACX;kBACD;;IAIH,MAAMH,QAAQ,CACZrD,UACAgE,MACIf,MAAM,CAACjD,SAASG,EAAE,CAAC,GAAG8D,OAAOhB,MAAM,CAACjD,SAASG,EAAE,CAAC,CAAC6D,IAAI,IAAI;IAE/D;;;GAGC,GACD,MAAME,kBAAkBzC,UAAU0C,SAAS,iBACzC,KAAC/G;QAAMgH,UAAS;kBACb,CAAC,YAAY,EAAE3C,UAAUiB,IAAI,CAACU,MAAM,CAACiB,cAAc,GAAG,mBAAmB,CAAC,GACzE,uEACA;SAEF;IAEJ,MAAMC,sBACJ,KAAC1H;QACC2H,QAAQ3D;QACR4D,SAAS3D;QACT8B,SAASb,WAAWa,OAAO;QAC3B8B,UAAU3C,WAAW4C,UAAU;QAC/BC,SAAS1D;;IAGb,IAAI2D;IACJ,IAAInD,UAAUT,MAAM,KAAK,WACvB4D,qBAAO,KAACzG;QAAgBqB,OAAM;;SAC3B,IAAIiC,UAAUT,MAAM,KAAK,WAAWS,UAAUT,MAAM,KAAK,WAAW;QACvE4D,qBAAO,KAACxG;YAAoB4C,QAAQS,UAAUT,MAAM;YAAE6D,MAAK;;IAC7D,OAAO,IAAI,CAACpD,UAAUiB,IAAI,CAACU,MAAM,EAAE;QACjCwB,qBACE,KAACtH;YAAKuC,SAAQ;sBACZ,cAAA,KAAClD;gBACC6C,OAAM;gBACNsF,aAAY;gBACZC,QAAQtB;;;IAIhB,OAAO,IAAInC,QAAQ;QACjBsD,qBACE,MAACnH;YAAMkC,SAAS;;gBACb2E;8BACD,KAAC7G;oBACCkC,SAAS;oBACTG,WAAU;oBACVkF,IAAI;wBAAEC,WAAW;wBAAQC,GAAG;wBAAGC,GAAG;oBAAE;oBACpCC,cAAW;8BAEVrC,SAAS7B,GAAG,CAAC,CAAClB,yBACb,KAAC1C;4BAAuBuC,SAAQ;4BAAWC,WAAU;sCACnD,cAAA,KAACvC;0CACC,cAAA,MAACE;oCAAMkC,SAAS;;sDACd,MAAClC;4CACC4H,WAAU;4CACV1F,SAAS;4CACTqF,IAAI;gDACFM,YAAY;gDACZC,gBAAgB;4CAClB;;8DAEA,KAACrH;oDAAa0B,MAAM,GAAGV,YAAY,CAAC,EAAEc,SAASG,EAAE,EAAE;8DAChDH,SAASc,IAAI,IAAI;;8DAEpB,KAACzC;oDAA2B2C,QAAQhB,SAASgB,MAAM;;;;sDAErD,KAACtD;4CAAWmC,SAAQ;4CAAQ2F,OAAM;sDAC/BxD,aAAahC;;sDAEhB,KAACvC;4CACC4H,WAAU;4CACV1F,SAAS;4CACTqF,IAAI;gDAAES,UAAU;gDAAQC,QAAQ;4CAAI;sDAEnC/E,cAAcO,GAAG,CAAC,CAAC,CAAC8C,KAAKxE,MAAM,iBAC9B,KAAChC;oDAECmI,MAAK;oDACL9F,SAAQ;oDACRL,OAAO,GAAGA,MAAM,CAAC,EAAE6D,MAAMrD,UAAUgE,MAAM;mDAHpCA;;;;;2BA1BNhE,SAASG,EAAE;;gBAsCzB+C;gBACAgB;;;IAGP,OAAO;QACL,MAAM0B,UAAkD3I,sBAAsB;YAC5E;gBAAE4I,OAAO;gBAAQC,YAAY;gBAAQC,MAAM;gBAAGC,UAAU;YAAI;YAC5D;gBAAEH,OAAO;gBAAWC,YAAY;gBAAWC,MAAM;gBAAGC,UAAU;YAAI;YAClE;gBACEH,OAAO;gBACPC,YAAY;gBACZG,OAAO;gBACPC,YAAY,CAAC,EAAEpD,GAAG,EAAE,iBAClB,KAACzE;wBAA2B2C,QAAQ8B,IAAI9B,MAAM;;YAElD;eACGL,cAAcO,GAAG,CAAC,CAAC,CAAC8C,KAAKxE,MAAM,GAAM,CAAA;oBACtCqG,OAAO7B;oBACP8B,YAAYtG;oBACZyG,OAAO;oBACPE,OAAO;oBACPC,aAAa;gBACf,CAAA;SACD,EAAExF,wBAAwBK,yBAAyBJ;QACpD+D,qBACE,MAACnH;YAAMkC,SAAS;;gBACb2E;8BACD,KAACxH;oBACCsI,cAAW;oBACXQ,SAASA;oBACTS,MAAMtD,SAAS7B,GAAG,CAAC,CAAClB,WAAc;4BAChCsG,KAAKtG,SAASG,EAAE;4BAChBW,MAAMd,SAASc,IAAI,IAAI;4BACvBC,SAASiB,aAAahC;4BACtBgB,QAAQhB,SAASgB,MAAM;2BACpBuF,OAAOC,WAAW,CACnB7F,cAAcO,GAAG,CAAC,CAAC,CAAC8C,IAAI,GAAK;gCAACA;gCAAKX,MAAMrD,UAAUgE;6BAAK;oBAG5DyC,QAAQ,CAACtG,KAAOZ,SAAS,GAAGL,YAAY,CAAC,EAAEiB,IAAI;oBAC/C;;;WAGC,GACDuG,YAAW;oBACXC,aAAa7E,WAAW6E,WAAW;oBACnCC,qBAAqB9E,WAAW8E,mBAAmB;oBACnDC,WAAW;oBACXC,aAAY;oBACZC,UAAU;;gBAEX7D;gBACAgB;;;IAGP;IAEA,qBACE,KAACzH;QACCuK,QAAO;QACPC,MAAM1K,eAAe,aAAa;YAAE2K,QAAQ;QAAa;QACzDC,aAAa;YACXpC,QACEtD,UAAUT,MAAM,KAAK,WAAWS,UAAUiB,IAAI,CAACU,MAAM,GACjDK,YACA2D;QACR;QACAC,cAAc;QACdC,cAAc;kBAEb1C;;AAGP;AACAnE,qBAAqBC,WAAW,GAAG;AAEnC,eAAe3B,yBAAwB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type OutreachSequence } from '../model/outreach.types';
|
|
2
|
+
import type { OutreachApi } from './use-outreach-api';
|
|
3
|
+
export interface OutreachStepTestDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose(): void;
|
|
6
|
+
orgId: string;
|
|
7
|
+
/** The stored sequence: the test is sent from what is saved. */
|
|
8
|
+
sequence: OutreachSequence;
|
|
9
|
+
stepIndex: number;
|
|
10
|
+
/** The consent group of the sequence's site: whose names a contact search shows. */
|
|
11
|
+
contactGroupId: string;
|
|
12
|
+
/** The member's own address, where the test goes unless they type another. */
|
|
13
|
+
selfEmail: string | null;
|
|
14
|
+
/** True while the editor holds edits the stored sequence does not. */
|
|
15
|
+
unsaved: boolean;
|
|
16
|
+
api: OutreachApi;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Send a test of one step (AGL-3325): the step as the sequence stores it,
|
|
20
|
+
* rendered for a chosen person — the sample person the preview uses, or an
|
|
21
|
+
* open lead or contact of the sequence's site — with the personal line the
|
|
22
|
+
* member types, sent through the sequence's mailbox to the member's own
|
|
23
|
+
* address or one they name. The route prefixes the subject with `[Test]`,
|
|
24
|
+
* counts it on nothing but the mailbox's test tally, and never enrolls the
|
|
25
|
+
* person whose record filled the tokens.
|
|
26
|
+
*/
|
|
27
|
+
export declare function OutreachStepTestDialog(props: OutreachStepTestDialogProps): import("react").JSX.Element;
|
|
28
|
+
export declare namespace OutreachStepTestDialog {
|
|
29
|
+
var displayName: string;
|
|
30
|
+
}
|
|
31
|
+
export default OutreachStepTestDialog;
|
|
@@ -0,0 +1,247 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { createElement as _createElement } from "react";
|
|
20
|
+
import { crmMergeUnresolvedMessage } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
21
|
+
import { Alert, Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
|
|
22
|
+
import { useEffect, useState } from "react";
|
|
23
|
+
import { isInThreadEmailStep } from "../engine/sequence-validation.js";
|
|
24
|
+
import { OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
|
|
25
|
+
import { OUTREACH_SAMPLE_PERSON } from "../model/step-preview.js";
|
|
26
|
+
import { useOutreachContactSearch, useOutreachLeadSearch } from "./use-outreach-crm.js";
|
|
27
|
+
const SAMPLE = {
|
|
28
|
+
kind: 'sample',
|
|
29
|
+
id: '',
|
|
30
|
+
name: `${OUTREACH_SAMPLE_PERSON.name} (sample)`,
|
|
31
|
+
email: OUTREACH_SAMPLE_PERSON.email
|
|
32
|
+
};
|
|
33
|
+
const GROUP_LABELS = {
|
|
34
|
+
sample: 'Sample',
|
|
35
|
+
lead: 'Leads',
|
|
36
|
+
contact: 'Contacts'
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Send a test of one step (AGL-3325): the step as the sequence stores it,
|
|
40
|
+
* rendered for a chosen person — the sample person the preview uses, or an
|
|
41
|
+
* open lead or contact of the sequence's site — with the personal line the
|
|
42
|
+
* member types, sent through the sequence's mailbox to the member's own
|
|
43
|
+
* address or one they name. The route prefixes the subject with `[Test]`,
|
|
44
|
+
* counts it on nothing but the mailbox's test tally, and never enrolls the
|
|
45
|
+
* person whose record filled the tokens.
|
|
46
|
+
*/ export function OutreachStepTestDialog(props) {
|
|
47
|
+
var _outcome_answer_unresolvedFields;
|
|
48
|
+
const { open, orgId, sequence, stepIndex, api } = props;
|
|
49
|
+
const [person, setPerson] = useState(SAMPLE);
|
|
50
|
+
const [text, setText] = useState('');
|
|
51
|
+
const [personalLine, setPersonalLine] = useState(OUTREACH_SAMPLE_PERSON.personalLine);
|
|
52
|
+
const [to, setTo] = useState('');
|
|
53
|
+
const [outcome, setOutcome] = useState({
|
|
54
|
+
status: 'idle'
|
|
55
|
+
});
|
|
56
|
+
// A fresh dialog for each step: the last test's answer belongs to it.
|
|
57
|
+
useEffect(()=>{
|
|
58
|
+
if (open) setOutcome({
|
|
59
|
+
status: 'idle'
|
|
60
|
+
});
|
|
61
|
+
}, [
|
|
62
|
+
open,
|
|
63
|
+
stepIndex
|
|
64
|
+
]);
|
|
65
|
+
const leads = useOutreachLeadSearch({
|
|
66
|
+
orgId,
|
|
67
|
+
hostId: sequence.hostId,
|
|
68
|
+
text,
|
|
69
|
+
enabled: open
|
|
70
|
+
});
|
|
71
|
+
const contacts = useOutreachContactSearch({
|
|
72
|
+
orgId: open ? orgId : null,
|
|
73
|
+
hostId: sequence.hostId,
|
|
74
|
+
contactGroupId: props.contactGroupId,
|
|
75
|
+
text
|
|
76
|
+
});
|
|
77
|
+
const options = [
|
|
78
|
+
SAMPLE,
|
|
79
|
+
...leads.data.map((lead)=>_extends({}, lead, {
|
|
80
|
+
kind: 'lead'
|
|
81
|
+
})),
|
|
82
|
+
...contacts.data.map((contact)=>_extends({}, contact, {
|
|
83
|
+
kind: 'contact'
|
|
84
|
+
}))
|
|
85
|
+
];
|
|
86
|
+
const choose = (next)=>{
|
|
87
|
+
const chosen = next != null ? next : SAMPLE;
|
|
88
|
+
setPerson(chosen);
|
|
89
|
+
// The sample person comes with a line; a real person waits for one.
|
|
90
|
+
setPersonalLine(chosen.kind === 'sample' ? OUTREACH_SAMPLE_PERSON.personalLine : '');
|
|
91
|
+
};
|
|
92
|
+
const step = sequence.steps[stepIndex];
|
|
93
|
+
const inThread = isInThreadEmailStep(sequence.steps, stepIndex);
|
|
94
|
+
const line = personalLine.trim();
|
|
95
|
+
const tooLong = line.length > OUTREACH_PERSONAL_LINE_MAX;
|
|
96
|
+
const sending = outcome.status === 'sending';
|
|
97
|
+
const send = async ()=>{
|
|
98
|
+
setOutcome({
|
|
99
|
+
status: 'sending'
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const answer = await api.sendStepTest(_extends({
|
|
103
|
+
sequenceId: sequence.id,
|
|
104
|
+
stepIndex
|
|
105
|
+
}, person.kind === 'lead' ? {
|
|
106
|
+
leadId: person.id
|
|
107
|
+
} : person.kind === 'contact' ? {
|
|
108
|
+
contactId: person.id
|
|
109
|
+
} : {}, {
|
|
110
|
+
personalLine: line
|
|
111
|
+
}, to.trim() ? {
|
|
112
|
+
to: to.trim()
|
|
113
|
+
} : {}));
|
|
114
|
+
setOutcome({
|
|
115
|
+
status: 'sent',
|
|
116
|
+
answer
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
setOutcome({
|
|
120
|
+
status: 'failed',
|
|
121
|
+
message: error.message
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const title = `Send a test of step ${stepIndex + 1}`;
|
|
126
|
+
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
127
|
+
open: open,
|
|
128
|
+
onClose: sending ? undefined : props.onClose,
|
|
129
|
+
fullWidth: true,
|
|
130
|
+
maxWidth: "sm",
|
|
131
|
+
"aria-label": title,
|
|
132
|
+
children: [
|
|
133
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
134
|
+
children: title
|
|
135
|
+
}),
|
|
136
|
+
/*#__PURE__*/ _jsx(DialogContent, {
|
|
137
|
+
dividers: true,
|
|
138
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
139
|
+
spacing: 2,
|
|
140
|
+
children: [
|
|
141
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
142
|
+
variant: "body2",
|
|
143
|
+
color: "text.secondary",
|
|
144
|
+
children: `The email goes through ${sequence.name}’s mailbox, exactly as a recipient would get it — the footer, the links, ` + (inThread ? 'the “Re:” subject of a reply — ' : '') + 'with “[Test]” in front of the subject. It counts toward nothing, and nobody is enrolled.'
|
|
145
|
+
}),
|
|
146
|
+
props.unsaved ? /*#__PURE__*/ _jsx(Alert, {
|
|
147
|
+
severity: "info",
|
|
148
|
+
children: "You have unsaved changes. The test sends the step as it was last saved."
|
|
149
|
+
}) : null,
|
|
150
|
+
(step == null ? void 0 : step.kind) !== 'email' ? /*#__PURE__*/ _jsx(Alert, {
|
|
151
|
+
severity: "warning",
|
|
152
|
+
children: "This step does not send an email."
|
|
153
|
+
}) : null,
|
|
154
|
+
/*#__PURE__*/ _jsx(Autocomplete, {
|
|
155
|
+
options: options,
|
|
156
|
+
value: person,
|
|
157
|
+
disableClearable: true,
|
|
158
|
+
inputValue: text,
|
|
159
|
+
onInputChange: (_event, next, reason)=>{
|
|
160
|
+
if (reason === 'input') setText(next);
|
|
161
|
+
else if (reason === 'clear') setText('');
|
|
162
|
+
},
|
|
163
|
+
onChange: (_event, next)=>choose(next),
|
|
164
|
+
// The search is the CRM's own; nothing is filtered again here.
|
|
165
|
+
filterOptions: (list)=>list,
|
|
166
|
+
groupBy: (option)=>GROUP_LABELS[option.kind],
|
|
167
|
+
getOptionLabel: (option)=>option.name || option.email || option.id,
|
|
168
|
+
isOptionEqualToValue: (option, value)=>option.kind === value.kind && option.id === value.id,
|
|
169
|
+
loading: leads.status === 'loading' || contacts.status === 'loading',
|
|
170
|
+
disabled: sending,
|
|
171
|
+
renderOption: (itemProps, option)=>/*#__PURE__*/ _createElement("li", _extends({}, itemProps, {
|
|
172
|
+
key: `${option.kind}:${option.id}`
|
|
173
|
+
}), /*#__PURE__*/ _jsxs(Stack, {
|
|
174
|
+
children: [
|
|
175
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
176
|
+
variant: "body2",
|
|
177
|
+
children: option.name || option.email || option.id
|
|
178
|
+
}),
|
|
179
|
+
option.email && option.email !== option.name ? /*#__PURE__*/ _jsx(Typography, {
|
|
180
|
+
variant: "caption",
|
|
181
|
+
color: "text.secondary",
|
|
182
|
+
children: option.email
|
|
183
|
+
}) : null
|
|
184
|
+
]
|
|
185
|
+
})),
|
|
186
|
+
renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
|
|
187
|
+
label: "Fill the tokens from",
|
|
188
|
+
helperText: "A sample person, or type a name or address to pick an open lead or contact of this sequence’s site. Their record fills the merge fields; the test still goes to you."
|
|
189
|
+
}))
|
|
190
|
+
}),
|
|
191
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
192
|
+
label: "Personal line",
|
|
193
|
+
placeholder: "One sentence on why you're writing to this person now.",
|
|
194
|
+
value: personalLine,
|
|
195
|
+
onChange: (event)=>setPersonalLine(event.target.value),
|
|
196
|
+
disabled: sending,
|
|
197
|
+
error: tooLong,
|
|
198
|
+
helperText: tooLong ? `Keep it to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.` : 'Goes where the emails say {{enrollment.personalLine}}.',
|
|
199
|
+
fullWidth: true,
|
|
200
|
+
size: "small"
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
203
|
+
label: "Send to",
|
|
204
|
+
type: "email",
|
|
205
|
+
value: to,
|
|
206
|
+
onChange: (event)=>setTo(event.target.value),
|
|
207
|
+
disabled: sending,
|
|
208
|
+
helperText: props.selfEmail ? `Leave empty to send it to yourself (${props.selfEmail}).` : 'Leave empty to send it to your own address.',
|
|
209
|
+
fullWidth: true,
|
|
210
|
+
size: "small"
|
|
211
|
+
}),
|
|
212
|
+
outcome.status === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
|
|
213
|
+
severity: "error",
|
|
214
|
+
children: outcome.message
|
|
215
|
+
}) : null,
|
|
216
|
+
outcome.status === 'sent' ? /*#__PURE__*/ _jsxs(Alert, {
|
|
217
|
+
severity: "success",
|
|
218
|
+
children: [
|
|
219
|
+
`Sent to ${outcome.answer.sentTo} as “${outcome.answer.subject}”. Check that inbox.`,
|
|
220
|
+
((_outcome_answer_unresolvedFields = outcome.answer.unresolvedFields) == null ? void 0 : _outcome_answer_unresolvedFields.length) ? ` ${crmMergeUnresolvedMessage(outcome.answer.unresolvedFields)}; they were sent empty.` : ''
|
|
221
|
+
]
|
|
222
|
+
}) : null
|
|
223
|
+
]
|
|
224
|
+
})
|
|
225
|
+
}),
|
|
226
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
227
|
+
children: [
|
|
228
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
229
|
+
onClick: props.onClose,
|
|
230
|
+
disabled: sending,
|
|
231
|
+
children: outcome.status === 'sent' ? 'Done' : 'Cancel'
|
|
232
|
+
}),
|
|
233
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
234
|
+
variant: "contained",
|
|
235
|
+
onClick: ()=>void send(),
|
|
236
|
+
disabled: sending || tooLong || (step == null ? void 0 : step.kind) !== 'email',
|
|
237
|
+
children: sending ? 'Sending…' : outcome.status === 'sent' ? 'Send again' : 'Send the test'
|
|
238
|
+
})
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
OutreachStepTestDialog.displayName = 'OutreachStepTestDialog';
|
|
245
|
+
export default OutreachStepTestDialog;
|
|
246
|
+
|
|
247
|
+
//# sourceMappingURL=step-test-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/step-test-dialog.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 { crmMergeUnresolvedMessage } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport {\n Alert,\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { isInThreadEmailStep } from '../engine/sequence-validation'\nimport type { OutreachStepTestResponse } from '../model/outreach-api'\nimport { OUTREACH_PERSONAL_LINE_MAX, type OutreachSequence } from '../model/outreach.types'\nimport { OUTREACH_SAMPLE_PERSON } from '../model/step-preview'\nimport type { OutreachApi } from './use-outreach-api'\nimport {\n type OutreachContactOption,\n useOutreachContactSearch,\n useOutreachLeadSearch,\n} from './use-outreach-crm'\n\n/** Whose record fills the tokens: the sample person, or an open lead or contact. */\ninterface PersonChoice extends OutreachContactOption {\n kind: 'sample' | 'lead' | 'contact'\n}\n\nconst SAMPLE: PersonChoice = {\n kind: 'sample',\n id: '',\n name: `${OUTREACH_SAMPLE_PERSON.name} (sample)`,\n email: OUTREACH_SAMPLE_PERSON.email,\n}\n\nconst GROUP_LABELS: Record<PersonChoice['kind'], string> = {\n sample: 'Sample',\n lead: 'Leads',\n contact: 'Contacts',\n}\n\ntype Outcome =\n | { status: 'idle' }\n | { status: 'sending' }\n | { status: 'sent'; answer: OutreachStepTestResponse & { unresolvedFields?: string[] } }\n | { status: 'failed'; message: string }\n\nexport interface OutreachStepTestDialogProps {\n open: boolean\n onClose(): void\n orgId: string\n /** The stored sequence: the test is sent from what is saved. */\n sequence: OutreachSequence\n stepIndex: number\n /** The consent group of the sequence's site: whose names a contact search shows. */\n contactGroupId: string\n /** The member's own address, where the test goes unless they type another. */\n selfEmail: string | null\n /** True while the editor holds edits the stored sequence does not. */\n unsaved: boolean\n api: OutreachApi\n}\n\n/**\n * Send a test of one step (AGL-3325): the step as the sequence stores it,\n * rendered for a chosen person — the sample person the preview uses, or an\n * open lead or contact of the sequence's site — with the personal line the\n * member types, sent through the sequence's mailbox to the member's own\n * address or one they name. The route prefixes the subject with `[Test]`,\n * counts it on nothing but the mailbox's test tally, and never enrolls the\n * person whose record filled the tokens.\n */\nexport function OutreachStepTestDialog(props: OutreachStepTestDialogProps) {\n const { open, orgId, sequence, stepIndex, api } = props\n const [person, setPerson] = useState<PersonChoice>(SAMPLE)\n const [text, setText] = useState('')\n const [personalLine, setPersonalLine] = useState<string>(OUTREACH_SAMPLE_PERSON.personalLine)\n const [to, setTo] = useState('')\n const [outcome, setOutcome] = useState<Outcome>({ status: 'idle' })\n\n // A fresh dialog for each step: the last test's answer belongs to it.\n useEffect(() => {\n if (open) setOutcome({ status: 'idle' })\n }, [open, stepIndex])\n\n const leads = useOutreachLeadSearch({\n orgId,\n hostId: sequence.hostId,\n text,\n enabled: open,\n })\n const contacts = useOutreachContactSearch({\n orgId: open ? orgId : null,\n hostId: sequence.hostId,\n contactGroupId: props.contactGroupId,\n text,\n })\n const options: PersonChoice[] = [\n SAMPLE,\n ...leads.data.map((lead) => ({ ...lead, kind: 'lead' as const })),\n ...contacts.data.map((contact) => ({ ...contact, kind: 'contact' as const })),\n ]\n const choose = (next: PersonChoice | null) => {\n const chosen = next ?? SAMPLE\n setPerson(chosen)\n // The sample person comes with a line; a real person waits for one.\n setPersonalLine(chosen.kind === 'sample' ? OUTREACH_SAMPLE_PERSON.personalLine : '')\n }\n\n const step = sequence.steps[stepIndex]\n const inThread = isInThreadEmailStep(sequence.steps, stepIndex)\n const line = personalLine.trim()\n const tooLong = line.length > OUTREACH_PERSONAL_LINE_MAX\n const sending = outcome.status === 'sending'\n\n const send = async () => {\n setOutcome({ status: 'sending' })\n try {\n const answer = await api.sendStepTest({\n sequenceId: sequence.id,\n stepIndex,\n ...(person.kind === 'lead'\n ? { leadId: person.id }\n : person.kind === 'contact'\n ? { contactId: person.id }\n : {}),\n personalLine: line,\n ...(to.trim() ? { to: to.trim() } : {}),\n })\n setOutcome({ status: 'sent', answer })\n } catch (error) {\n setOutcome({ status: 'failed', message: (error as Error).message })\n }\n }\n\n const title = `Send a test of step ${stepIndex + 1}`\n return (\n <Dialog open={open} onClose={sending ? undefined : props.onClose} fullWidth maxWidth=\"sm\" aria-label={title}>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`The email goes through ${sequence.name}’s mailbox, exactly as a recipient would get it — the footer, the links, ` +\n (inThread ? 'the “Re:” subject of a reply — ' : '') +\n 'with “[Test]” in front of the subject. It counts toward nothing, and nobody is enrolled.'}\n </Typography>\n {props.unsaved ? (\n <Alert severity=\"info\">You have unsaved changes. The test sends the step as it was last saved.</Alert>\n ) : null}\n {step?.kind !== 'email' ? (\n <Alert severity=\"warning\">This step does not send an email.</Alert>\n ) : null}\n <Autocomplete<PersonChoice, false, true>\n options={options}\n value={person}\n disableClearable\n inputValue={text}\n onInputChange={(_event, next, reason) => {\n if (reason === 'input') setText(next)\n else if (reason === 'clear') setText('')\n }}\n onChange={(_event, next) => choose(next)}\n // The search is the CRM's own; nothing is filtered again here.\n filterOptions={(list) => list}\n groupBy={(option) => GROUP_LABELS[option.kind]}\n getOptionLabel={(option) => option.name || option.email || option.id}\n isOptionEqualToValue={(option, value) => option.kind === value.kind && option.id === value.id}\n loading={leads.status === 'loading' || contacts.status === 'loading'}\n disabled={sending}\n renderOption={(itemProps, option) => (\n <li {...itemProps} key={`${option.kind}:${option.id}`}>\n <Stack>\n <Typography variant=\"body2\">{option.name || option.email || option.id}</Typography>\n {option.email && option.email !== option.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {option.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Fill the tokens from\"\n helperText=\"A sample person, or type a name or address to pick an open lead or contact of this sequence’s site. Their record fills the merge fields; the test still goes to you.\"\n />\n )}\n />\n <TextField\n label=\"Personal line\"\n placeholder=\"One sentence on why you're writing to this person now.\"\n value={personalLine}\n onChange={(event) => setPersonalLine(event.target.value)}\n disabled={sending}\n error={tooLong}\n helperText={\n tooLong\n ? `Keep it to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.`\n : 'Goes where the emails say {{enrollment.personalLine}}.'\n }\n fullWidth\n size=\"small\"\n />\n <TextField\n label=\"Send to\"\n type=\"email\"\n value={to}\n onChange={(event) => setTo(event.target.value)}\n disabled={sending}\n helperText={\n props.selfEmail\n ? `Leave empty to send it to yourself (${props.selfEmail}).`\n : 'Leave empty to send it to your own address.'\n }\n fullWidth\n size=\"small\"\n />\n {outcome.status === 'failed' ? <Alert severity=\"error\">{outcome.message}</Alert> : null}\n {outcome.status === 'sent' ? (\n <Alert severity=\"success\">\n {`Sent to ${outcome.answer.sentTo} as “${outcome.answer.subject}”. Check that inbox.`}\n {outcome.answer.unresolvedFields?.length\n ? ` ${crmMergeUnresolvedMessage(outcome.answer.unresolvedFields)}; they were sent empty.`\n : ''}\n </Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={props.onClose} disabled={sending}>\n {outcome.status === 'sent' ? 'Done' : 'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n onClick={() => void send()}\n disabled={sending || tooLong || step?.kind !== 'email'}\n >\n {sending ? 'Sending…' : outcome.status === 'sent' ? 'Send again' : 'Send the test'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nOutreachStepTestDialog.displayName = 'OutreachStepTestDialog'\n\nexport default OutreachStepTestDialog\n"],"names":["crmMergeUnresolvedMessage","Alert","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useEffect","useState","isInThreadEmailStep","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_SAMPLE_PERSON","useOutreachContactSearch","useOutreachLeadSearch","SAMPLE","kind","id","name","email","GROUP_LABELS","sample","lead","contact","OutreachStepTestDialog","props","outcome","open","orgId","sequence","stepIndex","api","person","setPerson","text","setText","personalLine","setPersonalLine","to","setTo","setOutcome","status","leads","hostId","enabled","contacts","contactGroupId","options","data","map","choose","next","chosen","step","steps","inThread","line","trim","tooLong","length","sending","send","answer","sendStepTest","sequenceId","leadId","contactId","error","message","title","onClose","undefined","fullWidth","maxWidth","aria-label","dividers","spacing","variant","color","unsaved","severity","value","disableClearable","inputValue","onInputChange","_event","reason","onChange","filterOptions","list","groupBy","option","getOptionLabel","isOptionEqualToValue","loading","disabled","renderOption","itemProps","li","key","renderInput","params","label","helperText","placeholder","event","target","size","type","selfEmail","sentTo","subject","unresolvedFields","onClick","displayName"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OutreachComplianceIssue } from '../model/compliance-settings';
|
|
2
|
-
import type { OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse } from '../model/outreach-api';
|
|
2
|
+
import type { OutreachCurateDraftRequest, OutreachCurateDraftResponse, OutreachCurateSaveRequest, OutreachCurateSaveResponse, OutreachDoNotContactDomainRequest, OutreachDoNotContactDomainResponse, OutreachDoNotContactDomainsResponse, OutreachEnrollmentAction, OutreachEnrollmentActionResponse, OutreachEnrollPersonRequest, OutreachEnrollPreviewResponse, OutreachEnrollResponse, OutreachEnrollSource, OutreachLinkDomainRequest, OutreachLinkDomainResponse, OutreachLinkDomainsResponse, OutreachPreviewRequest, OutreachPreviewResponse, OutreachRouteRefusalReason, OutreachSequenceAction, OutreachSequenceSaveResponse, OutreachSequenceStatusResponse, OutreachSettingsResponse, OutreachSettingsSaveRequest, OutreachSettingsSaveResponse, OutreachStepTestRequest, OutreachStepTestResponse } 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,20 @@ 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
|
+
/**
|
|
31
|
+
* Sends one step of a saved sequence to the member as a test (AGL-3325):
|
|
32
|
+
* the preview's email, through the sequence's mailbox, `[Test]` in front.
|
|
33
|
+
*/
|
|
34
|
+
sendStepTest(input: Omit<OutreachStepTestRequest, 'orgId'>): Promise<OutreachStepTestResponse & {
|
|
35
|
+
unresolvedFields?: string[];
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* One person's own copies of the sequence's email steps, drafted by the
|
|
39
|
+
* workspace's AI (AGL-3324); nothing is stored until the member confirms.
|
|
40
|
+
*/
|
|
41
|
+
curateDrafts(input: Omit<OutreachCurateDraftRequest, 'orgId'>): Promise<OutreachCurateDraftResponse>;
|
|
42
|
+
/** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */
|
|
43
|
+
saveCuratedStep(enrollmentId: string, stepIndex: number, override: OutreachCurateSaveRequest['override']): Promise<OutreachCurateSaveResponse>;
|
|
30
44
|
/** The domains on the do-not-contact list (AGL-3244). */
|
|
31
45
|
readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>;
|
|
32
46
|
/** Puts a domain on the list, or takes one off. */
|
|
@@ -131,6 +131,22 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
|
|
|
131
131
|
method: 'POST',
|
|
132
132
|
body: _extends({}, input)
|
|
133
133
|
}),
|
|
134
|
+
sendStepTest: (input)=>call(OUTREACH_API_ROUTES.stepTest, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: _extends({}, input)
|
|
137
|
+
}),
|
|
138
|
+
curateDrafts: (input)=>call(OUTREACH_API_ROUTES.curateDraft, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
body: _extends({}, input)
|
|
141
|
+
}),
|
|
142
|
+
saveCuratedStep: (enrollmentId, stepIndex, override)=>call(OUTREACH_API_ROUTES.curateSave, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: {
|
|
145
|
+
enrollmentId,
|
|
146
|
+
stepIndex,
|
|
147
|
+
override
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
134
150
|
readDoNotContactDomains: ()=>call(OUTREACH_API_ROUTES.doNotContactDomains, {
|
|
135
151
|
method: 'GET'
|
|
136
152
|
}),
|
|
@@ -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 OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\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 /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\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 readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\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","readLinkDomains","linkDomains","changeLinkDomain"],"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 OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\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 * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\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 /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\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 sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\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 readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\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","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
|
|
@@ -20,8 +20,13 @@ export interface OutreachLoad<T> {
|
|
|
20
20
|
}
|
|
21
21
|
/** The most sequences the list reads. */
|
|
22
22
|
export declare const OUTREACH_SEQUENCES_LIMIT = 200;
|
|
23
|
+
/** The sequences read, and whether the organization holds older ones. */
|
|
24
|
+
export interface OutreachSequencesLoad extends OutreachLoad<OutreachSequence[]> {
|
|
25
|
+
/** True when there are sequences older than the `OUTREACH_SEQUENCES_LIMIT` read. */
|
|
26
|
+
truncated?: boolean;
|
|
27
|
+
}
|
|
23
28
|
/** The organization's sequences, newest first. */
|
|
24
|
-
export declare function useOutreachSequences(orgId: string | null):
|
|
29
|
+
export declare function useOutreachSequences(orgId: string | null): OutreachSequencesLoad;
|
|
25
30
|
/** One sequence, or `null` once it is known not to exist. */
|
|
26
31
|
export declare function useOutreachSequence(orgId: string | null, sequenceId: string | null): OutreachLoad<OutreachSequence | null>;
|
|
27
32
|
/**
|
|
@@ -47,11 +47,14 @@ function failed(data, error, what) {
|
|
|
47
47
|
if (!orgId) return undefined;
|
|
48
48
|
return onSnapshot(query(collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.sequences), // Ordered, so the cap keeps the newest rather than an arbitrary set;
|
|
49
49
|
// every sequence carries `createdAtMs`, stamped by the save route.
|
|
50
|
-
orderBy('createdAtMs', 'desc'),
|
|
51
|
-
|
|
50
|
+
orderBy('createdAtMs', 'desc'), // One past the cap: the extra document says there are older
|
|
51
|
+
// sequences, and is never listed.
|
|
52
|
+
limit(OUTREACH_SEQUENCES_LIMIT + 1)), (snapshot)=>{
|
|
53
|
+
const sequences = snapshot.docs.slice(0, OUTREACH_SEQUENCES_LIMIT).map((entry)=>readStoredOutreachSequence(entry.id, entry.data())).filter((sequence)=>sequence !== null).sort((a, b)=>b.createdAtMs - a.createdAtMs);
|
|
52
54
|
setResult({
|
|
53
55
|
status: 'ready',
|
|
54
|
-
data: sequences
|
|
56
|
+
data: sequences,
|
|
57
|
+
truncated: snapshot.docs.length > OUTREACH_SEQUENCES_LIMIT
|
|
55
58
|
});
|
|
56
59
|
}, (error)=>setResult(failed([], error, 'sequences')));
|
|
57
60
|
}, [
|