@antelopejs/dms-mailing 0.4.0 → 0.5.0

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/pages/sends.js +2 -2
  3. package/dist/pages/sends.js.map +1 -1
  4. package/dist/pages/suppressions.js +2 -2
  5. package/dist/pages/suppressions.js.map +1 -1
  6. package/dist/pages/templates.js +3 -3
  7. package/dist/pages/templates.js.map +1 -1
  8. package/frontend-vue/app/components/CategoriesInput.vue +7 -14
  9. package/frontend-vue/app/components/DuplicateModal.vue +9 -3
  10. package/frontend-vue/app/components/Editor.vue +3 -3
  11. package/frontend-vue/app/components/NewTemplateModal.vue +9 -3
  12. package/frontend-vue/app/components/OverviewGate.vue +20 -29
  13. package/frontend-vue/app/components/PageHeader.vue +8 -12
  14. package/frontend-vue/app/components/RealSendModal.vue +3 -1
  15. package/frontend-vue/app/components/RenderingModal.vue +5 -7
  16. package/frontend-vue/app/components/RetentionInput.vue +13 -23
  17. package/frontend-vue/app/components/SendDrawer.vue +5 -2
  18. package/frontend-vue/app/components/StarterGallery.vue +11 -9
  19. package/frontend-vue/app/components/TemplateCard.vue +19 -4
  20. package/frontend-vue/app/components/TemplateDrawer.vue +8 -2
  21. package/frontend-vue/app/components/TemplateGallery.vue +25 -20
  22. package/frontend-vue/app/components/TemplatePreviewFrame.vue +8 -6
  23. package/frontend-vue/app/components/TestSendModal.vue +3 -1
  24. package/frontend-vue/app/components/VersionHistory.vue +3 -3
  25. package/frontend-vue/app/components/WebhookStatus.vue +20 -17
  26. package/frontend-vue/app/components/editor/EditorWorkspace.vue +5 -0
  27. package/frontend-vue/app/composables/useEditorActions.ts +3 -1
  28. package/frontend-vue/app/composables/useEditorSession.ts +6 -4
  29. package/frontend-vue/app/composables/useMailingApi.ts +5 -58
  30. package/frontend-vue/app/composables/useMailingReads.ts +229 -0
  31. package/frontend-vue/app/composables/useOverviewSetup.ts +15 -7
  32. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -27
  33. package/frontend-vue/app/utils/gallery.ts +19 -7
  34. package/frontend-vue/app/utils/period.ts +0 -5
  35. package/frontend-vue/package.json +1 -1
  36. package/frontend-vue/tests/block-components.test.ts +0 -3
  37. package/frontend-vue/tests/categories-input.test.ts +44 -13
  38. package/frontend-vue/tests/conditions.test.ts +0 -2
  39. package/frontend-vue/tests/editor-block-list.test.ts +1 -0
  40. package/frontend-vue/tests/editor-session.test.ts +5 -0
  41. package/frontend-vue/tests/footer-block.test.ts +1 -0
  42. package/frontend-vue/tests/funnel-card.test.ts +1 -0
  43. package/frontend-vue/tests/gallery.test.ts +16 -0
  44. package/frontend-vue/tests/helpers/api-data.ts +173 -0
  45. package/frontend-vue/tests/i18n-editor.test.ts +0 -9
  46. package/frontend-vue/tests/new-template-modal.test.ts +18 -5
  47. package/frontend-vue/tests/on-demand-reads.test.ts +81 -0
  48. package/frontend-vue/tests/overview-gate.test.ts +60 -21
  49. package/frontend-vue/tests/page-reads.test.ts +157 -0
  50. package/frontend-vue/tests/period.test.ts +0 -9
  51. package/frontend-vue/tests/retention-input.test.ts +31 -9
  52. package/frontend-vue/tests/send-modals.test.ts +9 -1
  53. package/frontend-vue/tests/shortcuts.test.ts +1 -0
  54. package/frontend-vue/tests/template-drawer.test.ts +31 -14
  55. package/frontend-vue/tests/template-gallery.test.ts +46 -24
  56. package/frontend-vue/tests/variable-path.test.ts +1 -0
  57. package/frontend-vue/vitest.config.ts +4 -1
  58. package/package.json +5 -5
  59. package/src/pages/sends.ts +2 -2
  60. package/src/pages/suppressions.ts +2 -2
  61. package/src/pages/templates.ts +3 -3
  62. package/frontend-vue/tests/smoke.test.ts +0 -8
