@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,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
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useTemplateCategories } from '../composables/useTemplateCategories'
|
|
3
3
|
import { useEventListener } from '@vueuse/core'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useTemplatesOverview,
|
|
6
|
+
useTemplateTotal,
|
|
7
|
+
} from '../composables/useMailingReads'
|
|
5
8
|
import { useTemplateDrawer } from '../composables/useTemplateDrawer'
|
|
6
9
|
import {
|
|
7
10
|
groupByCategory,
|
|
@@ -44,29 +47,33 @@ const ALL_TAB = 'all'
|
|
|
44
47
|
|
|
45
48
|
const props = defineProps<TemplateGalleryProps>()
|
|
46
49
|
|
|
47
|
-
const api = useMailingApi()
|
|
48
50
|
const { t } = useI18n()
|
|
49
51
|
const overlays = useTemplateDrawer()
|
|
50
52
|
|
|
51
53
|
const { categories } = useTemplateCategories()
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
+
const overview = useTemplatesOverview()
|
|
55
|
+
const total = useTemplateTotal()
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/** Each template's figures; none for a failed read. */
|
|
58
|
+
const statsById = computed(
|
|
59
|
+
() =>
|
|
60
|
+
new Map<string, TemplateStats>(
|
|
61
|
+
(overview.error.value ? [] : (overview.data.value?.items ?? [])).map(
|
|
62
|
+
(entry) => [entry.id, entry],
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
/** How many templates the tenant has; unknown while read or once failed. */
|
|
67
|
+
const templateCount = computed<number | null>(() =>
|
|
68
|
+
total.error.value ? null : (total.data.value?.total ?? null),
|
|
69
|
+
)
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
/** The template count and the figures, read again with the rows. */
|
|
72
|
+
function refreshFigures(): void {
|
|
73
|
+
void overview.refresh()
|
|
74
|
+
void total.refresh()
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
onMounted(() => {
|
|
66
|
-
void loadStats()
|
|
67
|
-
void loadTemplates()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
77
|
/** The page of the table on screen: its query and its page index. */
|
|
71
78
|
const viewKeyOf = (): string =>
|
|
72
79
|
JSON.stringify([props.context.query, props.context.pagination.pageIndex])
|
|
@@ -86,8 +93,7 @@ watch(
|
|
|
86
93
|
const isRefresh = viewKey === shownViewKey
|
|
87
94
|
shownViewKey = viewKey
|
|
88
95
|
if (!isRefresh) return
|
|
89
|
-
|
|
90
|
-
void loadTemplates()
|
|
96
|
+
refreshFigures()
|
|
91
97
|
},
|
|
92
98
|
)
|
|
93
99
|
|
|
@@ -157,10 +163,9 @@ const emptyDescription = computed(() =>
|
|
|
157
163
|
: t('dms_mailing.templates.empty.view_description'),
|
|
158
164
|
)
|
|
159
165
|
|
|
166
|
+
/** The rows, and with them the template count and the figures. */
|
|
160
167
|
function refreshAll(): void {
|
|
161
168
|
void props.context.refresh()
|
|
162
|
-
void loadStats()
|
|
163
|
-
void loadTemplates()
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
function openRow(row: TemplateRow): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useDebounceFn, useIntersectionObserver } from '@vueuse/core'
|
|
3
3
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
4
|
+
import { useStarterPreviewRead } from '../composables/useMailingReads'
|
|
4
5
|
import type {
|
|
5
6
|
PreviewResponse,
|
|
6
7
|
PreviewVersion,
|
|
@@ -62,6 +63,10 @@ const emit = defineEmits<{ resolved: [PreviewResponse] }>()
|
|
|
62
63
|
const api = useMailingApi()
|
|
63
64
|
const { locale: uiLocale } = useI18n()
|
|
64
65
|
|
|
66
|
+
const readStarterPreview = useStarterPreviewRead(
|
|
67
|
+
() => props.starterId,
|
|
68
|
+
() => props.locale ?? uiLocale.value,
|
|
69
|
+
)
|
|
65
70
|
const fetchedHtml = ref('')
|
|
66
71
|
const loading = ref(false)
|
|
67
72
|
const height = ref(FALLBACK_HEIGHT)
|
|
@@ -86,11 +91,8 @@ useIntersectionObserver(box, ([entry]) => {
|
|
|
86
91
|
if (entry?.isIntersecting) isVisible.value = true
|
|
87
92
|
})
|
|
88
93
|
|
|
89
|
-
async function fetchStarter(
|
|
90
|
-
const preview = await
|
|
91
|
-
starterId,
|
|
92
|
-
props.locale ?? uiLocale.value,
|
|
93
|
-
)
|
|
94
|
+
async function fetchStarter(): Promise<void> {
|
|
95
|
+
const preview = await readStarterPreview()
|
|
94
96
|
fetchedHtml.value = preview.html
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -111,7 +113,7 @@ async function load(): Promise<void> {
|
|
|
111
113
|
loading.value = true
|
|
112
114
|
try {
|
|
113
115
|
await (props.starterId
|
|
114
|
-
? fetchStarter(
|
|
116
|
+
? fetchStarter()
|
|
115
117
|
: fetchTemplate(props.templateId as string))
|
|
116
118
|
} catch {
|
|
117
119
|
fetchedHtml.value = ''
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import FormFieldRow from '../build/components/FormFieldRow.vue'
|
|
3
3
|
import FormRows from '../build/components/FormRows.vue'
|
|
4
4
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
5
|
+
import { useTemplateContentRead } from '../composables/useMailingReads'
|
|
5
6
|
import type { SendResult, TestSendInput } from '../composables/useMailingApi'
|
|
6
7
|
import { apiErrorMessage } from '../utils/api-error'
|
|
7
8
|
import { sendLogRoute } from '../utils/send-link'
|
|
@@ -54,6 +55,7 @@ const { user } = useCurrentUser()
|
|
|
54
55
|
const { uniqueLocales } = useUniqueLocales()
|
|
55
56
|
|
|
56
57
|
const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
|
|
58
|
+
const readContent = useTemplateContentRead(templateId)
|
|
57
59
|
const detail = ref<TemplateContentResponse | null>(null)
|
|
58
60
|
const ownEmail = (user.value as SignedInUser | null)?.email
|
|
59
61
|
const recipients = ref<string[]>(ownEmail ? [ownEmail] : [])
|
|
@@ -65,7 +67,7 @@ const sending = ref(false)
|
|
|
65
67
|
onMounted(async () => {
|
|
66
68
|
if (!templateId.value) return
|
|
67
69
|
try {
|
|
68
|
-
detail.value = await
|
|
70
|
+
detail.value = await readContent()
|
|
69
71
|
locale.value ||= detail.value.fallbackLocale
|
|
70
72
|
} catch (error) {
|
|
71
73
|
toast.add({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Component } from 'vue'
|
|
3
|
-
import {
|
|
3
|
+
import { useTemplateVersionsRead } from '../composables/useMailingReads'
|
|
4
4
|
import type { TemplateStatus, TemplateVersionSummary } from '../types/mailing'
|
|
5
5
|
import { apiErrorMessage } from '../utils/api-error'
|
|
6
6
|
import {
|
|
@@ -31,7 +31,6 @@ const props = withDefaults(defineProps<VersionHistoryProps>(), {
|
|
|
31
31
|
locale: undefined,
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
const api = useMailingApi()
|
|
35
34
|
const modal = useModal()
|
|
36
35
|
const toast = useToast()
|
|
37
36
|
const { confirm } = useConfirm()
|
|
@@ -39,13 +38,14 @@ const { t, locale: uiLocale } = useI18n()
|
|
|
39
38
|
const { processApiMessage } = useTranslation()
|
|
40
39
|
const previewComponent = resolveComponent(PREVIEW_COMPONENT) as Component
|
|
41
40
|
|
|
41
|
+
const readVersions = useTemplateVersionsRead(() => props.templateId)
|
|
42
42
|
const versions = ref<TemplateVersionSummary[]>([])
|
|
43
43
|
const loading = ref(true)
|
|
44
44
|
|
|
45
45
|
async function load(): Promise<void> {
|
|
46
46
|
loading.value = true
|
|
47
47
|
try {
|
|
48
|
-
versions.value = (await
|
|
48
|
+
versions.value = (await readVersions()).versions
|
|
49
49
|
} catch (error) {
|
|
50
50
|
toast.add({
|
|
51
51
|
color: 'error',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useMailingApi } from '../composables/useMailingApi'
|
|
3
|
-
import {
|
|
3
|
+
import { useWebhookStatus } from '../composables/useMailingReads'
|
|
4
|
+
import { CARD_DATE_FORMAT, formatCardDate } from '../utils/gallery'
|
|
4
5
|
import {
|
|
5
6
|
type WebhookLineTone,
|
|
6
7
|
type WebhookStatus,
|
|
@@ -33,9 +34,14 @@ const TONE_CLASSES: Record<WebhookLineTone, string> = {
|
|
|
33
34
|
const api = useMailingApi()
|
|
34
35
|
const toast = useToast()
|
|
35
36
|
const { t, locale } = useI18n()
|
|
37
|
+
const { dateTimeFormat } = useRegionalFormat()
|
|
36
38
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
+
const read = useWebhookStatus()
|
|
40
|
+
/** The status read, `null` when the read failed. */
|
|
41
|
+
const status = computed<WebhookStatus | null>(() =>
|
|
42
|
+
read.error.value ? null : read.data.value,
|
|
43
|
+
)
|
|
44
|
+
const isLoading = computed(() => !read.data.value && !read.error.value)
|
|
39
45
|
const isTesting = ref(false)
|
|
40
46
|
|
|
41
47
|
const lines = computed(() =>
|
|
@@ -51,17 +57,15 @@ function lineText(line: WebhookStatusLine): string {
|
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
function lineTime(line: WebhookStatusLine): string {
|
|
54
|
-
return line.at
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
isLoading.value = false
|
|
64
|
-
}
|
|
60
|
+
return line.at
|
|
61
|
+
? formatCardDate(
|
|
62
|
+
line.at,
|
|
63
|
+
new Date(),
|
|
64
|
+
locale.value,
|
|
65
|
+
undefined,
|
|
66
|
+
dateTimeFormat(CARD_DATE_FORMAT),
|
|
67
|
+
)
|
|
68
|
+
: ''
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
async function sendTest(): Promise<void> {
|
|
@@ -69,7 +73,8 @@ async function sendTest(): Promise<void> {
|
|
|
69
73
|
isTesting.value = true
|
|
70
74
|
try {
|
|
71
75
|
const result = await api.webhookTest()
|
|
72
|
-
|
|
76
|
+
read.data.value = result.status
|
|
77
|
+
read.error.value = null
|
|
73
78
|
const message = webhookTestMessage(result)
|
|
74
79
|
toast.add({
|
|
75
80
|
color: message.color,
|
|
@@ -84,8 +89,6 @@ async function sendTest(): Promise<void> {
|
|
|
84
89
|
isTesting.value = false
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
|
-
|
|
88
|
-
onMounted(() => void load())
|
|
89
92
|
</script>
|
|
90
93
|
|
|
91
94
|
<template>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import { ref } from 'vue'
|
|
3
3
|
import type { Block } from '../../types/mailing'
|
|
4
4
|
import { useEditorContext } from '../../composables/useEditorContext'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useSortableList,
|
|
7
|
+
type SortableListChange,
|
|
8
|
+
} from '../../composables/useSortableList'
|
|
9
|
+
import { DRAG_GROUP, DRAG_HANDLE, DRAG_ITEM } from '../../utils/blocks'
|
|
6
10
|
import EditorBlock from './EditorBlock.vue'
|
|
7
11
|
import EditorInsertPoint from './EditorInsertPoint.vue'
|
|
8
12
|
|
|
@@ -12,40 +16,39 @@ interface Props {
|
|
|
12
16
|
|
|
13
17
|
const props = defineProps<Props>()
|
|
14
18
|
|
|
15
|
-
interface DraggableChange {
|
|
16
|
-
added?: { element: Block }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
19
|
const { editor } = useEditorContext()
|
|
20
20
|
|
|
21
|
-
function onChange(change:
|
|
21
|
+
function onChange(change: SortableListChange<Block>): void {
|
|
22
22
|
editor.touch()
|
|
23
23
|
if (change.added) editor.select(change.added.element.id)
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
const root = ref<HTMLElement | null>(null)
|
|
27
|
+
|
|
28
|
+
// The list is the editor's: a drop changes it in place, as a click does.
|
|
29
|
+
useSortableList(root, () => props.list, {
|
|
30
|
+
group: DRAG_GROUP,
|
|
31
|
+
handle: DRAG_HANDLE,
|
|
32
|
+
draggable: DRAG_ITEM,
|
|
33
|
+
ghostClass: 'opacity-30',
|
|
34
|
+
onChange,
|
|
35
|
+
})
|
|
25
36
|
</script>
|
|
26
37
|
|
|
27
38
|
<template>
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<template #footer>
|
|
44
|
-
<EditorInsertPoint
|
|
45
|
-
:list="props.list"
|
|
46
|
-
:index="props.list.length"
|
|
47
|
-
:is-empty="props.list.length === 0"
|
|
48
|
-
/>
|
|
49
|
-
</template>
|
|
50
|
-
</draggable>
|
|
39
|
+
<div ref="root" class="flex flex-col">
|
|
40
|
+
<div
|
|
41
|
+
v-for="(element, index) in props.list"
|
|
42
|
+
:key="element.id"
|
|
43
|
+
data-drag-item
|
|
44
|
+
>
|
|
45
|
+
<EditorInsertPoint :list="props.list" :index="index" />
|
|
46
|
+
<EditorBlock :block="element" :index="index" :list="props.list" />
|
|
47
|
+
</div>
|
|
48
|
+
<EditorInsertPoint
|
|
49
|
+
:list="props.list"
|
|
50
|
+
:index="props.list.length"
|
|
51
|
+
:is-empty="props.list.length === 0"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
51
54
|
</template>
|