@antelopejs/dms-mailing 0.4.1 → 0.5.1

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 (86) hide show
  1. package/README.md +61 -339
  2. package/dist/data/templates-table.js +2 -10
  3. package/dist/data/templates-table.js.map +1 -1
  4. package/dist/db/models/index.d.ts +1 -0
  5. package/dist/db/models/index.js +1 -0
  6. package/dist/db/models/index.js.map +1 -1
  7. package/dist/db/models/send-events.model.d.ts +1 -0
  8. package/dist/db/models/sends.model.d.ts +1 -0
  9. package/dist/db/models/settings.model.d.ts +1 -0
  10. package/dist/db/models/suppressions.model.d.ts +1 -0
  11. package/dist/db/models/template-slugs.model.d.ts +27 -0
  12. package/dist/db/models/template-slugs.model.js +43 -0
  13. package/dist/db/models/template-slugs.model.js.map +1 -0
  14. package/dist/db/models/template-versions.model.d.ts +1 -0
  15. package/dist/db/models/templates.model.d.ts +1 -0
  16. package/dist/db/models/webhook-status.model.d.ts +1 -0
  17. package/dist/db/tables/index.d.ts +1 -0
  18. package/dist/db/tables/index.js +1 -0
  19. package/dist/db/tables/index.js.map +1 -1
  20. package/dist/db/tables/template-slugs.table.d.ts +7 -0
  21. package/dist/db/tables/template-slugs.table.js +35 -0
  22. package/dist/db/tables/template-slugs.table.js.map +1 -0
  23. package/dist/routes/templates.js +1 -0
  24. package/dist/routes/templates.js.map +1 -1
  25. package/dist/services/templates.d.ts +2 -0
  26. package/dist/services/templates.js +26 -0
  27. package/dist/services/templates.js.map +1 -1
  28. package/frontend-vue/app/components/CategoriesInput.vue +18 -24
  29. package/frontend-vue/app/components/DuplicateModal.vue +9 -3
  30. package/frontend-vue/app/components/Editor.vue +3 -3
  31. package/frontend-vue/app/components/NewTemplateModal.vue +9 -3
  32. package/frontend-vue/app/components/OverviewGate.vue +20 -29
  33. package/frontend-vue/app/components/PageHeader.vue +8 -12
  34. package/frontend-vue/app/components/RealSendModal.vue +3 -1
  35. package/frontend-vue/app/components/RenderingModal.vue +5 -7
  36. package/frontend-vue/app/components/RetentionInput.vue +13 -23
  37. package/frontend-vue/app/components/SendDrawer.vue +5 -2
  38. package/frontend-vue/app/components/StarterGallery.vue +11 -9
  39. package/frontend-vue/app/components/TemplateCard.vue +19 -4
  40. package/frontend-vue/app/components/TemplateDrawer.vue +8 -2
  41. package/frontend-vue/app/components/TemplateGallery.vue +25 -20
  42. package/frontend-vue/app/components/TemplatePreviewFrame.vue +8 -6
  43. package/frontend-vue/app/components/TestSendModal.vue +3 -1
  44. package/frontend-vue/app/components/VersionHistory.vue +3 -3
  45. package/frontend-vue/app/components/WebhookStatus.vue +20 -17
  46. package/frontend-vue/app/components/editor/EditorBlockList.vue +33 -30
  47. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +14 -14
  48. package/frontend-vue/app/components/editor/EditorWorkspace.vue +5 -0
  49. package/frontend-vue/app/composables/useEditorActions.ts +3 -1
  50. package/frontend-vue/app/composables/useEditorSession.ts +6 -4
  51. package/frontend-vue/app/composables/useMailingApi.ts +5 -58
  52. package/frontend-vue/app/composables/useMailingReads.ts +229 -0
  53. package/frontend-vue/app/composables/useOverviewSetup.ts +15 -7
  54. package/frontend-vue/app/composables/useSortableList.ts +159 -0
  55. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -27
  56. package/frontend-vue/app/utils/blocks/dnd.ts +2 -0
  57. package/frontend-vue/app/utils/gallery.ts +19 -7
  58. package/frontend-vue/app/utils/period.ts +0 -5
  59. package/frontend-vue/i18n/locales/mailing-en-GB.json +1 -1
  60. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1 -1
  61. package/frontend-vue/package.json +4 -3
  62. package/frontend-vue/pnpm-lock.yaml +6 -13
  63. package/frontend-vue/tests/categories-input.test.ts +43 -13
  64. package/frontend-vue/tests/editor-block-list.test.ts +46 -6
  65. package/frontend-vue/tests/editor-session.test.ts +4 -0
  66. package/frontend-vue/tests/gallery.test.ts +16 -0
  67. package/frontend-vue/tests/helpers/api-data.ts +173 -0
  68. package/frontend-vue/tests/new-template-modal.test.ts +17 -5
  69. package/frontend-vue/tests/on-demand-reads.test.ts +81 -0
  70. package/frontend-vue/tests/overview-gate.test.ts +59 -21
  71. package/frontend-vue/tests/page-reads.test.ts +157 -0
  72. package/frontend-vue/tests/period.test.ts +0 -9
  73. package/frontend-vue/tests/retention-input.test.ts +30 -9
  74. package/frontend-vue/tests/send-modals.test.ts +8 -1
  75. package/frontend-vue/tests/sortable-list.test.ts +253 -0
  76. package/frontend-vue/tests/template-drawer.test.ts +30 -14
  77. package/frontend-vue/tests/template-gallery.test.ts +45 -24
  78. package/package.json +31 -14
  79. package/skills/dms-mailing/SKILL.md +63 -0
  80. package/src/data/templates-table.ts +7 -27
  81. package/src/db/models/index.ts +1 -0
  82. package/src/db/models/template-slugs.model.ts +61 -0
  83. package/src/db/tables/index.ts +1 -0
  84. package/src/db/tables/template-slugs.table.ts +29 -0
  85. package/src/routes/templates.ts +2 -0
  86. package/src/services/templates.ts +49 -0
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import draggable from 'vuedraggable'
2
+ import { ref } from 'vue'
3
+ import { useSortableList } from '../../composables/useSortableList'
3
4
  import {
4
5
  PALETTE_GROUP,
5
6
  cloneFromPalette,
@@ -23,23 +24,22 @@ const emit = defineEmits<{ add: [PaletteEntry] }>()
23
24
 
24
25
  const { t } = useI18n()
25
26
 
26
- const clone = (entry: PaletteEntry) =>
27
- cloneFromPalette(entry, props.blockContext())
27
+ const root = ref<HTMLElement | null>(null)
28
+
29
+ // A tile stays in the palette: the canvas receives the block it stands for.
30
+ useSortableList(root, () => props.entries, {
31
+ group: PALETTE_GROUP,
32
+ sort: false,
33
+ disabled: () => props.disabled,
34
+ clone: (entry) => cloneFromPalette(entry, props.blockContext()),
35
+ })
28
36
  </script>
29
37
 
30
38
  <template>
31
39
  <section>
32
40
  <DmsEyebrow :label="title" class="mb-2 block" />
33
- <draggable
34
- :list="entries"
35
- :group="PALETTE_GROUP"
36
- :clone="clone"
37
- :sort="false"
38
- :disabled="disabled"
39
- item-key="type"
40
- class="grid grid-cols-2 gap-1.5"
41
- >
42
- <template #item="{ element }">
41
+ <div ref="root" class="grid grid-cols-2 gap-1.5">
42
+ <template v-for="element in entries" :key="element.type">
43
43
  <button
44
44
  type="button"
45
45
  class="border-default bg-default text-toned hover:border-primary/60 hover:text-highlighted group flex min-h-9 cursor-grab items-center gap-2 rounded-md border px-2.5 py-1 text-left text-xs transition disabled:cursor-not-allowed disabled:opacity-50"
@@ -61,6 +61,6 @@ const clone = (entry: PaletteEntry) =>
61
61
  </span>
62
62
  </button>
63
63
  </template>
64
- </draggable>
64
+ </div>
65
65
  </section>
66
66
  </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
 
@@ -0,0 +1,229 @@
1
+ import { toValue, type MaybeRefOrGetter } from 'vue'
2
+ import {
3
+ mailingPath,
4
+ TEMPLATE_LIST_LIMIT,
5
+ type CategoriesResponse,
6
+ type CategoryUsageResponse,
7
+ type ChangesResponse,
8
+ type StarterPreviewResponse,
9
+ type StartersResponse,
10
+ type TemplateListResponse,
11
+ type TemplateOverviewResponse,
12
+ type VersionsResponse,
13
+ } from './useMailingApi'
14
+ import type { SetupFacts } from '../utils/overview-setup'
15
+ import type { WebhookStatus } from '../utils/webhook-status'
16
+ import type {
17
+ ProviderInfo,
18
+ RetentionPreview,
19
+ SendDetailResponse,
20
+ SendHtmlResponse,
21
+ TemplateContentResponse,
22
+ TemplatePerformance,
23
+ } from '../types/mailing'
24
+
25
+ /**
26
+ * The module's reads, through `useApiData`. The ones a page renders: a server
27
+ * render reads them and hands them to the browser with the page, and the
28
+ * components showing one request share it. The ones a drawer, a dialog or the
29
+ * editor makes when it opens or asks: `ReadOnDemand`. Writes go through
30
+ * `useMailingApi`.
31
+ */
32
+
33
+ /** The e-mail provider the page header names; nothing read while `enabled` is false. */
34
+ export function useProviderInfo(
35
+ enabled: MaybeRefOrGetter<boolean>,
36
+ ): ApiDataResult<ProviderInfo> {
37
+ return useApiData<ProviderInfo>(() =>
38
+ toValue(enabled) ? mailingPath('provider') : null,
39
+ )
40
+ }
41
+
42
+ /** What the overview's setup checklist is made of. */
43
+ export function useSetupFacts(): ApiDataResult<SetupFacts> {
44
+ return useApiData<SetupFacts>(mailingPath('metrics', 'setup'))
45
+ }
46
+
47
+ /** The webhook's last event and last refused call. */
48
+ export function useWebhookStatus(): ApiDataResult<WebhookStatus> {
49
+ return useApiData<WebhookStatus>(mailingPath('settings', 'webhook-status'))
50
+ }
51
+
52
+ /** What a retention of `days` deletes, and when; nothing read without days. */
53
+ export function useRetentionPreview(
54
+ days: MaybeRefOrGetter<number | null>,
55
+ ): ApiDataResult<RetentionPreview> {
56
+ return useApiData<RetentionPreview>(
57
+ () => (toValue(days) ? mailingPath('settings', 'retention-preview') : null),
58
+ { query: () => ({ days: String(toValue(days)) }) },
59
+ )
60
+ }
61
+
62
+ /** How many templates each category holds. */
63
+ export function useCategoryUsage(): ApiDataResult<CategoryUsageResponse> {
64
+ return useApiData<CategoryUsageResponse>(
65
+ mailingPath('settings', 'category-usage'),
66
+ )
67
+ }
68
+
69
+ /** The tenant's template categories, as stored. */
70
+ export function useStoredCategories(): ApiDataResult<CategoriesResponse> {
71
+ return useApiData<CategoriesResponse>(mailingPath('templates', 'categories'))
72
+ }
73
+
74
+ /**
75
+ * Each template's sends and rates, for the gallery's cards: read again with
76
+ * the gallery's rows, rather than with the page.
77
+ */
78
+ export function useTemplatesOverview(): ApiDataResult<TemplateOverviewResponse> {
79
+ return useApiData<TemplateOverviewResponse>(
80
+ mailingPath('templates', 'overview'),
81
+ { refreshOnPageRefresh: false },
82
+ )
83
+ }
84
+
85
+ /**
86
+ * How many templates the tenant has, whatever the gallery's filters: the
87
+ * list route's total, without its rows. Read again with the gallery's rows.
88
+ */
89
+ export function useTemplateTotal(): ApiDataResult<
90
+ Pick<TemplateListResponse, 'total'>
91
+ > {
92
+ return useApiData<Pick<TemplateListResponse, 'total'>>(
93
+ mailingPath('tables', 'templates', 'list'),
94
+ { query: { limit: '1' }, pick: ['total'], refreshOnPageRefresh: false },
95
+ )
96
+ }
97
+
98
+ /** The starter templates the module ships. */
99
+ export function useStarters(): ApiDataResult<StartersResponse> {
100
+ return useApiData<StartersResponse>(mailingPath('templates', 'starters'))
101
+ }
102
+
103
+ /** The query of a read made on demand. */
104
+ type ReadQuery = MaybeRefOrGetter<Record<string, string> | undefined>
105
+
106
+ /**
107
+ * A read a drawer, a dialog, a preview frame or the editor makes when it
108
+ * opens or asks, through `useApiData` with `immediate: false`: the call reads
109
+ * the current URL and resolves with its answer, or rejects with what the read
110
+ * failed with. The caller keeps the answer, so the page's refreshes leave it.
111
+ */
112
+ export type ReadOnDemand<T> = () => Promise<T>
113
+
114
+ function useReadOnDemand<T>(
115
+ url: MaybeRefOrGetter<string | null>,
116
+ query?: ReadQuery,
117
+ ): ReadOnDemand<T> {
118
+ const read = useApiData<T>(url, {
119
+ query,
120
+ immediate: false,
121
+ refreshOnPageRefresh: false,
122
+ })
123
+ return async () => {
124
+ await read.execute({ dedupe: 'defer' })
125
+ if (read.error.value) throw read.error.value
126
+ return read.data.value as T
127
+ }
128
+ }
129
+
130
+ /** A template's path below `/templates/<id>`; none without an id. */
131
+ const templatePath =
132
+ (id: MaybeRefOrGetter<string>, segment: string) => (): string | null =>
133
+ toValue(id) ? mailingPath('templates', toValue(id), segment) : null
134
+
135
+ /** The template's record, its draft and published content, for a drawer or the editor. */
136
+ export function useTemplateContentRead(
137
+ id: MaybeRefOrGetter<string>,
138
+ ): ReadOnDemand<TemplateContentResponse> {
139
+ return useReadOnDemand(templatePath(id, 'content'))
140
+ }
141
+
142
+ /** The template's sends and rates. */
143
+ export function useTemplatePerformanceRead(
144
+ id: MaybeRefOrGetter<string>,
145
+ ): ReadOnDemand<TemplatePerformance> {
146
+ return useReadOnDemand(templatePath(id, 'performance'))
147
+ }
148
+
149
+ /** What publishing the template's draft would change. */
150
+ export function useTemplateChangesRead(
151
+ id: MaybeRefOrGetter<string>,
152
+ ): ReadOnDemand<ChangesResponse> {
153
+ return useReadOnDemand(templatePath(id, 'changes'))
154
+ }
155
+
156
+ /** The template's published versions. */
157
+ export function useTemplateVersionsRead(
158
+ id: MaybeRefOrGetter<string>,
159
+ ): ReadOnDemand<VersionsResponse> {
160
+ return useReadOnDemand(templatePath(id, 'versions'))
161
+ }
162
+
163
+ /** Every template of the tenant, by name, for the slugs a new one must avoid. */
164
+ export function useTemplateListRead(): ReadOnDemand<TemplateListResponse> {
165
+ return useReadOnDemand(mailingPath('tables', 'templates', 'list'), {
166
+ limit: TEMPLATE_LIST_LIMIT,
167
+ sortKey: 'name',
168
+ })
169
+ }
170
+
171
+ /** The starter templates, for a dialog. */
172
+ export function useStartersRead(): ReadOnDemand<StartersResponse> {
173
+ return useReadOnDemand(mailingPath('templates', 'starters'))
174
+ }
175
+
176
+ /** A starter's preview, in `locale` when given. */
177
+ export function useStarterPreviewRead(
178
+ starterId: MaybeRefOrGetter<string | undefined>,
179
+ locale: MaybeRefOrGetter<string | undefined>,
180
+ ): ReadOnDemand<StarterPreviewResponse> {
181
+ return useReadOnDemand(
182
+ () => {
183
+ const id = toValue(starterId)
184
+ return id ? mailingPath('templates', 'starters', id, 'preview') : null
185
+ },
186
+ () => {
187
+ const value = toValue(locale)
188
+ return value ? { locale: value } : undefined
189
+ },
190
+ )
191
+ }
192
+
193
+ /** A send's path below `/sends`; none without an id. */
194
+ const sendPath =
195
+ (id: MaybeRefOrGetter<string>, ...segments: string[]) =>
196
+ (): string | null =>
197
+ toValue(id) ? mailingPath('sends', toValue(id), ...segments) : null
198
+
199
+ /** A send with its events and the template it used. */
200
+ export function useSendRead(
201
+ id: MaybeRefOrGetter<string>,
202
+ ): ReadOnDemand<SendDetailResponse> {
203
+ return useReadOnDemand(sendPath(id))
204
+ }
205
+
206
+ /** The e-mail a send rendered. */
207
+ export function useSendHtmlRead(
208
+ id: MaybeRefOrGetter<string>,
209
+ ): ReadOnDemand<SendHtmlResponse> {
210
+ return useReadOnDemand(sendPath(id, 'html'))
211
+ }
212
+
213
+ /** The e-mail provider and the tracking it reports, for a drawer. */
214
+ export function useProviderRead(): ReadOnDemand<ProviderInfo> {
215
+ return useReadOnDemand(mailingPath('provider'))
216
+ }
217
+
218
+ /** The reads the editor makes when it asks: what publishing would change, and the template's figures. */
219
+ export interface EditorReads {
220
+ changes: ReadOnDemand<ChangesResponse>
221
+ performance: ReadOnDemand<TemplatePerformance>
222
+ }
223
+
224
+ export function useEditorReads(id: MaybeRefOrGetter<string>): EditorReads {
225
+ return {
226
+ changes: useTemplateChangesRead(id),
227
+ performance: useTemplatePerformanceRead(id),
228
+ }
229
+ }
@@ -1,13 +1,21 @@
1
- import type { Ref } from 'vue'
1
+ import { computed, type ComputedRef } from 'vue'
2
+ import { useSetupFacts } from './useMailingReads'
3
+ import { needsSetup } from '../utils/overview-setup'
2
4
 
3
- const HIDES_PERIOD_KEY = 'mailing-setup-hides-period:'
5
+ /** The period scope of the Overview page, whose dashboard the setup gates. */
6
+ const OVERVIEW_PERIOD_SCOPE = 'mailing-overview'
4
7
 
5
8
  /**
6
9
  * Whether the page header should hold its period selector back for `scope`:
7
- * the setup card stands in for the dashboard, so there is no figure for a
8
- * period to filter. Shared between the Overview gate, which sets it, and the
9
- * page header, which reads it.
10
+ * on the Overview, while the setup card stands in for the dashboard, there is
11
+ * no figure for a period to filter. Read from the setup facts the Overview
12
+ * gate shows (one request for both), so a server render hides it as the
13
+ * browser does.
10
14
  */
11
- export function useSetupHidesPeriod(scope: string | undefined): Ref<boolean> {
12
- return useDmsState<boolean>(`${HIDES_PERIOD_KEY}${scope ?? ''}`, () => false)
15
+ export function useSetupHidesPeriod(
16
+ scope: string | undefined,
17
+ ): ComputedRef<boolean> {
18
+ if (scope !== OVERVIEW_PERIOD_SCOPE) return computed(() => false)
19
+ const { data } = useSetupFacts()
20
+ return computed(() => !!data.value && needsSetup(data.value))
13
21
  }