@antelopejs/dms-mailing 0.4.1 → 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.
- package/README.md +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +7 -14
- 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/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/useTemplateCategories.ts +8 -27
- package/frontend-vue/app/utils/gallery.ts +19 -7
- package/frontend-vue/app/utils/period.ts +0 -5
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +43 -13
- 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/template-drawer.test.ts +30 -14
- package/frontend-vue/tests/template-gallery.test.ts +45 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ dashboard and settings. It owns the data (tenant-scoped Mongo tables), renders
|
|
|
12
12
|
e-mails through the DMS html-render chain and sends them through
|
|
13
13
|
`@antelopejs/interface-email`.
|
|
14
14
|
|
|
15
|
-
The module targets `@antelopejs/dms` 0.
|
|
15
|
+
The module targets `@antelopejs/dms` 0.9, `@antelopejs/interface-dms` 0.7 and `@antelopejs/dms-frontend` 0.6. Every read goes through the DMS's `useApiData`: what a page renders is read by the server render and handed to the browser; what a drawer, a dialog, a preview frame or the editor shows is read when it opens or asks (`immediate: false`, `useMailingReads`).
|
|
16
16
|
Like every DMS module, its pages live in the module's own sidebar (Mailing:
|
|
17
17
|
Overview, Templates, Sends; Configure: Settings, Suppression list) and are
|
|
18
18
|
reserved to the platform owner. The design decisions behind the v2 screens are recorded in
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import draggable from 'vuedraggable'
|
|
3
3
|
import { useLocalizedDefaults } from '../composables/useLocalizedDefaults'
|
|
4
|
-
import {
|
|
4
|
+
import { useCategoryUsage } from '../composables/useMailingReads'
|
|
5
5
|
import { isMarketingCategory } from '../utils/system-variables'
|
|
6
6
|
import {
|
|
7
7
|
CATEGORY_ICONS,
|
|
@@ -53,7 +53,6 @@ const MOVE_FIELD_COMPONENT = 'DmsSelect'
|
|
|
53
53
|
const LABEL_PART = 'label'
|
|
54
54
|
const ROW_FIELD_UI = { error: 'mt-1 ps-9 text-[12px]' }
|
|
55
55
|
|
|
56
|
-
const api = useMailingApi()
|
|
57
56
|
const { t } = useI18n()
|
|
58
57
|
const localized = useLocalizedDefaults()
|
|
59
58
|
const { confirm } = useConfirm()
|
|
@@ -65,7 +64,11 @@ const root = ref<HTMLElement | null>(null)
|
|
|
65
64
|
const source = computed(() => props.modelValue ?? props.initialValue ?? [])
|
|
66
65
|
const rows = ref<EditableCategory[]>(toRows(source.value))
|
|
67
66
|
const removed = ref<CategoryEntry[]>(movesOf(source.value))
|
|
68
|
-
const
|
|
67
|
+
const usageRead = useCategoryUsage()
|
|
68
|
+
/** Templates per category; none known while the read is missing or failed. */
|
|
69
|
+
const usage = computed<Record<string, number>>(() =>
|
|
70
|
+
usageRead.error.value ? {} : (usageRead.data.value?.counts ?? {}),
|
|
71
|
+
)
|
|
69
72
|
const pickerOpen = ref<string | null>(null)
|
|
70
73
|
let lastEmitted = serializeCategories(currentEntries())
|
|
71
74
|
|
|
@@ -216,14 +219,6 @@ async function moveBy(index: number, offset: number): Promise<void> {
|
|
|
216
219
|
handles?.[moved.index]?.focus()
|
|
217
220
|
}
|
|
218
221
|
|
|
219
|
-
async function loadUsage(): Promise<void> {
|
|
220
|
-
try {
|
|
221
|
-
usage.value = (await api.categoryUsage()).counts
|
|
222
|
-
} catch {
|
|
223
|
-
usage.value = {}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
222
|
watch(source, (value) => {
|
|
228
223
|
if (serializeCategories(value) === lastEmitted) return
|
|
229
224
|
rows.value = toRows(value)
|
|
@@ -238,11 +233,9 @@ watch(
|
|
|
238
233
|
rows.value
|
|
239
234
|
.filter((row) => row.isNew && saved.has(row.id))
|
|
240
235
|
.forEach((row) => (row.isNew = false))
|
|
241
|
-
void
|
|
236
|
+
void usageRead.refresh()
|
|
242
237
|
},
|
|
243
238
|
)
|
|
244
|
-
|
|
245
|
-
onMounted(loadUsage)
|
|
246
239
|
</script>
|
|
247
240
|
|
|
248
241
|
<template>
|
|
@@ -2,6 +2,10 @@
|
|
|
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 {
|
|
6
|
+
useTemplateContentRead,
|
|
7
|
+
useTemplateListRead,
|
|
8
|
+
} from '../composables/useMailingReads'
|
|
5
9
|
import {
|
|
6
10
|
HTTP_CONFLICT,
|
|
7
11
|
apiErrorMessage,
|
|
@@ -32,6 +36,8 @@ const { t } = useI18n()
|
|
|
32
36
|
const { processApiMessage } = useTranslation()
|
|
33
37
|
|
|
34
38
|
const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
|
|
39
|
+
const readTemplates = useTemplateListRead()
|
|
40
|
+
const readContent = useTemplateContentRead(templateId)
|
|
35
41
|
const source = ref<TemplateRow | null>(props.rowData ?? null)
|
|
36
42
|
const takenSlugs = ref(new Set<string>())
|
|
37
43
|
const name = ref('')
|
|
@@ -49,13 +55,13 @@ function prefill(template: TemplateRow): void {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
onMounted(async () => {
|
|
52
|
-
const list = await
|
|
58
|
+
const list = await readTemplates().catch(() => ({ results: [] }))
|
|
53
59
|
takenSlugs.value = new Set(list.results.map((row: TemplateRow) => row.slug))
|
|
54
60
|
source.value ??=
|
|
55
61
|
list.results.find((row: TemplateRow) => row._id === templateId.value) ??
|
|
56
62
|
null
|
|
57
63
|
if (!source.value && templateId.value) {
|
|
58
|
-
const detail = await
|
|
64
|
+
const detail = await readContent().catch(() => null)
|
|
59
65
|
source.value = detail?.template ?? null
|
|
60
66
|
}
|
|
61
67
|
if (source.value) prefill(source.value)
|
|
@@ -174,7 +180,7 @@ async function submit(): Promise<void> {
|
|
|
174
180
|
:ui="{ base: 'font-mono' }"
|
|
175
181
|
:disabled="loading"
|
|
176
182
|
:trailing-icon="slugIssue ? undefined : 'i-ph-check-circle'"
|
|
177
|
-
@update:model-value="(value) => onSlugInput(String(value))"
|
|
183
|
+
@update:model-value="(value: unknown) => onSlugInput(String(value))"
|
|
178
184
|
/>
|
|
179
185
|
</template>
|
|
180
186
|
</FormFieldRow>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref } from 'vue'
|
|
3
3
|
import type { TemplateContentResponse } from '../types/mailing'
|
|
4
|
-
import {
|
|
4
|
+
import { useTemplateContentRead } from '../composables/useMailingReads'
|
|
5
5
|
import { useRecordLabel } from '../composables/useEditorContext'
|
|
6
6
|
import { templateIdFrom } from '../utils/editor-route'
|
|
7
7
|
import { TEMPLATES_PATH } from '../utils/pages'
|
|
@@ -15,10 +15,10 @@ const props = defineProps<Props>()
|
|
|
15
15
|
|
|
16
16
|
const { t } = useI18n()
|
|
17
17
|
const route = useDmsRoute()
|
|
18
|
-
const api = useMailingApi()
|
|
19
18
|
const setRecordLabel = useRecordLabel()
|
|
20
19
|
|
|
21
20
|
const templateId = computed(() => templateIdFrom(props.routeParams, route.path))
|
|
21
|
+
const readContent = useTemplateContentRead(templateId)
|
|
22
22
|
const locale = ref<string | null>(
|
|
23
23
|
route.query.locale ? String(route.query.locale) : null,
|
|
24
24
|
)
|
|
@@ -31,7 +31,7 @@ const generation = ref(0)
|
|
|
31
31
|
async function load(): Promise<void> {
|
|
32
32
|
failed.value = false
|
|
33
33
|
try {
|
|
34
|
-
loaded.value = await
|
|
34
|
+
loaded.value = await readContent()
|
|
35
35
|
setRecordLabel(loaded.value.template.name)
|
|
36
36
|
generation.value += 1
|
|
37
37
|
} catch {
|
|
@@ -5,6 +5,10 @@ import FormRows from '../build/components/FormRows.vue'
|
|
|
5
5
|
import NewTemplateSourceTile from './NewTemplateSourceTile.vue'
|
|
6
6
|
import { useLocalizedDefaults } from '../composables/useLocalizedDefaults'
|
|
7
7
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
8
|
+
import {
|
|
9
|
+
useStartersRead,
|
|
10
|
+
useTemplateListRead,
|
|
11
|
+
} from '../composables/useMailingReads'
|
|
8
12
|
import {
|
|
9
13
|
HTTP_CONFLICT,
|
|
10
14
|
apiErrorMessage,
|
|
@@ -85,6 +89,8 @@ const source = ref<SourceChoice>(
|
|
|
85
89
|
const isSubmitted = ref(false)
|
|
86
90
|
const saving = ref(false)
|
|
87
91
|
const templates = ref<TemplateRow[]>([])
|
|
92
|
+
const readTemplates = useTemplateListRead()
|
|
93
|
+
const readStarters = useStartersRead()
|
|
88
94
|
const localized = useLocalizedDefaults()
|
|
89
95
|
const storedStarters = ref<StarterSummary[]>([])
|
|
90
96
|
const starters = computed(() => localized.starters(storedStarters.value))
|
|
@@ -94,8 +100,8 @@ const templateSearch = ref('')
|
|
|
94
100
|
|
|
95
101
|
onMounted(async () => {
|
|
96
102
|
const [templateList, starterList] = await Promise.all([
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
readTemplates().catch(() => ({ results: [] as TemplateRow[] })),
|
|
104
|
+
readStarters().catch(() => ({ starters: [] as StarterSummary[] })),
|
|
99
105
|
])
|
|
100
106
|
templates.value = templateList.results
|
|
101
107
|
storedStarters.value = starterList.starters
|
|
@@ -324,7 +330,7 @@ async function submit(): Promise<void> {
|
|
|
324
330
|
:ui="{ base: 'font-mono' }"
|
|
325
331
|
:trailing-icon="slugIssue ? undefined : 'i-ph-check-circle'"
|
|
326
332
|
:aria-invalid="Boolean(slugError)"
|
|
327
|
-
@update:model-value="(value) => onSlugInput(String(value))"
|
|
333
|
+
@update:model-value="(value: unknown) => onSlugInput(String(value))"
|
|
328
334
|
/>
|
|
329
335
|
<UBadge
|
|
330
336
|
color="neutral"
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { useSetupHidesPeriod } from '../composables/useOverviewSetup'
|
|
2
|
+
import { useSetupFacts } from '../composables/useMailingReads'
|
|
4
3
|
import {
|
|
5
4
|
isSetupComplete,
|
|
6
5
|
needsSetup,
|
|
7
6
|
setupSteps,
|
|
8
|
-
type SetupFacts,
|
|
9
7
|
type SetupStep,
|
|
10
8
|
type SetupStepState,
|
|
11
9
|
} from '../utils/overview-setup'
|
|
@@ -13,10 +11,10 @@ import type { Component } from 'vue'
|
|
|
13
11
|
import type { MailingComponentProps } from '../types/component'
|
|
14
12
|
|
|
15
13
|
defineProps<MailingComponentProps>()
|
|
14
|
+
defineOptions({ inheritAttrs: false })
|
|
16
15
|
|
|
17
16
|
type GateState = 'loading' | 'error' | 'ready'
|
|
18
17
|
|
|
19
|
-
const OVERVIEW_PERIOD_SCOPE = 'mailing-overview'
|
|
20
18
|
const NEW_TEMPLATE_MODAL = 'MailingNewTemplateModal'
|
|
21
19
|
const NEW_TEMPLATE_MODAL_SIZE = 'lg'
|
|
22
20
|
const KEY_PREFIX = 'dms_mailing.setup.'
|
|
@@ -34,15 +32,25 @@ const STEP_BUTTON_VARIANTS: Record<SetupStepState, 'solid' | 'outline'> = {
|
|
|
34
32
|
todo: 'outline',
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
const api = useMailingApi()
|
|
38
35
|
const { t } = useI18n()
|
|
39
36
|
const { open: openModal } = useModal()
|
|
40
|
-
const hidesPeriod = useSetupHidesPeriod(OVERVIEW_PERIOD_SCOPE)
|
|
41
37
|
|
|
42
|
-
const facts
|
|
43
|
-
|
|
38
|
+
const { data: facts, error, status, refresh } = useSetupFacts()
|
|
39
|
+
/**
|
|
40
|
+
* The header's period selector sets up once the dashboard has: a server
|
|
41
|
+
* render, which renders the dashboard once the facts are in, would otherwise
|
|
42
|
+
* set the selector up first and render the cards with a period the browser's
|
|
43
|
+
* cards do not have yet as they hydrate.
|
|
44
|
+
*/
|
|
45
|
+
const DashboardRendered = holdPageHeaderActions()
|
|
44
46
|
const isResumed = ref(false)
|
|
45
47
|
|
|
48
|
+
/** A failure shows once its retry is over; the facts read last stay meanwhile. */
|
|
49
|
+
const state = computed<GateState>(() => {
|
|
50
|
+
if (facts.value) return 'ready'
|
|
51
|
+
return error.value && status.value !== 'pending' ? 'error' : 'loading'
|
|
52
|
+
})
|
|
53
|
+
|
|
46
54
|
const steps = computed<SetupStep[]>(() =>
|
|
47
55
|
facts.value ? setupSteps(facts.value) : [],
|
|
48
56
|
)
|
|
@@ -63,40 +71,22 @@ const errorActions = computed(() => [
|
|
|
63
71
|
{
|
|
64
72
|
label: key('retry'),
|
|
65
73
|
icon: 'i-ph-arrows-clockwise',
|
|
66
|
-
onClick: () => void
|
|
74
|
+
onClick: () => void refresh(),
|
|
67
75
|
},
|
|
68
76
|
])
|
|
69
77
|
|
|
70
78
|
const stepDescription = (step: SetupStep): string =>
|
|
71
79
|
key(`steps.${step.id}.${step.state === 'done' ? 'done' : 'description'}`)
|
|
72
80
|
|
|
73
|
-
async function load(): Promise<void> {
|
|
74
|
-
state.value = 'loading'
|
|
75
|
-
try {
|
|
76
|
-
facts.value = await api.setupFacts()
|
|
77
|
-
state.value = 'ready'
|
|
78
|
-
} catch {
|
|
79
|
-
state.value = 'error'
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
81
|
function createTemplate(): void {
|
|
84
82
|
const modal = openModal({
|
|
85
83
|
title: t('dms_mailing.setup.create_template'),
|
|
86
84
|
component: resolveComponent(NEW_TEMPLATE_MODAL) as Component,
|
|
87
85
|
size: NEW_TEMPLATE_MODAL_SIZE,
|
|
88
86
|
})
|
|
89
|
-
|
|
87
|
+
const reread = () => refresh()
|
|
88
|
+
void modal.result.then(reread, reread)
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
watch(isBlocking, (blocking) => {
|
|
93
|
-
hidesPeriod.value = blocking
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
onMounted(load)
|
|
97
|
-
onBeforeUnmount(() => {
|
|
98
|
-
hidesPeriod.value = false
|
|
99
|
-
})
|
|
100
90
|
</script>
|
|
101
91
|
|
|
102
92
|
<template>
|
|
@@ -217,4 +207,5 @@ onBeforeUnmount(() => {
|
|
|
217
207
|
|
|
218
208
|
<slot v-if="showsDashboard" />
|
|
219
209
|
</div>
|
|
210
|
+
<DashboardRendered />
|
|
220
211
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useProviderInfo } from '../composables/useMailingReads'
|
|
3
3
|
import { useSetupHidesPeriod } from '../composables/useOverviewSetup'
|
|
4
4
|
import { presetShortLabelKey } from '../utils/period'
|
|
5
5
|
import type { Component } from 'vue'
|
|
@@ -26,13 +26,18 @@ const props = withDefaults(defineProps<PageHeaderProps>(), {
|
|
|
26
26
|
showProvider: true,
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
const api = useMailingApi()
|
|
30
29
|
const { t } = useI18n()
|
|
31
30
|
|
|
32
31
|
const StatusPill = resolveComponent('DmsStatusPill') as Component
|
|
33
32
|
const PeriodSelector = resolveComponent('DmsPeriodSelector') as Component
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
/** Shown as "no provider" when the read fails. */
|
|
35
|
+
const NO_PROVIDER: ProviderInfo = { name: '', connected: false, features: null }
|
|
36
|
+
|
|
37
|
+
const providerRead = useProviderInfo(() => props.showProvider)
|
|
38
|
+
const provider = computed<ProviderInfo | null>(() =>
|
|
39
|
+
providerRead.error.value ? NO_PROVIDER : providerRead.data.value,
|
|
40
|
+
)
|
|
36
41
|
const hidesPeriod = useSetupHidesPeriod(props.periodScope)
|
|
37
42
|
|
|
38
43
|
const presetLabels = computed(() =>
|
|
@@ -82,15 +87,6 @@ function renderPeriod() {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
usePageHeaderActions(() => [renderProvider(), renderPeriod()])
|
|
85
|
-
|
|
86
|
-
onMounted(async () => {
|
|
87
|
-
if (!props.showProvider) return
|
|
88
|
-
try {
|
|
89
|
-
provider.value = await api.provider()
|
|
90
|
-
} catch {
|
|
91
|
-
provider.value = { name: '', connected: false, features: null }
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
90
|
</script>
|
|
95
91
|
|
|
96
92
|
<template>
|
|
@@ -4,6 +4,7 @@ import { onBeforeUnmount } from 'vue'
|
|
|
4
4
|
import FormFieldRow from '../build/components/FormFieldRow.vue'
|
|
5
5
|
import FormRows from '../build/components/FormRows.vue'
|
|
6
6
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
7
|
+
import { useTemplateContentRead } from '../composables/useMailingReads'
|
|
7
8
|
import type { SendResult } from '../composables/useMailingApi'
|
|
8
9
|
import { useTemplateDrawer } from '../composables/useTemplateDrawer'
|
|
9
10
|
import { apiErrorMessage } from '../utils/api-error'
|
|
@@ -64,6 +65,7 @@ const { uniqueLocales } = useUniqueLocales()
|
|
|
64
65
|
const overlays = useTemplateDrawer()
|
|
65
66
|
|
|
66
67
|
const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
|
|
68
|
+
const readContent = useTemplateContentRead(templateId)
|
|
67
69
|
const detail = ref<TemplateContentResponse | null>(null)
|
|
68
70
|
const loading = ref(true)
|
|
69
71
|
const recipients = ref<string[]>([])
|
|
@@ -104,7 +106,7 @@ const missingRequired = computed(() =>
|
|
|
104
106
|
onMounted(async () => {
|
|
105
107
|
if (!templateId.value) return
|
|
106
108
|
try {
|
|
107
|
-
detail.value = await
|
|
109
|
+
detail.value = await readContent()
|
|
108
110
|
values.value = initialSendValues(fields.value, detail.value.testData)
|
|
109
111
|
locale.value = detail.value.fallbackLocale
|
|
110
112
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useSendHtmlRead, useSendRead } from '../composables/useMailingReads'
|
|
3
3
|
import type { SendHtmlResponse, SendRow } from '../types/mailing'
|
|
4
4
|
|
|
5
5
|
type RenderingView = 'desktop' | 'mobile' | 'html'
|
|
@@ -44,7 +44,6 @@ const FRAME_WIDTHS: Record<RenderingView, string> = {
|
|
|
44
44
|
html: '100%',
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const api = useMailingApi()
|
|
48
47
|
const { t, locale } = useI18n()
|
|
49
48
|
|
|
50
49
|
const view = ref<RenderingView>('desktop')
|
|
@@ -54,6 +53,8 @@ const loading = ref(false)
|
|
|
54
53
|
const failed = ref(false)
|
|
55
54
|
|
|
56
55
|
const sendIdValue = computed(() => props.rowData?._id ?? props.sendId ?? '')
|
|
56
|
+
const readSend = useSendRead(sendIdValue)
|
|
57
|
+
const readHtml = useSendHtmlRead(sendIdValue)
|
|
57
58
|
const key = (path: string, params: Record<string, unknown> = {}): string =>
|
|
58
59
|
t(`${KEY_PREFIX}${path}`, params)
|
|
59
60
|
|
|
@@ -85,7 +86,7 @@ const meta = computed(() => {
|
|
|
85
86
|
|
|
86
87
|
async function loadSend(): Promise<void> {
|
|
87
88
|
if (send.value) return
|
|
88
|
-
const detail = await
|
|
89
|
+
const detail = await readSend()
|
|
89
90
|
send.value = detail.send
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -94,10 +95,7 @@ async function load(): Promise<void> {
|
|
|
94
95
|
loading.value = true
|
|
95
96
|
failed.value = false
|
|
96
97
|
try {
|
|
97
|
-
const [html] = await Promise.all([
|
|
98
|
-
api.sendHtml(sendIdValue.value),
|
|
99
|
-
loadSend(),
|
|
100
|
-
])
|
|
98
|
+
const [html] = await Promise.all([readHtml(), loadSend()])
|
|
101
99
|
rendering.value = html
|
|
102
100
|
} catch (error) {
|
|
103
101
|
failed.value = true
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { refDebounced } from '@vueuse/core'
|
|
3
|
+
import { useRetentionPreview } from '../composables/useMailingReads'
|
|
4
4
|
import {
|
|
5
5
|
clampRetention,
|
|
6
|
-
|
|
6
|
+
CLOCK_FORMAT,
|
|
7
7
|
isWithinADay,
|
|
8
8
|
MAX_RETENTION_DAYS,
|
|
9
9
|
MIN_RETENTION_DAYS,
|
|
@@ -35,8 +35,8 @@ const DATE_FORMAT: Intl.DateTimeFormatOptions = {
|
|
|
35
35
|
day: 'numeric',
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const api = useMailingApi()
|
|
39
38
|
const { t, locale } = useI18n()
|
|
39
|
+
const { dateTimeFormat } = useRegionalFormat()
|
|
40
40
|
const { confirm } = useConfirm()
|
|
41
41
|
const form = useEnclosingForm()
|
|
42
42
|
|
|
@@ -56,7 +56,13 @@ const isCustom = computed(
|
|
|
56
56
|
isCustomPicked.value ||
|
|
57
57
|
(days.value !== null && retentionChoice(days.value) === RETENTION_CUSTOM),
|
|
58
58
|
)
|
|
59
|
-
|
|
59
|
+
/** The days the preview reads, once the choice holds still. */
|
|
60
|
+
const previewDays = refDebounced(days, PREVIEW_DEBOUNCE_MS)
|
|
61
|
+
const previewRead = useRetentionPreview(previewDays)
|
|
62
|
+
/** A failed read shows no preview. */
|
|
63
|
+
const preview = computed<RetentionPreview | null>(() =>
|
|
64
|
+
previewRead.error.value ? null : previewRead.data.value,
|
|
65
|
+
)
|
|
60
66
|
|
|
61
67
|
const key = (
|
|
62
68
|
path: string,
|
|
@@ -99,8 +105,8 @@ function previewParams(value: RetentionPreview): Record<string, unknown> {
|
|
|
99
105
|
return {
|
|
100
106
|
when: isWithinADay(value.nextRunAt, new Date())
|
|
101
107
|
? key('tonight')
|
|
102
|
-
: new Date(value.nextRunAt)
|
|
103
|
-
time:
|
|
108
|
+
: dateTimeFormat(DATE_FORMAT).format(new Date(value.nextRunAt)),
|
|
109
|
+
time: dateTimeFormat(CLOCK_FORMAT).format(new Date(value.nextRunAt)),
|
|
104
110
|
count: new Intl.NumberFormat(locale.value).format(value.count),
|
|
105
111
|
duration: key('days', { count: value.days }, value.days),
|
|
106
112
|
}
|
|
@@ -148,22 +154,6 @@ function guardSubmit(): boolean {
|
|
|
148
154
|
return false
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
const loadPreview = useDebounceFn(async (value: number) => {
|
|
152
|
-
try {
|
|
153
|
-
preview.value = await api.retentionPreview(value)
|
|
154
|
-
} catch {
|
|
155
|
-
preview.value = null
|
|
156
|
-
}
|
|
157
|
-
}, PREVIEW_DEBOUNCE_MS)
|
|
158
|
-
|
|
159
|
-
watch(
|
|
160
|
-
days,
|
|
161
|
-
(value) => {
|
|
162
|
-
if (value) void loadPreview(value)
|
|
163
|
-
},
|
|
164
|
-
{ immediate: true },
|
|
165
|
-
)
|
|
166
|
-
|
|
167
157
|
watch(
|
|
168
158
|
() => props.modelValue,
|
|
169
159
|
(value) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { TEMPLATES_PATH } from '../utils/pages'
|
|
3
3
|
import { useClipboard } from '@vueuse/core'
|
|
4
4
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
5
|
+
import { useProviderRead, useSendRead } from '../composables/useMailingReads'
|
|
5
6
|
import { useReplayToast } from '../composables/useReplayToast'
|
|
6
7
|
import {
|
|
7
8
|
describeProblem,
|
|
@@ -90,6 +91,8 @@ const { copy } = useClipboard()
|
|
|
90
91
|
const KeyValueList = resolveComponent('DmsKeyValueList') as Component
|
|
91
92
|
|
|
92
93
|
const sendId = computed(() => props.rowData?._id ?? props.sendId ?? '')
|
|
94
|
+
const readSend = useSendRead(sendId)
|
|
95
|
+
const readProvider = useProviderRead()
|
|
93
96
|
const detail = ref<SendDetailResponse | null>(null)
|
|
94
97
|
const tracking = ref<TimelineTracking | null>(null)
|
|
95
98
|
const loading = ref(false)
|
|
@@ -231,7 +234,7 @@ async function load(): Promise<void> {
|
|
|
231
234
|
if (!sendId.value) return
|
|
232
235
|
loading.value = true
|
|
233
236
|
try {
|
|
234
|
-
detail.value = await
|
|
237
|
+
detail.value = await readSend()
|
|
235
238
|
} catch (error) {
|
|
236
239
|
useApiError(error)
|
|
237
240
|
} finally {
|
|
@@ -242,7 +245,7 @@ async function load(): Promise<void> {
|
|
|
242
245
|
/** The provider's tracking, so the timeline waits only for what it reports. */
|
|
243
246
|
async function loadTracking(): Promise<void> {
|
|
244
247
|
try {
|
|
245
|
-
const provider = await
|
|
248
|
+
const provider = await readProvider()
|
|
246
249
|
tracking.value = provider.features
|
|
247
250
|
? {
|
|
248
251
|
openTracking: !!provider.features.openTracking,
|
|
@@ -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
|
|
28
|
-
|
|
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 =
|
|
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 {
|
|
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(
|
|
89
|
-
|
|
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(
|
|
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
|
-
|
|
133
|
-
|
|
138
|
+
readContent(),
|
|
139
|
+
readPerformance().catch(() => null),
|
|
134
140
|
])
|
|
135
141
|
detail.value = content
|
|
136
142
|
performance.value = stats
|