@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.
- package/README.md +61 -339
- package/dist/data/templates-table.js +2 -10
- package/dist/data/templates-table.js.map +1 -1
- package/dist/db/models/index.d.ts +1 -0
- package/dist/db/models/index.js +1 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/send-events.model.d.ts +1 -0
- package/dist/db/models/sends.model.d.ts +1 -0
- package/dist/db/models/settings.model.d.ts +1 -0
- package/dist/db/models/suppressions.model.d.ts +1 -0
- package/dist/db/models/template-slugs.model.d.ts +27 -0
- package/dist/db/models/template-slugs.model.js +43 -0
- package/dist/db/models/template-slugs.model.js.map +1 -0
- package/dist/db/models/template-versions.model.d.ts +1 -0
- package/dist/db/models/templates.model.d.ts +1 -0
- package/dist/db/models/webhook-status.model.d.ts +1 -0
- package/dist/db/tables/index.d.ts +1 -0
- package/dist/db/tables/index.js +1 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/template-slugs.table.d.ts +7 -0
- package/dist/db/tables/template-slugs.table.js +35 -0
- package/dist/db/tables/template-slugs.table.js.map +1 -0
- package/dist/routes/templates.js +1 -0
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/templates.d.ts +2 -0
- package/dist/services/templates.js +26 -0
- package/dist/services/templates.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +18 -24
- package/frontend-vue/app/components/DuplicateModal.vue +9 -3
- package/frontend-vue/app/components/Editor.vue +3 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +9 -3
- package/frontend-vue/app/components/OverviewGate.vue +20 -29
- package/frontend-vue/app/components/PageHeader.vue +8 -12
- package/frontend-vue/app/components/RealSendModal.vue +3 -1
- package/frontend-vue/app/components/RenderingModal.vue +5 -7
- package/frontend-vue/app/components/RetentionInput.vue +13 -23
- package/frontend-vue/app/components/SendDrawer.vue +5 -2
- package/frontend-vue/app/components/StarterGallery.vue +11 -9
- package/frontend-vue/app/components/TemplateCard.vue +19 -4
- package/frontend-vue/app/components/TemplateDrawer.vue +8 -2
- package/frontend-vue/app/components/TemplateGallery.vue +25 -20
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +8 -6
- package/frontend-vue/app/components/TestSendModal.vue +3 -1
- package/frontend-vue/app/components/VersionHistory.vue +3 -3
- package/frontend-vue/app/components/WebhookStatus.vue +20 -17
- package/frontend-vue/app/components/editor/EditorBlockList.vue +33 -30
- package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +14 -14
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +5 -0
- package/frontend-vue/app/composables/useEditorActions.ts +3 -1
- package/frontend-vue/app/composables/useEditorSession.ts +6 -4
- package/frontend-vue/app/composables/useMailingApi.ts +5 -58
- package/frontend-vue/app/composables/useMailingReads.ts +229 -0
- package/frontend-vue/app/composables/useOverviewSetup.ts +15 -7
- package/frontend-vue/app/composables/useSortableList.ts +159 -0
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -27
- package/frontend-vue/app/utils/blocks/dnd.ts +2 -0
- package/frontend-vue/app/utils/gallery.ts +19 -7
- package/frontend-vue/app/utils/period.ts +0 -5
- package/frontend-vue/i18n/locales/mailing-en-GB.json +1 -1
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +1 -1
- package/frontend-vue/package.json +4 -3
- package/frontend-vue/pnpm-lock.yaml +6 -13
- package/frontend-vue/tests/categories-input.test.ts +43 -13
- package/frontend-vue/tests/editor-block-list.test.ts +46 -6
- package/frontend-vue/tests/editor-session.test.ts +4 -0
- package/frontend-vue/tests/gallery.test.ts +16 -0
- package/frontend-vue/tests/helpers/api-data.ts +173 -0
- package/frontend-vue/tests/new-template-modal.test.ts +17 -5
- package/frontend-vue/tests/on-demand-reads.test.ts +81 -0
- package/frontend-vue/tests/overview-gate.test.ts +59 -21
- package/frontend-vue/tests/page-reads.test.ts +157 -0
- package/frontend-vue/tests/period.test.ts +0 -9
- package/frontend-vue/tests/retention-input.test.ts +30 -9
- package/frontend-vue/tests/send-modals.test.ts +8 -1
- package/frontend-vue/tests/sortable-list.test.ts +253 -0
- package/frontend-vue/tests/template-drawer.test.ts +30 -14
- package/frontend-vue/tests/template-gallery.test.ts +45 -24
- package/package.json +31 -14
- package/skills/dms-mailing/SKILL.md +63 -0
- package/src/data/templates-table.ts +7 -27
- package/src/db/models/index.ts +1 -0
- package/src/db/models/template-slugs.model.ts +61 -0
- package/src/db/tables/index.ts +1 -0
- package/src/db/tables/template-slugs.table.ts +29 -0
- package/src/routes/templates.ts +2 -0
- package/src/services/templates.ts +49 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
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
|
|
27
|
-
|
|
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
|
-
<
|
|
34
|
-
|
|
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
|
-
</
|
|
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.
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
191
|
-
*
|
|
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
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import { useSetupFacts } from './useMailingReads'
|
|
3
|
+
import { needsSetup } from '../utils/overview-setup'
|
|
2
4
|
|
|
3
|
-
|
|
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,
|
|
8
|
-
* period to filter.
|
|
9
|
-
*
|
|
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(
|
|
12
|
-
|
|
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
|
}
|