@antelopejs/dms-mailing 0.6.1 → 0.7.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 +8 -7
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +2 -3
- package/dist/constants.js.map +1 -1
- package/dist/data/templates-table.d.ts +4 -1
- package/dist/data/templates-table.js +55 -20
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +5 -5
- package/dist/db/models/index.d.ts +0 -1
- package/dist/db/models/index.js +0 -1
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/send-events.model.js +2 -3
- package/dist/db/models/send-events.model.js.map +1 -1
- package/dist/db/models/sends.model.js +2 -3
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +10 -1
- package/dist/db/models/templates.model.js +29 -10
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +0 -1
- package/dist/db/tables/index.js +0 -1
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/settings.table.d.ts +0 -1
- package/dist/db/tables/settings.table.js +0 -4
- package/dist/db/tables/settings.table.js.map +1 -1
- package/dist/db/tables/templates.table.d.ts +7 -4
- package/dist/db/tables/templates.table.js +4 -14
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/formats.d.ts +13 -0
- package/dist/engine/formats.js +85 -0
- package/dist/engine/formats.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +14 -7
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +3 -5
- package/dist/engine/tokens.js.map +1 -1
- package/dist/frontend-module.d.ts +2 -0
- package/dist/frontend-module.js +24 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/tenant-export.js +4 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +3 -3
- package/dist/implementations/dms-mailing/index.js +8 -7
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +2 -20
- package/dist/index.js.map +1 -1
- package/dist/pages/overview.d.ts +1 -1
- package/dist/pages/overview.js +11 -13
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +1 -1
- package/dist/pages/sends.js +16 -8
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +8 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/templates.js +3 -8
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.d.ts +12 -0
- package/dist/pages/toolbar.js +41 -0
- package/dist/pages/toolbar.js.map +1 -0
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js +8 -8
- package/dist/realtime.js.map +1 -1
- package/dist/routes/metrics.d.ts +9 -2
- package/dist/routes/metrics.js +90 -35
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +1 -0
- package/dist/routes/period.js +19 -0
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +6 -3
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -7
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.d.ts +8 -4
- package/dist/routes/templates.js +41 -20
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/categories.d.ts +2 -1
- package/dist/services/categories.js +9 -0
- package/dist/services/categories.js.map +1 -1
- package/dist/services/declarations.js +42 -184
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +1 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +7 -0
- package/dist/services/localized-text.js +28 -0
- package/dist/services/localized-text.js.map +1 -0
- package/dist/services/send-import.js +0 -2
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send-stat-items.js +34 -12
- package/dist/services/send-stat-items.js.map +1 -1
- package/dist/services/send-stats.d.ts +7 -0
- package/dist/services/send-stats.js +17 -5
- package/dist/services/send-stats.js.map +1 -1
- package/dist/services/send.d.ts +2 -0
- package/dist/services/send.js +11 -12
- package/dist/services/send.js.map +1 -1
- package/dist/services/sender-declaration.d.ts +2 -0
- package/dist/services/sender-declaration.js +18 -0
- package/dist/services/sender-declaration.js.map +1 -0
- package/dist/services/settings.d.ts +1 -1
- package/dist/services/settings.js +1 -5
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +2 -2
- package/dist/services/starters.js +1 -10
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +13 -7
- package/dist/services/templates.js +72 -33
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -1
- package/dist/services/webhook-status.js +6 -5
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/settings.d.ts +8 -0
- package/dist/types/settings.js +33 -6
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +25 -7
- package/dist/validation/declarations.schema.js +16 -3
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/settings.schema.d.ts +41 -41
- package/dist/validation/settings.schema.js +30 -6
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +13 -0
- package/dist/validation/templates.schema.js +6 -1
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +15 -13
- package/frontend-vue/app/components/FunnelCard.vue +46 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
- package/frontend-vue/app/components/OverviewGate.vue +0 -8
- package/frontend-vue/app/components/ProviderPill.vue +50 -0
- package/frontend-vue/app/components/SendDrawer.vue +206 -183
- package/frontend-vue/app/components/StarterGallery.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +4 -1
- package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
- package/frontend-vue/app/components/TemplateGallery.vue +57 -17
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
- package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
- package/frontend-vue/app/composables/useMailingApi.ts +3 -3
- package/frontend-vue/app/composables/useMailingReads.ts +2 -2
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
- package/frontend-vue/app/types/mailing.ts +18 -0
- package/frontend-vue/app/utils/categories.ts +58 -49
- package/frontend-vue/app/utils/funnel.ts +25 -0
- package/frontend-vue/app/utils/period.ts +0 -7
- package/frontend-vue/app/utils/starters.ts +3 -1
- package/frontend-vue/app/utils/tokens.ts +6 -1
- package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +4 -10
- package/frontend-vue/tests/categories.test.ts +55 -17
- package/frontend-vue/tests/conditions.test.ts +1 -4
- package/frontend-vue/tests/funnel-card.test.ts +27 -0
- package/frontend-vue/tests/funnel.test.ts +25 -0
- package/frontend-vue/tests/localized-defaults.test.ts +0 -29
- package/frontend-vue/tests/new-template-modal.test.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -24
- package/frontend-vue/tests/page-reads.test.ts +9 -25
- package/frontend-vue/tests/period.test.ts +0 -12
- package/frontend-vue/tests/replay-toast.test.ts +2 -1
- package/frontend-vue/tests/send-drawer.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +1 -1
- package/frontend-vue/tests/template-gallery.test.ts +90 -3
- package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
- package/package.json +8 -7
- package/skills/dms-mailing/SKILL.md +25 -18
- package/src/constants.ts +2 -2
- package/src/data/sends-table.ts +1 -1
- package/src/data/templates-table.ts +125 -33
- package/src/db/models/index.ts +0 -1
- package/src/db/models/send-events.model.ts +2 -2
- package/src/db/models/sends.model.ts +7 -8
- package/src/db/models/templates.model.ts +64 -16
- package/src/db/tables/index.ts +0 -1
- package/src/db/tables/settings.table.ts +0 -8
- package/src/db/tables/templates.table.ts +12 -17
- package/src/engine/formats.ts +124 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/resolve.ts +18 -7
- package/src/engine/tokens.ts +7 -5
- package/src/frontend-module.ts +36 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/tenant-export.ts +8 -1
- package/src/implementations/dms-mailing/index.ts +5 -16
- package/src/index.ts +3 -29
- package/src/pages/overview.ts +20 -16
- package/src/pages/sends.ts +27 -11
- package/src/pages/settings/form.ts +16 -0
- package/src/pages/templates.ts +4 -8
- package/src/pages/toolbar.ts +77 -0
- package/src/realtime.ts +17 -8
- package/src/routes/metrics.ts +158 -45
- package/src/routes/period.ts +32 -0
- package/src/routes/sends.ts +13 -4
- package/src/routes/settings.ts +11 -8
- package/src/routes/templates.ts +81 -37
- package/src/services/categories.ts +14 -1
- package/src/services/declarations.ts +64 -317
- package/src/services/events.ts +1 -1
- package/src/services/localized-text.ts +53 -0
- package/src/services/send-import.ts +2 -2
- package/src/services/send-stat-items.ts +60 -13
- package/src/services/send-stats.ts +37 -5
- package/src/services/send.ts +19 -20
- package/src/services/sender-declaration.ts +27 -0
- package/src/services/settings.ts +3 -10
- package/src/services/starters.ts +3 -15
- package/src/services/templates.ts +109 -63
- package/src/services/webhook-status.ts +3 -3
- package/src/types/settings.ts +55 -5
- package/src/validation/declarations.schema.ts +29 -3
- package/src/validation/settings.schema.ts +56 -14
- package/src/validation/templates.schema.ts +10 -0
- package/dist/db/models/template-slugs.model.d.ts +0 -27
- package/dist/db/models/template-slugs.model.js +0 -43
- package/dist/db/models/template-slugs.model.js.map +0 -1
- package/dist/db/tables/template-slugs.table.d.ts +0 -7
- package/dist/db/tables/template-slugs.table.js +0 -35
- package/dist/db/tables/template-slugs.table.js.map +0 -1
- package/dist/hooks/database-ready.d.ts +0 -7
- package/dist/hooks/database-ready.js +0 -26
- package/dist/hooks/database-ready.js.map +0 -1
- package/dist/services/fingerprint.d.ts +0 -1
- package/dist/services/fingerprint.js +0 -20
- package/dist/services/fingerprint.js.map +0 -1
- package/frontend-vue/app/components/PageHeader.vue +0 -94
- package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
- package/src/db/models/template-slugs.model.ts +0 -61
- package/src/db/tables/template-slugs.table.ts +0 -29
- package/src/hooks/database-ready.ts +0 -47
- package/src/services/fingerprint.ts +0 -22
|
@@ -27,7 +27,12 @@ interface TemplatePreviewFrameProps {
|
|
|
27
27
|
width: number
|
|
28
28
|
scale?: number
|
|
29
29
|
refreshKey?: number
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Fetches only once the frame scrolls into view (galleries). The frame then
|
|
32
|
+
* waits on a blank sheet rather than a skeleton: the gallery's own skeleton
|
|
33
|
+
* covers its first load, and a thumbnail starting to load as it scrolls into
|
|
34
|
+
* view would bring a skeleton back.
|
|
35
|
+
*/
|
|
31
36
|
lazy?: boolean
|
|
32
37
|
/** Accessible name of the frame. */
|
|
33
38
|
label?: string
|
|
@@ -61,14 +66,15 @@ const props = withDefaults(defineProps<TemplatePreviewFrameProps>(), {
|
|
|
61
66
|
const emit = defineEmits<{ resolved: [PreviewResponse] }>()
|
|
62
67
|
|
|
63
68
|
const api = useMailingApi()
|
|
64
|
-
const { locale: uiLocale } = useI18n()
|
|
69
|
+
const { t, locale: uiLocale } = useI18n()
|
|
65
70
|
|
|
66
71
|
const readStarterPreview = useStarterPreviewRead(
|
|
67
72
|
() => props.starterId,
|
|
68
73
|
() => props.locale ?? uiLocale.value,
|
|
69
74
|
)
|
|
70
75
|
const fetchedHtml = ref('')
|
|
71
|
-
|
|
76
|
+
/** The last read failed: the frame shows its fallback, not a skeleton for ever. */
|
|
77
|
+
const hasFailed = ref(false)
|
|
72
78
|
const height = ref(FALLBACK_HEIGHT)
|
|
73
79
|
const frame = ref<HTMLIFrameElement | null>(null)
|
|
74
80
|
const box = ref<HTMLElement | null>(null)
|
|
@@ -87,6 +93,19 @@ function withoutScroll(html: string): string {
|
|
|
87
93
|
|
|
88
94
|
const shownHtml = computed(() => withoutScroll(props.html ?? fetchedHtml.value))
|
|
89
95
|
|
|
96
|
+
const hasSource = computed(
|
|
97
|
+
() =>
|
|
98
|
+
props.html !== undefined || Boolean(props.starterId || props.templateId),
|
|
99
|
+
)
|
|
100
|
+
/**
|
|
101
|
+
* Waiting for the HTML: from the first render (a server render, the debounce
|
|
102
|
+
* and a lazy frame not yet in view included), so the placeholder holds its
|
|
103
|
+
* place without a gap between it and the content.
|
|
104
|
+
*/
|
|
105
|
+
const isWaiting = computed(
|
|
106
|
+
() => !shownHtml.value && hasSource.value && !hasFailed.value,
|
|
107
|
+
)
|
|
108
|
+
|
|
90
109
|
useIntersectionObserver(box, ([entry]) => {
|
|
91
110
|
if (entry?.isIntersecting) isVisible.value = true
|
|
92
111
|
})
|
|
@@ -110,15 +129,14 @@ async function fetchTemplate(templateId: string): Promise<void> {
|
|
|
110
129
|
async function load(): Promise<void> {
|
|
111
130
|
if (props.html !== undefined || !isVisible.value) return
|
|
112
131
|
if (!props.starterId && !props.templateId) return
|
|
113
|
-
loading.value = true
|
|
114
132
|
try {
|
|
115
133
|
await (props.starterId
|
|
116
134
|
? fetchStarter()
|
|
117
135
|
: fetchTemplate(props.templateId as string))
|
|
136
|
+
hasFailed.value = false
|
|
118
137
|
} catch {
|
|
119
138
|
fetchedHtml.value = ''
|
|
120
|
-
|
|
121
|
-
loading.value = false
|
|
139
|
+
hasFailed.value = true
|
|
122
140
|
}
|
|
123
141
|
}
|
|
124
142
|
|
|
@@ -172,12 +190,10 @@ const frameStyle = computed(() => ({
|
|
|
172
190
|
|
|
173
191
|
<template>
|
|
174
192
|
<div ref="box" class="relative overflow-hidden" :style="boxStyle">
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class="absolute inset-0 size-full"
|
|
178
|
-
/>
|
|
193
|
+
<!-- Rendered only with HTML: an empty frame loads too, and measuring it
|
|
194
|
+
would shrink the box to a few pixels. -->
|
|
179
195
|
<iframe
|
|
180
|
-
v-
|
|
196
|
+
v-if="shownHtml"
|
|
181
197
|
ref="frame"
|
|
182
198
|
:srcdoc="shownHtml"
|
|
183
199
|
sandbox="allow-same-origin"
|
|
@@ -191,5 +207,19 @@ const frameStyle = computed(() => ({
|
|
|
191
207
|
:style="frameStyle"
|
|
192
208
|
@load="measure"
|
|
193
209
|
/>
|
|
210
|
+
<div
|
|
211
|
+
v-else-if="isWaiting && lazy"
|
|
212
|
+
class="absolute inset-0 bg-white"
|
|
213
|
+
data-preview-placeholder
|
|
214
|
+
/>
|
|
215
|
+
<USkeleton v-else-if="isWaiting" class="absolute inset-0 size-full" />
|
|
216
|
+
<div
|
|
217
|
+
v-else-if="hasFailed"
|
|
218
|
+
class="bg-elevated text-dimmed absolute inset-0 flex flex-col items-center justify-center gap-1.5 text-[12px]"
|
|
219
|
+
data-preview-fallback
|
|
220
|
+
>
|
|
221
|
+
<UIcon name="i-ph-image-broken" class="size-5" aria-hidden="true" />
|
|
222
|
+
{{ t('dms_mailing.common.preview_unavailable') }}
|
|
223
|
+
</div>
|
|
194
224
|
</div>
|
|
195
225
|
</template>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import type { TemplateDetails } from '../../composables/useMailingApi'
|
|
4
4
|
import { useEditorContext } from '../../composables/useEditorContext'
|
|
5
|
-
import { useLocalizedDefaults } from '../../composables/useLocalizedDefaults'
|
|
6
5
|
|
|
7
6
|
const KEY = 'dms_mailing.editor.template_tab'
|
|
8
7
|
const NO_CATEGORY = '__none__'
|
|
@@ -10,7 +9,6 @@ const NO_CATEGORY = '__none__'
|
|
|
10
9
|
const { t } = useI18n()
|
|
11
10
|
const toast = useToast()
|
|
12
11
|
const { editor, session, actions, templateId } = useEditorContext()
|
|
13
|
-
const localized = useLocalizedDefaults()
|
|
14
12
|
|
|
15
13
|
const template = computed(() => session.state.template)
|
|
16
14
|
const name = ref(template.value.name)
|
|
@@ -22,7 +20,7 @@ watch(
|
|
|
22
20
|
|
|
23
21
|
const categoryItems = computed(() => [
|
|
24
22
|
{ value: NO_CATEGORY, label: t(`${KEY}.no_category`), icon: 'i-ph-minus' },
|
|
25
|
-
...
|
|
23
|
+
...session.state.categories.map((category) => ({
|
|
26
24
|
value: category.id,
|
|
27
25
|
label: category.label,
|
|
28
26
|
icon: category.icon,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useEditorContext } from '../../composables/useEditorContext'
|
|
4
|
-
import { useLocalizedDefaults } from '../../composables/useLocalizedDefaults'
|
|
5
4
|
import { TEMPLATES_PATH } from '../../utils/pages'
|
|
6
5
|
import type { StatusKey } from '../../utils/lifecycle'
|
|
7
6
|
|
|
@@ -14,7 +13,6 @@ const STATUS_TONES: Record<StatusKey, string> = {
|
|
|
14
13
|
const SEPARATOR = ' · '
|
|
15
14
|
|
|
16
15
|
const { t } = useI18n()
|
|
17
|
-
const localized = useLocalizedDefaults()
|
|
18
16
|
const { editor, session } = useEditorContext()
|
|
19
17
|
|
|
20
18
|
const template = computed(() => session.state.template)
|
|
@@ -38,11 +36,7 @@ const statusLabel = computed(() => {
|
|
|
38
36
|
const categoryLabel = computed(() => {
|
|
39
37
|
const id = template.value.category
|
|
40
38
|
if (!id) return t('dms_mailing.editor.toolbar.uncategorised')
|
|
41
|
-
return (
|
|
42
|
-
localized
|
|
43
|
-
.categories(session.state.categories)
|
|
44
|
-
.find((entry) => entry.id === id)?.label ?? id
|
|
45
|
-
)
|
|
39
|
+
return session.state.categories.find((entry) => entry.id === id)?.label ?? id
|
|
46
40
|
})
|
|
47
41
|
</script>
|
|
48
42
|
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import type { StarterSummary
|
|
2
|
-
import {
|
|
3
|
-
import { localizeStarters } from '../utils/starters'
|
|
1
|
+
import type { StarterSummary } from '../types/mailing'
|
|
2
|
+
import { localizeStarters, type OptionalTranslate } from '../utils/starters'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The starters the module ships, named in the interface's language. Template
|
|
6
|
+
* names and category labels are localized fields the server reads in the
|
|
7
|
+
* interface's language.
|
|
8
8
|
*/
|
|
9
9
|
export function useLocalizedDefaults() {
|
|
10
10
|
const { t, te } = useI18n()
|
|
11
11
|
const translate: OptionalTranslate = (key) => (te(key) ? t(key) : undefined)
|
|
12
12
|
return {
|
|
13
|
-
categories: (list: TemplateCategory[]) =>
|
|
14
|
-
localizeCategories(list, translate),
|
|
15
13
|
starters: (list: StarterSummary[]) => localizeStarters(list, translate),
|
|
16
14
|
}
|
|
17
15
|
}
|
|
@@ -192,14 +192,14 @@ export function useMailingApi() {
|
|
|
192
192
|
return {
|
|
193
193
|
webhookTest: () =>
|
|
194
194
|
post<WebhookTestResult>(mailingPath('settings', 'webhook-test'), {}),
|
|
195
|
+
/** Renames in the interface's language, the other languages kept. */
|
|
195
196
|
saveTemplateDetails: (id: string, details: TemplateDetails) =>
|
|
196
|
-
$authFetch<unknown>(mailingPath('
|
|
197
|
+
$authFetch<unknown>(mailingPath('templates', id, 'details'), {
|
|
197
198
|
method: 'PUT',
|
|
198
|
-
query: { id },
|
|
199
199
|
body: details,
|
|
200
200
|
}),
|
|
201
201
|
createTemplate: (input: CreateTemplateInput) =>
|
|
202
|
-
post<
|
|
202
|
+
post<TemplateRow>(mailingPath('tables', 'templates', 'new'), input),
|
|
203
203
|
saveContent: (id: string, input: SaveContentInput) =>
|
|
204
204
|
$authFetch<SavedResponse>(mailingPath('templates', id, 'content'), {
|
|
205
205
|
method: 'POST',
|
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
* `useMailingApi`.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
/** The e-mail provider the page
|
|
33
|
+
/** The e-mail provider the page toolbar names; nothing read while `enabled` is false. */
|
|
34
34
|
export function useProviderInfo(
|
|
35
35
|
enabled: MaybeRefOrGetter<boolean>,
|
|
36
36
|
): ApiDataResult<ProviderInfo> {
|
|
@@ -66,7 +66,7 @@ export function useCategoryUsage(): ApiDataResult<CategoryUsageResponse> {
|
|
|
66
66
|
)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** The tenant's template categories,
|
|
69
|
+
/** The tenant's template categories, labelled in the interface's language. */
|
|
70
70
|
export function useStoredCategories(): ApiDataResult<CategoriesResponse> {
|
|
71
71
|
return useApiData<CategoriesResponse>(mailingPath('templates', 'categories'))
|
|
72
72
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { useLocalizedDefaults } from './useLocalizedDefaults'
|
|
2
1
|
import { useStoredCategories } from './useMailingReads'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* The tenant's template categories,
|
|
6
|
-
* read the components showing them share (a table's category
|
|
7
|
-
* gallery), none when it failed.
|
|
4
|
+
* The tenant's template categories, labelled in the interface's language by
|
|
5
|
+
* the server: one read the components showing them share (a table's category
|
|
6
|
+
* cells, the gallery), none when it failed.
|
|
7
|
+
* `isAnswered` turns true once the read has answered, with the categories or
|
|
8
|
+
* a failure.
|
|
8
9
|
*/
|
|
9
10
|
export function useTemplateCategories() {
|
|
10
|
-
const localized = useLocalizedDefaults()
|
|
11
11
|
const { data, error } = useStoredCategories()
|
|
12
12
|
const categories = computed(() =>
|
|
13
|
-
|
|
13
|
+
error.value ? [] : (data.value?.categories ?? []),
|
|
14
14
|
)
|
|
15
|
-
|
|
15
|
+
const isAnswered = computed(() => Boolean(data.value || error.value))
|
|
16
|
+
return { categories, isAnswered }
|
|
16
17
|
}
|
|
@@ -271,12 +271,26 @@ export const SEND_STATUSES: SendStatus[] = [
|
|
|
271
271
|
/** What a provider reports: a suppressed send never reached one. */
|
|
272
272
|
export type SendEventType = Exclude<SendStatus, 'suppressed'>
|
|
273
273
|
|
|
274
|
+
/** A text written once per language: `{ en: 'Orders', fr: 'Commandes' }`. */
|
|
275
|
+
export type LocalizedText = Record<string, string>
|
|
276
|
+
|
|
277
|
+
/** A template category as one reader sees it: its label in their language. */
|
|
274
278
|
export interface TemplateCategory {
|
|
275
279
|
id: string
|
|
276
280
|
label: string
|
|
277
281
|
icon: string
|
|
278
282
|
}
|
|
279
283
|
|
|
284
|
+
/**
|
|
285
|
+
* A template category as the settings store it, read and written whole by
|
|
286
|
+
* the settings form: its label in every language.
|
|
287
|
+
*/
|
|
288
|
+
export interface StoredCategory {
|
|
289
|
+
id: string
|
|
290
|
+
label: LocalizedText
|
|
291
|
+
icon: string
|
|
292
|
+
}
|
|
293
|
+
|
|
280
294
|
export interface TemplateRow {
|
|
281
295
|
_id: string
|
|
282
296
|
slug: string
|
|
@@ -521,6 +535,10 @@ export interface FunnelStep {
|
|
|
521
535
|
rate: number | null
|
|
522
536
|
/** `false` when nothing reports the events this step counts. */
|
|
523
537
|
isMeasured?: boolean
|
|
538
|
+
/** The count over the comparison window; absent with the comparison off. */
|
|
539
|
+
previousCount?: number
|
|
540
|
+
/** The change of the count, in percent; absent from an empty comparison. */
|
|
541
|
+
delta?: number
|
|
524
542
|
}
|
|
525
543
|
|
|
526
544
|
export interface ProviderInfo {
|
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
LocalizedText,
|
|
3
|
+
StoredCategory,
|
|
4
|
+
TemplateCategory,
|
|
5
|
+
} from '../types/mailing'
|
|
2
6
|
|
|
3
7
|
const ID_SEPARATOR = '-'
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
/** The language a localized label falls back to, as the server reads it. */
|
|
10
|
+
export const FALLBACK_LOCALE = 'en'
|
|
11
|
+
|
|
12
|
+
/** A label in `locale`, else in the fallback language. */
|
|
13
|
+
export function labelIn(label: LocalizedText, locale: string): string {
|
|
14
|
+
return label[locale] ?? label[FALLBACK_LOCALE] ?? ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `label` with `value` written in `locale`: the other languages are kept,
|
|
19
|
+
* and the fallback one gets the value while it has none, so a category
|
|
20
|
+
* added in another language still reads in every language.
|
|
21
|
+
*/
|
|
22
|
+
export function withLabelIn(
|
|
23
|
+
label: LocalizedText,
|
|
24
|
+
locale: string,
|
|
25
|
+
value: string,
|
|
26
|
+
): LocalizedText {
|
|
27
|
+
return { [FALLBACK_LOCALE]: value, ...label, [locale]: value }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function serializeCategories(categories: StoredCategory[]): string {
|
|
6
31
|
return JSON.stringify(categories)
|
|
7
32
|
}
|
|
8
33
|
|
|
@@ -34,7 +59,9 @@ const ICON_PREFIX = 'i-ph-'
|
|
|
34
59
|
* The icon a new category starts with: the first one of the picker no
|
|
35
60
|
* category uses yet, so a new row never looks like an existing one.
|
|
36
61
|
*/
|
|
37
|
-
export function newCategoryIcon(
|
|
62
|
+
export function newCategoryIcon(
|
|
63
|
+
categories: Pick<TemplateCategory, 'icon'>[],
|
|
64
|
+
): string {
|
|
38
65
|
const used = new Set(categories.map((category) => category.icon))
|
|
39
66
|
return CATEGORY_ICONS.find((icon) => !used.has(icon)) ?? DEFAULT_CATEGORY_ICON
|
|
40
67
|
}
|
|
@@ -47,12 +74,30 @@ export function iconName(icon: string): string {
|
|
|
47
74
|
}
|
|
48
75
|
|
|
49
76
|
/**
|
|
50
|
-
* A row of the categories editor
|
|
51
|
-
*
|
|
77
|
+
* A row of the categories editor: `label` is the text of the language being
|
|
78
|
+
* edited, `labels` what the row stores in every language. `isNew` rows were
|
|
79
|
+
* added in this edit: their id follows the label until the form is saved,
|
|
80
|
+
* then stays fixed for good.
|
|
52
81
|
*/
|
|
53
82
|
export interface EditableCategory extends TemplateCategory {
|
|
54
83
|
key: string
|
|
55
84
|
isNew: boolean
|
|
85
|
+
labels: LocalizedText
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The rows of `categories`, each label shown in `locale`. */
|
|
89
|
+
export function toEditableRows(
|
|
90
|
+
categories: StoredCategory[],
|
|
91
|
+
locale: string,
|
|
92
|
+
keyOf: () => string,
|
|
93
|
+
): EditableCategory[] {
|
|
94
|
+
return categories.map((category) => ({
|
|
95
|
+
...category,
|
|
96
|
+
label: labelIn(category.label, locale),
|
|
97
|
+
labels: category.label,
|
|
98
|
+
key: keyOf(),
|
|
99
|
+
isNew: false,
|
|
100
|
+
}))
|
|
56
101
|
}
|
|
57
102
|
|
|
58
103
|
const UNIQUE_SUFFIX_START = 2
|
|
@@ -69,11 +114,13 @@ export function uniqueId(base: string, taken: Set<string>): string {
|
|
|
69
114
|
/**
|
|
70
115
|
* The categories an edit stores, one per row in its order: saved rows keep
|
|
71
116
|
* their id, new rows get one derived from their label, unique among the
|
|
72
|
-
* others.
|
|
117
|
+
* others. Each label is written in `locale`, its other languages kept. A row
|
|
118
|
+
* without a label is sent too, for the server to name it.
|
|
73
119
|
*/
|
|
74
120
|
export function toStoredCategories(
|
|
75
121
|
rows: EditableCategory[],
|
|
76
|
-
|
|
122
|
+
locale: string,
|
|
123
|
+
): StoredCategory[] {
|
|
77
124
|
const taken = new Set(rows.filter((row) => !row.isNew).map((row) => row.id))
|
|
78
125
|
return rows.map((row) => {
|
|
79
126
|
const id = row.isNew
|
|
@@ -82,7 +129,7 @@ export function toStoredCategories(
|
|
|
82
129
|
taken.add(id)
|
|
83
130
|
return {
|
|
84
131
|
id,
|
|
85
|
-
label: row.label.trim(),
|
|
132
|
+
label: withLabelIn(row.labels, locale, row.label.trim()),
|
|
86
133
|
icon: row.icon || DEFAULT_CATEGORY_ICON,
|
|
87
134
|
}
|
|
88
135
|
})
|
|
@@ -92,12 +139,12 @@ export function toStoredCategories(
|
|
|
92
139
|
* A category as the editor hands it to the form: a kept one, or one removed
|
|
93
140
|
* whose templates move to `moveTo` once the form is saved.
|
|
94
141
|
*/
|
|
95
|
-
export interface CategoryEntry extends
|
|
142
|
+
export interface CategoryEntry extends StoredCategory {
|
|
96
143
|
moveTo?: string
|
|
97
144
|
}
|
|
98
145
|
|
|
99
146
|
/** The kept categories of a list, without the removed ones it moves away. */
|
|
100
|
-
export function keptCategories(entries: CategoryEntry[]):
|
|
147
|
+
export function keptCategories(entries: CategoryEntry[]): StoredCategory[] {
|
|
101
148
|
return entries.filter((entry) => entry.moveTo === undefined)
|
|
102
149
|
}
|
|
103
150
|
|
|
@@ -115,7 +162,7 @@ export function categoryMoves(entries: CategoryEntry[]): Map<string, string> {
|
|
|
115
162
|
* whose templates move elsewhere, still under its own id.
|
|
116
163
|
*/
|
|
117
164
|
export function withMoves(
|
|
118
|
-
categories:
|
|
165
|
+
categories: StoredCategory[],
|
|
119
166
|
removed: CategoryEntry[],
|
|
120
167
|
): CategoryEntry[] {
|
|
121
168
|
return [...categories, ...removed.filter((entry) => entry.moveTo)]
|
|
@@ -200,41 +247,3 @@ export function toStoredCategory(
|
|
|
200
247
|
): string | null {
|
|
201
248
|
return value || null
|
|
202
249
|
}
|
|
203
|
-
|
|
204
|
-
/** Answers the text of an i18n key, or nothing when the catalog lacks it. */
|
|
205
|
-
export type OptionalTranslate = (key: string) => string | undefined
|
|
206
|
-
|
|
207
|
-
/** The labels the backend's `DEFAULT_CATEGORIES` (`src/types/settings.ts`) ship with. */
|
|
208
|
-
const DEFAULT_CATEGORY_LABELS: Record<string, string> = {
|
|
209
|
-
orders: 'Orders',
|
|
210
|
-
account: 'Account & access',
|
|
211
|
-
billing: 'Billing',
|
|
212
|
-
marketing: 'Marketing',
|
|
213
|
-
forms: 'Forms',
|
|
214
|
-
}
|
|
215
|
-
const DEFAULT_CATEGORY_KEY_PREFIX = 'dms_mailing.categories.defaults'
|
|
216
|
-
|
|
217
|
-
/** The i18n key of a shipped category nobody renamed; absent otherwise. */
|
|
218
|
-
export function defaultCategoryLabelKey(
|
|
219
|
-
category: TemplateCategory,
|
|
220
|
-
): string | undefined {
|
|
221
|
-
return DEFAULT_CATEGORY_LABELS[category.id] === category.label
|
|
222
|
-
? `${DEFAULT_CATEGORY_KEY_PREFIX}.${category.id}`
|
|
223
|
-
: undefined
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* The categories as the interface shows them: a shipped one still under its
|
|
228
|
-
* original English label reads in the interface's language, a label someone
|
|
229
|
-
* wrote stays as written. Nothing stored changes.
|
|
230
|
-
*/
|
|
231
|
-
export function localizeCategories(
|
|
232
|
-
categories: TemplateCategory[],
|
|
233
|
-
translate: OptionalTranslate,
|
|
234
|
-
): TemplateCategory[] {
|
|
235
|
-
return categories.map((category) => {
|
|
236
|
-
const key = defaultCategoryLabelKey(category)
|
|
237
|
-
const label = key ? translate(key) : undefined
|
|
238
|
-
return label ? { ...category, label } : category
|
|
239
|
-
})
|
|
240
|
-
}
|
|
@@ -72,3 +72,28 @@ export function formatRate(rate: number | null, locale: string): string {
|
|
|
72
72
|
maximumFractionDigits: RATE_DECIMALS,
|
|
73
73
|
}).format(rate / PERCENT)
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/** Whether the steps carry a comparison window: none does with it off. */
|
|
77
|
+
export const isCompared = (steps: FunnelStepReading[]): boolean =>
|
|
78
|
+
steps.some((step) => step.previousCount !== undefined)
|
|
79
|
+
|
|
80
|
+
const DELTA_ARROWS: Record<number, string> = { 1: '▲ ', 0: '', [-1]: '▼ ' }
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A step's change as the card prints it ("▲ 12 %"); a dash when none can be
|
|
84
|
+
* stated, against a step the comparison window never reached.
|
|
85
|
+
*/
|
|
86
|
+
export function formatDelta(delta: number | undefined, locale: string): string {
|
|
87
|
+
if (delta === undefined) return ABSENT_RATE
|
|
88
|
+
return `${DELTA_ARROWS[Math.sign(delta)] ?? ''}${formatRate(Math.abs(delta), locale)}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type DeltaTone = 'success' | 'error' | 'neutral'
|
|
92
|
+
|
|
93
|
+
/** Growth is good down the funnel, bad for the leak out of it. */
|
|
94
|
+
export function deltaTone(step: FunnelStepReading): DeltaTone {
|
|
95
|
+
if (!step.delta) return 'neutral'
|
|
96
|
+
const isGrowing = step.delta > 0
|
|
97
|
+
const isLeak = step.id === FUNNEL_LEAK_STEP
|
|
98
|
+
return isGrowing !== isLeak ? 'success' : 'error'
|
|
99
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const MINUTE_MS = 60_000
|
|
2
2
|
const DAY_MS = 86_400_000
|
|
3
|
-
const PRESET_KEY_SEPARATOR = /-/g
|
|
4
|
-
const KEY_SEPARATOR = '_'
|
|
5
3
|
|
|
6
4
|
/** Hour and minute on a 24-hour clock, the way the cards stamp "updated 14:32". */
|
|
7
5
|
export const CLOCK_FORMAT: Intl.DateTimeFormatOptions = {
|
|
@@ -10,11 +8,6 @@ export const CLOCK_FORMAT: Intl.DateTimeFormatOptions = {
|
|
|
10
8
|
hourCycle: 'h23',
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
/** The i18n key of a preset's short segmented label ("24H", "7D"…). */
|
|
14
|
-
export function presetShortLabelKey(preset: string): string {
|
|
15
|
-
return `dms_mailing.period.short.${preset.replace(PRESET_KEY_SEPARATOR, KEY_SEPARATOR)}`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
/** Whole minutes elapsed since `iso`; 0 for a missing or future date. */
|
|
19
12
|
export function minutesSince(
|
|
20
13
|
iso: string | null | undefined,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { StarterSummary } from '../types/mailing'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
/** Answers the text of an i18n key, or nothing when the catalog lacks it. */
|
|
4
|
+
export type OptionalTranslate = (key: string) => string | undefined
|
|
3
5
|
|
|
4
6
|
const STARTER_NAME_KEY_PREFIX = 'dms_mailing.starters.names'
|
|
5
7
|
|
|
@@ -93,11 +93,16 @@ export function splitTokens(text: string): TextPart[] {
|
|
|
93
93
|
return splitRawSegments(text).map(({ kind, value }) => ({ kind, value }))
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A test value as the data holds it. Amounts and dates are written for the
|
|
98
|
+
* recipient's locale by the engine alone (`src/engine/formats.ts`): the
|
|
99
|
+
* preview and the sends show them formatted, the paper shows them raw.
|
|
100
|
+
*/
|
|
96
101
|
function formatValue(value: unknown): string {
|
|
97
102
|
return value instanceof Date ? value.toISOString() : String(value)
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
/** Reads one token path in `data`,
|
|
105
|
+
/** Reads one token path in `data`, as the paper shows it. */
|
|
101
106
|
export function resolveToken(
|
|
102
107
|
path: string,
|
|
103
108
|
data: Record<string, unknown>,
|
|
@@ -437,11 +437,13 @@
|
|
|
437
437
|
"metrics": {
|
|
438
438
|
"bounces": "Bounces",
|
|
439
439
|
"click_rate": "Click rate",
|
|
440
|
+
"compared": "Compared period",
|
|
440
441
|
"deliverability": "Deliverability",
|
|
441
442
|
"domain_sends": "Domains with 5 or more deliveries",
|
|
442
443
|
"domains": "Open rate by domain",
|
|
443
444
|
"open_rate": "Open rate",
|
|
444
445
|
"previous": "Previous period",
|
|
446
|
+
"previous_year": "Previous year",
|
|
445
447
|
"ranking": "Best opened templates",
|
|
446
448
|
"sends": "Sends",
|
|
447
449
|
"unsubscribes": "Unsubscribes",
|
|
@@ -532,7 +534,8 @@
|
|
|
532
534
|
"send_again": "Send again…",
|
|
533
535
|
"copy_address": "Copy the address"
|
|
534
536
|
},
|
|
535
|
-
"
|
|
537
|
+
"heading": "{recipientEmail} · {templateSlug}",
|
|
538
|
+
"position": "Send {index} of {total}",
|
|
536
539
|
"replayed_by": "Sent again on {date}",
|
|
537
540
|
"replay_of": "This send repeats an earlier one",
|
|
538
541
|
"see_send": "See the send",
|
|
@@ -619,8 +622,9 @@
|
|
|
619
622
|
"latency_detail": "API call to {provider}",
|
|
620
623
|
"latency_detail_generic": "API call to the provider",
|
|
621
624
|
"joined": "{left} · {right}",
|
|
622
|
-
"
|
|
623
|
-
"
|
|
625
|
+
"versus": "vs {previous}",
|
|
626
|
+
"delta_up_versus": "▲ {delta} vs {previous}",
|
|
627
|
+
"delta_down_versus": "▼ {delta} vs {previous}",
|
|
624
628
|
"rate": "{rate}",
|
|
625
629
|
"latency_ms": "{value} ms",
|
|
626
630
|
"latency_s": "{value} s",
|
|
@@ -999,9 +1003,6 @@
|
|
|
999
1003
|
"archived": "Archived",
|
|
1000
1004
|
"attention": "Needs attention"
|
|
1001
1005
|
},
|
|
1002
|
-
"filters": {
|
|
1003
|
-
"category": "Category"
|
|
1004
|
-
},
|
|
1005
1006
|
"footer": {
|
|
1006
1007
|
"count": "{count} template | {count} templates",
|
|
1007
1008
|
"hint": "Open a template to edit it · ↵ open · T test send"
|
|
@@ -1075,8 +1076,12 @@
|
|
|
1075
1076
|
"description": "The overview dashboard, or the setup steps until the workspace can send."
|
|
1076
1077
|
},
|
|
1077
1078
|
"page_header": {
|
|
1078
|
-
"name": "
|
|
1079
|
-
"description": "The provider state and the period switch
|
|
1079
|
+
"name": "Page toolbar",
|
|
1080
|
+
"description": "The provider state, the last update, live mode and the period switch at the top of the page."
|
|
1081
|
+
},
|
|
1082
|
+
"provider_pill": {
|
|
1083
|
+
"name": "Provider state",
|
|
1084
|
+
"description": "The pill in the page toolbar saying whether a provider carries the mail."
|
|
1080
1085
|
},
|
|
1081
1086
|
"settings_form": {
|
|
1082
1087
|
"name": "Mailing settings",
|
|
@@ -1194,15 +1199,8 @@
|
|
|
1194
1199
|
"current_sender": "Sender as set in the settings today",
|
|
1195
1200
|
"meant_for": "The e-mail meant for {name}"
|
|
1196
1201
|
},
|
|
1197
|
-
"
|
|
1198
|
-
"
|
|
1199
|
-
"last_hour": "1H",
|
|
1200
|
-
"last_24h": "24H",
|
|
1201
|
-
"today": "1D",
|
|
1202
|
-
"last_7_days": "7D",
|
|
1203
|
-
"last_30_days": "30D",
|
|
1204
|
-
"last_90_days": "90D"
|
|
1205
|
-
}
|
|
1202
|
+
"live": {
|
|
1203
|
+
"new_sends": "{count} new send | {count} new sends"
|
|
1206
1204
|
},
|
|
1207
1205
|
"page_header": {
|
|
1208
1206
|
"provider_connected": "{name} connected",
|
|
@@ -1266,11 +1264,13 @@
|
|
|
1266
1264
|
"error_title": "The funnel could not be loaded",
|
|
1267
1265
|
"retry": "Try again",
|
|
1268
1266
|
"untracked": "Delivery is not measured: connect the provider’s webhook to follow the funnel.",
|
|
1269
|
-
"no_loss": "No e-mail was lost between the steps."
|
|
1267
|
+
"no_loss": "No e-mail was lost between the steps.",
|
|
1268
|
+
"compared_count": "vs {value} in the compared period"
|
|
1270
1269
|
},
|
|
1271
1270
|
"common": {
|
|
1272
1271
|
"cancel": "Cancel",
|
|
1273
|
-
"on_date": "{date}"
|
|
1272
|
+
"on_date": "{date}",
|
|
1273
|
+
"preview_unavailable": "Preview unavailable"
|
|
1274
1274
|
},
|
|
1275
1275
|
"duplicate": {
|
|
1276
1276
|
"title": "Duplicate template",
|
|
@@ -1389,15 +1389,6 @@
|
|
|
1389
1389
|
"boolean": "Yes / no",
|
|
1390
1390
|
"array": "List"
|
|
1391
1391
|
},
|
|
1392
|
-
"categories": {
|
|
1393
|
-
"defaults": {
|
|
1394
|
-
"orders": "Orders",
|
|
1395
|
-
"account": "Account & access",
|
|
1396
|
-
"billing": "Billing",
|
|
1397
|
-
"marketing": "Marketing",
|
|
1398
|
-
"forms": "Forms"
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1401
1392
|
"starters": {
|
|
1402
1393
|
"names": {
|
|
1403
1394
|
"order-confirmed": "Order confirmation",
|