@@ -2,6 +2,7 @@
2
2
  import { useTemplateCategories } from '../composables/useTemplateCategories'
3
3
  import { useLocalizedDefaults } from '../composables/useLocalizedDefaults'
4
4
  import { useMailingApi } from '../composables/useMailingApi'
5
+ import { useStarters } from '../composables/useMailingReads'
5
6
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
6
7
  import {
7
8
  HTTP_CONFLICT,
@@ -24,18 +25,19 @@ const { processApiMessage } = useTranslation()
24
25
  const overlays = useTemplateDrawer()
25
26
 
26
27
  const localized = useLocalizedDefaults()
27
- const storedStarters = ref<StarterSummary[]>([])
28
- const starters = computed(() => localized.starters(storedStarters.value))
28
+ const starterList = useStarters()
29
+ /** The starters, none when the read failed. */
30
+ const starters = computed(() =>
31
+ localized.starters(
32
+ starterList.error.value ? [] : (starterList.data.value?.starters ?? []),
33
+ ),
34
+ )
29
35
  const { categories } = useTemplateCategories()
30
- const loading = ref(true)
36
+ const loading = computed(
37
+ () => !starterList.data.value && !starterList.error.value,
38
+ )
31
39
  const creatingId = ref<string | null>(null)
32
40
 
33
- onMounted(async () => {
34
- const starterList = await api.starters().catch(() => ({ starters: [] }))
35
- storedStarters.value = starterList.starters
36
- loading.value = false
37
- })
38
-
39
41
  function knownCategory(id: string): string | undefined {
40
42
  return categories.value.some((category) => category.id === id)
41
43
  ? id
@@ -1,5 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { formatCardDate, formatCardSince, firstName } from '../utils/gallery'
2
+ import {
3
+ CARD_DATE_FORMAT,
4
+ formatCardDate,
5
+ formatCardSince,
6
+ firstName,
7
+ } from '../utils/gallery'
3
8
  import { localeChipClass, localeChips } from '../utils/locales'
4
9
  import {
5
10
  cardFooter,
@@ -47,6 +52,7 @@ const emit = defineEmits<{
47
52
  }>()
48
53
 
49
54
  const { t, locale } = useI18n()
55
+ const { dateTimeFormat } = useRegionalFormat()
50
56
  const { uniqueLocales } = useUniqueLocales()
51
57
 
52
58
  const isLive = computed(() => props.template.status === 'live')
@@ -85,8 +91,12 @@ const isNeverSent = computed(() => props.attention.includes('never_sent'))
85
91
  const footer = computed(() => cardFooter(props.template, props.stats))
86
92
 
87
93
  const footerDate = computed(() =>
88
- formatCardDate(footer.value.at, new Date(), locale.value, (date) =>
89
- t('dms_mailing.common.on_date', { date }),
94
+ formatCardDate(
95
+ footer.value.at,
96
+ new Date(),
97
+ locale.value,
98
+ (date) => t('dms_mailing.common.on_date', { date }),
99
+ dateTimeFormat(CARD_DATE_FORMAT),
90
100
  ),
91
101
  )
92
102
 
@@ -107,7 +117,12 @@ const openRateLabel = computed(() =>
107
117
 
108
118
  /** "live for 1 hr" within a day, "live since Sep 27" after. */
109
119
  const liveSince = computed(() =>
110
- formatCardSince(footer.value.at, new Date(), locale.value),
120
+ formatCardSince(
121
+ footer.value.at,
122
+ new Date(),
123
+ locale.value,
124
+ dateTimeFormat(CARD_DATE_FORMAT),
125
+ ),
111
126
  )
112
127
 
113
128
  const FOOTER_TEXTS: Partial<Record<CardFooterKind, () => string>> = {
@@ -5,6 +5,10 @@ import {
5
5
  useMailingApi,
6
6
  type TemplateDetails,
7
7
  } from '../composables/useMailingApi'
8
+ import {
9
+ useTemplateContentRead,
10
+ useTemplatePerformanceRead,
11
+ } from '../composables/useMailingReads'
8
12
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
9
13
  import { apiErrorMessage } from '../utils/api-error'
10
14
  import { formatCardDate } from '../utils/gallery'
@@ -106,6 +110,8 @@ const LIFECYCLE: Record<LifecycleAction, LifecycleSpec> = {
106
110
  }
107
111
 
108
112
  const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
113
+ const readContent = useTemplateContentRead(templateId)
114
+ const readPerformance = useTemplatePerformanceRead(templateId)
109
115
  const detail = ref<TemplateContentResponse | null>(null)
110
116
  const performance = ref<TemplatePerformance | null>(null)
111
117
  const loading = ref(true)
@@ -129,8 +135,8 @@ async function load(): Promise<void> {
129
135
  loading.value = true
130
136
  try {
131
137
  const [content, stats] = await Promise.all([
132
- api.templateContent(templateId.value),
133
- api.performance(templateId.value).catch(() => null),
138
+ readContent(),
139
+ readPerformance().catch(() => null),
134
140
  ])
135
141
  detail.value = content
136
142
  performance.value = stats
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { useTemplateCategories } from '../composables/useTemplateCategories'
3
3
  import { useEventListener } from '@vueuse/core'
4
- import { useMailingApi } from '../composables/useMailingApi'
4
+ import {
5
+ useTemplatesOverview,
6
+ useTemplateTotal,
7
+ } from '../composables/useMailingReads'
5
8
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
6
9
  import {
7
10
  groupByCategory,
@@ -44,29 +47,33 @@ const ALL_TAB = 'all'
44
47
 
45
48
  const props = defineProps<TemplateGalleryProps>()
46
49
 
47
- const api = useMailingApi()
48
50
  const { t } = useI18n()
49
51
  const overlays = useTemplateDrawer()
50
52
 
51
53
  const { categories } = useTemplateCategories()
52
- const statsById = ref(new Map<string, TemplateStats>())
53
- const templateCount = ref<number | null>(null)
54
+ const overview = useTemplatesOverview()
55
+ const total = useTemplateTotal()
54
56
 
55
- async function loadStats(): Promise<void> {
56
- const response = await api.templatesOverview().catch(() => ({ items: [] }))
57
- statsById.value = new Map(response.items.map((entry) => [entry.id, entry]))
58
- }
57
+ /** Each template's figures; none for a failed read. */
58
+ const statsById = computed(
59
+ () =>
60
+ new Map<string, TemplateStats>(
61
+ (overview.error.value ? [] : (overview.data.value?.items ?? [])).map(
62
+ (entry) => [entry.id, entry],
63
+ ),
64
+ ),
65
+ )
66
+ /** How many templates the tenant has; unknown while read or once failed. */
67
+ const templateCount = computed<number | null>(() =>
68
+ total.error.value ? null : (total.data.value?.total ?? null),
69
+ )
59
70
 
60
- async function loadTemplates(): Promise<void> {
61
- const response = await api.listTemplates().catch(() => null)
62
- templateCount.value = response ? response.total : null
71
+ /** The template count and the figures, read again with the rows. */
72
+ function refreshFigures(): void {
73
+ void overview.refresh()
74
+ void total.refresh()
63
75
  }
64
76
 
65
- onMounted(() => {
66
- void loadStats()
67
- void loadTemplates()
68
- })
69
-
70
77
  /** The page of the table on screen: its query and its page index. */
71
78
  const viewKeyOf = (): string =>
72
79
  JSON.stringify([props.context.query, props.context.pagination.pageIndex])
@@ -86,8 +93,7 @@ watch(
86
93
  const isRefresh = viewKey === shownViewKey
87
94
  shownViewKey = viewKey
88
95
  if (!isRefresh) return
89
- void loadStats()
90
- void loadTemplates()
96
+ refreshFigures()
91
97
  },
92
98
  )
93
99
 
@@ -157,10 +163,9 @@ const emptyDescription = computed(() =>
157
163
  : t('dms_mailing.templates.empty.view_description'),
158
164
  )
159
165
 
166
+ /** The rows, and with them the template count and the figures. */
160
167
  function refreshAll(): void {
161
168
  void props.context.refresh()
162
- void loadStats()
163
- void loadTemplates()
164
169
  }
165
170
 
166
171
  function openRow(row: TemplateRow): void {
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useDebounceFn, useIntersectionObserver } from '@vueuse/core'
3
3
  import { useMailingApi } from '../composables/useMailingApi'
4
+ import { useStarterPreviewRead } from '../composables/useMailingReads'
4
5
  import type {
5
6
  PreviewResponse,
6
7
  PreviewVersion,
@@ -62,6 +63,10 @@ const emit = defineEmits<{ resolved: [PreviewResponse] }>()
62
63
  const api = useMailingApi()
63
64
  const { locale: uiLocale } = useI18n()
64
65
 
66
+ const readStarterPreview = useStarterPreviewRead(
67
+ () => props.starterId,
68
+ () => props.locale ?? uiLocale.value,
69
+ )
65
70
  const fetchedHtml = ref('')
66
71
  const loading = ref(false)
67
72
  const height = ref(FALLBACK_HEIGHT)
@@ -86,11 +91,8 @@ useIntersectionObserver(box, ([entry]) => {
86
91
  if (entry?.isIntersecting) isVisible.value = true
87
92
  })
88
93
 
89
- async function fetchStarter(starterId: string): Promise<void> {
90
- const preview = await api.starterPreview(
91
- starterId,
92
- props.locale ?? uiLocale.value,
93
- )
94
+ async function fetchStarter(): Promise<void> {
95
+ const preview = await readStarterPreview()
94
96
  fetchedHtml.value = preview.html
95
97
  }
96
98
 
@@ -111,7 +113,7 @@ async function load(): Promise<void> {
111
113
  loading.value = true
112
114
  try {
113
115
  await (props.starterId
114
- ? fetchStarter(props.starterId)
116
+ ? fetchStarter()
115
117
  : fetchTemplate(props.templateId as string))
116
118
  } catch {
117
119
  fetchedHtml.value = ''
@@ -2,6 +2,7 @@
2
2
  import FormFieldRow from '../build/components/FormFieldRow.vue'
3
3
  import FormRows from '../build/components/FormRows.vue'
4
4
  import { useMailingApi } from '../composables/useMailingApi'
5
+ import { useTemplateContentRead } from '../composables/useMailingReads'
5
6
  import type { SendResult, TestSendInput } from '../composables/useMailingApi'
6
7
  import { apiErrorMessage } from '../utils/api-error'
7
8
  import { sendLogRoute } from '../utils/send-link'
@@ -54,6 +55,7 @@ const { user } = useCurrentUser()
54
55
  const { uniqueLocales } = useUniqueLocales()
55
56
 
56
57
  const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
58
+ const readContent = useTemplateContentRead(templateId)
57
59
  const detail = ref<TemplateContentResponse | null>(null)
58
60
  const ownEmail = (user.value as SignedInUser | null)?.email
59
61
  const recipients = ref<string[]>(ownEmail ? [ownEmail] : [])
@@ -65,7 +67,7 @@ const sending = ref(false)
65
67
  onMounted(async () => {
66
68
  if (!templateId.value) return
67
69
  try {
68
- detail.value = await api.templateContent(templateId.value)
70
+ detail.value = await readContent()
69
71
  locale.value ||= detail.value.fallbackLocale
70
72
  } catch (error) {
71
73
  toast.add({
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from 'vue'
3
- import { useMailingApi } from '../composables/useMailingApi'
3
+ import { useTemplateVersionsRead } from '../composables/useMailingReads'
4
4
  import type { TemplateStatus, TemplateVersionSummary } from '../types/mailing'
5
5
  import { apiErrorMessage } from '../utils/api-error'
6
6
  import {
@@ -31,7 +31,6 @@ const props = withDefaults(defineProps<VersionHistoryProps>(), {
31
31
  locale: undefined,
32
32
  })
33
33
 
34
- const api = useMailingApi()
35
34
  const modal = useModal()
36
35
  const toast = useToast()
37
36
  const { confirm } = useConfirm()
@@ -39,13 +38,14 @@ const { t, locale: uiLocale } = useI18n()
39
38
  const { processApiMessage } = useTranslation()
40
39
  const previewComponent = resolveComponent(PREVIEW_COMPONENT) as Component
41
40
 
41
+ const readVersions = useTemplateVersionsRead(() => props.templateId)
42
42
  const versions = ref<TemplateVersionSummary[]>([])
43
43
  const loading = ref(true)
44
44
 
45
45
  async function load(): Promise<void> {
46
46
  loading.value = true
47
47
  try {
48
- versions.value = (await api.versions(props.templateId)).versions
48
+ versions.value = (await readVersions()).versions
49
49
  } catch (error) {
50
50
  toast.add({
51
51
  color: 'error',
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useMailingApi } from '../composables/useMailingApi'
3
- import { formatCardDate } from '../utils/gallery'
3
+ import { useWebhookStatus } from '../composables/useMailingReads'
4
+ import { CARD_DATE_FORMAT, formatCardDate } from '../utils/gallery'
4
5
  import {
5
6
  type WebhookLineTone,
6
7
  type WebhookStatus,
@@ -33,9 +34,14 @@ const TONE_CLASSES: Record<WebhookLineTone, string> = {
33
34
  const api = useMailingApi()
34
35
  const toast = useToast()
35
36
  const { t, locale } = useI18n()
37
+ const { dateTimeFormat } = useRegionalFormat()
36
38
 
37
- const status = ref<WebhookStatus | null>(null)
38
- const isLoading = ref(true)
39
+ const read = useWebhookStatus()
40
+ /** The status read, `null` when the read failed. */
41
+ const status = computed<WebhookStatus | null>(() =>
42
+ read.error.value ? null : read.data.value,
43
+ )
44
+ const isLoading = computed(() => !read.data.value && !read.error.value)
39
45
  const isTesting = ref(false)
40
46
 
41
47
  const lines = computed(() =>
@@ -51,17 +57,15 @@ function lineText(line: WebhookStatusLine): string {
51
57
  }
52
58
 
53
59
  function lineTime(line: WebhookStatusLine): string {
54
- return line.at ? formatCardDate(line.at, new Date(), locale.value) : ''
55
- }
56
-
57
- async function load(): Promise<void> {
58
- try {
59
- status.value = await api.webhookStatus()
60
- } catch {
61
- status.value = null
62
- } finally {
63
- isLoading.value = false
64
- }
60
+ return line.at
61
+ ? formatCardDate(
62
+ line.at,
63
+ new Date(),
64
+ locale.value,
65
+ undefined,
66
+ dateTimeFormat(CARD_DATE_FORMAT),
67
+ )
68
+ : ''
65
69
  }
66
70
 
67
71
  async function sendTest(): Promise<void> {
@@ -69,7 +73,8 @@ async function sendTest(): Promise<void> {
69
73
  isTesting.value = true
70
74
  try {
71
75
  const result = await api.webhookTest()
72
- status.value = result.status
76
+ read.data.value = result.status
77
+ read.error.value = null
73
78
  const message = webhookTestMessage(result)
74
79
  toast.add({
75
80
  color: message.color,
@@ -84,8 +89,6 @@ async function sendTest(): Promise<void> {
84
89
  isTesting.value = false
85
90
  }
86
91
  }
87
-
88
- onMounted(() => void load())
89
92
  </script>
90
93
 
91
94
  <template>
@@ -2,6 +2,7 @@
2
2
  import { onMounted, watch } from 'vue'
3
3
  import type { TemplateContentResponse } from '../../types/mailing'
4
4
  import { useMailingApi } from '../../composables/useMailingApi'
5
+ import { useEditorReads } from '../../composables/useMailingReads'
5
6
  import { provideEditorContext } from '../../composables/useEditorContext'
6
7
  import { useEditorActions } from '../../composables/useEditorActions'
7
8
  import { useEditorSession } from '../../composables/useEditorSession'
@@ -49,9 +50,12 @@ function reportFailure(error: unknown): void {
49
50
  })
50
51
  }
51
52
 
53
+ const reads = useEditorReads(props.templateId)
54
+
52
55
  const session = useEditorSession({
53
56
  editor,
54
57
  api,
58
+ reads,
55
59
  templateId: props.templateId,
56
60
  loaded: props.loaded,
57
61
  onError: reportFailure,
@@ -61,6 +65,7 @@ const actions = useEditorActions({
61
65
  editor,
62
66
  session,
63
67
  api,
68
+ reads,
64
69
  templateId: props.templateId,
65
70
  reload: props.reload,
66
71
  onError: reportFailure,
@@ -1,5 +1,6 @@
1
1
  import type { Component } from 'vue'
2
2
  import type { MailingApi } from './useMailingApi'
3
+ import type { EditorReads } from './useMailingReads'
3
4
  import type { EditorSession } from './useEditorSession'
4
5
  import { isDraftConflict } from './useEditorSession'
5
6
  import type { EditorState } from './useTemplateEditor'
@@ -34,6 +35,7 @@ export interface ActionDeps {
34
35
  editor: EditorState
35
36
  session: EditorSession
36
37
  api: MailingApi
38
+ reads: EditorReads
37
39
  templateId: string
38
40
  reload: () => Promise<void>
39
41
  onError: (error: unknown) => void
@@ -163,7 +165,7 @@ async function runPublish(deps: ActionDeps, tools: ActionTools): Promise<void> {
163
165
  * editor's last write is a conflict: the review would not show what ships.
164
166
  */
165
167
  async function refreshChanges(deps: ActionDeps): Promise<boolean> {
166
- const response = await deps.api.changes(deps.templateId).catch(() => null)
168
+ const response = await deps.reads.changes().catch(() => null)
167
169
  if (!response) return true
168
170
  const { state } = deps.session
169
171
  if (response.revision !== state.revision) {
@@ -8,6 +8,7 @@ import type {
8
8
  TemplateRow,
9
9
  } from '../types/mailing'
10
10
  import type { MailingApi, TemplateDetails } from './useMailingApi'
11
+ import type { EditorReads } from './useMailingReads'
11
12
  import type { EditorState } from './useTemplateEditor'
12
13
  import type { TokenFieldHandle } from './useEditorContext'
13
14
  import { deepClone } from '../utils/clone'
@@ -64,6 +65,7 @@ export interface SessionState {
64
65
  export interface SessionDeps {
65
66
  editor: EditorState
66
67
  api: MailingApi
68
+ reads: EditorReads
67
69
  templateId: string
68
70
  loaded: TemplateContentResponse
69
71
  onError: (error: unknown) => void
@@ -142,7 +144,7 @@ export function retryDelay(failures: number): number {
142
144
  }
143
145
 
144
146
  function createSaver(deps: SessionDeps, state: SessionState) {
145
- const { editor, api, templateId, onError } = deps
147
+ const { editor, api, reads, templateId, onError } = deps
146
148
  let timer: ReturnType<typeof setTimeout> | null = null
147
149
  let inFlight: Promise<boolean> | null = null
148
150
  let failures = 0
@@ -207,7 +209,7 @@ function createSaver(deps: SessionDeps, state: SessionState) {
207
209
  async function overwrite(): Promise<boolean> {
208
210
  cancel()
209
211
  if (inFlight) await inFlight
210
- const latest = await api.changes(templateId).catch(() => null)
212
+ const latest = await reads.changes().catch(() => null)
211
213
  if (!latest) return false
212
214
  state.revision = latest.revision
213
215
  return run()
@@ -249,7 +251,7 @@ function createSaver(deps: SessionDeps, state: SessionState) {
249
251
  }
250
252
 
251
253
  function createRecordActions(
252
- { api, templateId }: SessionDeps,
254
+ { api, reads, templateId }: SessionDeps,
253
255
  state: SessionState,
254
256
  ) {
255
257
  return {
@@ -274,7 +276,7 @@ function createRecordActions(
274
276
  state.testDataSource = state.savedTestDataSource
275
277
  },
276
278
  async loadPerformance(): Promise<void> {
277
- state.performance = await api.performance(templateId).catch(() => null)
279
+ state.performance = await reads.performance().catch(() => null)
278
280
  },
279
281
  }
280
282
  }
@@ -2,29 +2,22 @@ import type {
2
2
  FunnelStep,
3
3
  PreviewResponse,
4
4
  PreviewVersion,
5
- ProviderInfo,
6
- RetentionPreview,
7
- SendDetailResponse,
8
- SendHtmlResponse,
9
5
  StarterSummary,
10
6
  SuppressionSummary,
11
7
  TemplateChange,
12
8
  TemplateContent,
13
9
  TemplateCategory,
14
- TemplateContentResponse,
15
- TemplatePerformance,
16
10
  TemplateRow,
17
11
  TemplateStats,
18
12
  TemplateVersionSummary,
19
13
  VariableDefinition,
20
14
  } from '../types/mailing'
21
- import type { SetupFacts } from '../utils/overview-setup'
22
- import type { WebhookStatus, WebhookTestResult } from '../utils/webhook-status'
15
+ import type { WebhookTestResult } from '../utils/webhook-status'
23
16
 
24
17
  const BASE = '/api/mailing'
25
18
 
26
19
  /** Far above any real tenant's template count; the data-api caps at 100. */
27
- const TEMPLATE_LIST_LIMIT = '100'
20
+ export const TEMPLATE_LIST_LIMIT = '100'
28
21
 
29
22
  /**
30
23
  * Joins the module's API segments. Exported on its own so the path shape stays
@@ -187,8 +180,9 @@ export interface StarterPreviewResponse {
187
180
  }
188
181
 
189
182
  /**
190
- * The single place that knows the mailing endpoints; every component reads
191
- * through it so a route change is a one-file edit.
183
+ * The mailing endpoints a component writes to, and the previews it renders
184
+ * from a draft it sends. Every read goes through `useMailingReads`, with the
185
+ * same paths.
192
186
  */
193
187
  export function useMailingApi() {
194
188
  const { $authFetch } = useAuthFetch()
@@ -196,29 +190,14 @@ export function useMailingApi() {
196
190
  $authFetch<T>(path, { method: 'POST', body })
197
191
 
198
192
  return {
199
- provider: () => $authFetch<ProviderInfo>(mailingPath('provider')),
200
- setupFacts: () => $authFetch<SetupFacts>(mailingPath('metrics', 'setup')),
201
- webhookStatus: () =>
202
- $authFetch<WebhookStatus>(mailingPath('settings', 'webhook-status')),
203
193
  webhookTest: () =>
204
194
  post<WebhookTestResult>(mailingPath('settings', 'webhook-test'), {}),
205
- templateContent: (id: string) =>
206
- $authFetch<TemplateContentResponse>(
207
- mailingPath('templates', id, 'content'),
208
- ),
209
195
  saveTemplateDetails: (id: string, details: TemplateDetails) =>
210
196
  $authFetch<unknown>(mailingPath('tables', 'templates', 'edit'), {
211
197
  method: 'PUT',
212
198
  query: { id },
213
199
  body: details,
214
200
  }),
215
- listCategories: () =>
216
- $authFetch<CategoriesResponse>(mailingPath('templates', 'categories')),
217
- listTemplates: () =>
218
- $authFetch<TemplateListResponse>(
219
- mailingPath('tables', 'templates', 'list'),
220
- { query: { limit: TEMPLATE_LIST_LIMIT, sortKey: 'name' } },
221
- ),
222
201
  createTemplate: (input: CreateTemplateInput) =>
223
202
  post<string[]>(mailingPath('tables', 'templates', 'new'), input),
224
203
  saveContent: (id: string, input: SaveContentInput) =>
@@ -231,30 +210,11 @@ export function useMailingApi() {
231
210
  },
232
211
  keepalive: input.keepalive,
233
212
  }),
234
- changes: (id: string) =>
235
- $authFetch<ChangesResponse>(mailingPath('templates', id, 'changes')),
236
- versions: (id: string) =>
237
- $authFetch<VersionsResponse>(mailingPath('templates', id, 'versions')),
238
213
  restoreVersion: (id: string, version: number, revision?: number) =>
239
214
  post<RestoreVersionResponse>(
240
215
  mailingPath('templates', id, 'versions', String(version), 'restore'),
241
216
  { revision },
242
217
  ),
243
- performance: (id: string) =>
244
- $authFetch<TemplatePerformance>(
245
- mailingPath('templates', id, 'performance'),
246
- ),
247
- templatesOverview: () =>
248
- $authFetch<TemplateOverviewResponse>(
249
- mailingPath('templates', 'overview'),
250
- ),
251
- starters: () =>
252
- $authFetch<StartersResponse>(mailingPath('templates', 'starters')),
253
- starterPreview: (starterId: string, locale?: string) =>
254
- $authFetch<StarterPreviewResponse>(
255
- mailingPath('templates', 'starters', starterId, 'preview'),
256
- { query: locale ? { locale } : {} },
257
- ),
258
218
  publish: (id: string, revision?: number) =>
259
219
  post<PublishResponse>(mailingPath('templates', id, 'publish'), {
260
220
  revision,
@@ -291,21 +251,8 @@ export function useMailingApi() {
291
251
  post<TestSendResponse>(mailingPath('templates', id, 'send'), input),
292
252
  testSend: (id: string, input: TestSendInput) =>
293
253
  post<TestSendResponse>(mailingPath('templates', id, 'test-send'), input),
294
- send: (id: string) =>
295
- $authFetch<SendDetailResponse>(mailingPath('sends', id)),
296
- sendHtml: (id: string) =>
297
- $authFetch<SendHtmlResponse>(mailingPath('sends', id, 'html')),
298
254
  replay: (id: string, to?: string) =>
299
255
  post<SendResult>(mailingPath('sends', id, 'replay'), to ? { to } : {}),
300
- retentionPreview: (days: number) =>
301
- $authFetch<RetentionPreview>(
302
- mailingPath('settings', 'retention-preview'),
303
- { query: { days: String(days) } },
304
- ),
305
- categoryUsage: () =>
306
- $authFetch<CategoryUsageResponse>(
307
- mailingPath('settings', 'category-usage'),
308
- ),
309
256
  }
310
257
  }
311
258