@antelopejs/dms-mailing 0.7.1 → 0.8.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.
Files changed (152) hide show
  1. package/README.md +21 -4
  2. package/dist/crons/jobs.d.ts +1 -1
  3. package/dist/data/body.js +2 -1
  4. package/dist/data/body.js.map +1 -1
  5. package/dist/data/templates-table.js +4 -3
  6. package/dist/data/templates-table.js.map +1 -1
  7. package/dist/db/models/template-versions.model.js.map +1 -1
  8. package/dist/db/models/templates.model.js +2 -1
  9. package/dist/db/models/templates.model.js.map +1 -1
  10. package/dist/engine/changes.js +2 -1
  11. package/dist/engine/changes.js.map +1 -1
  12. package/dist/engine/conditions.js +2 -3
  13. package/dist/engine/conditions.js.map +1 -1
  14. package/dist/engine/fallback-html.js +8 -8
  15. package/dist/engine/fallback-html.js.map +1 -1
  16. package/dist/engine/formats.js +6 -6
  17. package/dist/engine/formats.js.map +1 -1
  18. package/dist/engine/paths.js +2 -3
  19. package/dist/engine/paths.js.map +1 -1
  20. package/dist/engine/system-variables.js +1 -1
  21. package/dist/engine/system-variables.js.map +1 -1
  22. package/dist/engine/tokens.js.map +1 -1
  23. package/dist/pages/sends.js.map +1 -1
  24. package/dist/pages/toolbar.js.map +1 -1
  25. package/dist/routes/events.js +2 -2
  26. package/dist/routes/events.js.map +1 -1
  27. package/dist/routes/metrics.js +6 -5
  28. package/dist/routes/metrics.js.map +1 -1
  29. package/dist/routes/sends.js +1 -1
  30. package/dist/routes/sends.js.map +1 -1
  31. package/dist/routes/settings.js +6 -6
  32. package/dist/routes/settings.js.map +1 -1
  33. package/dist/routes/templates.js +4 -4
  34. package/dist/routes/templates.js.map +1 -1
  35. package/dist/services/attention.js +2 -1
  36. package/dist/services/attention.js.map +1 -1
  37. package/dist/services/categories.js +6 -6
  38. package/dist/services/categories.js.map +1 -1
  39. package/dist/services/checked-send.js +16 -9
  40. package/dist/services/checked-send.js.map +1 -1
  41. package/dist/services/declarations.js.map +1 -1
  42. package/dist/services/events.js +11 -7
  43. package/dist/services/events.js.map +1 -1
  44. package/dist/services/localized-text.d.ts +2 -1
  45. package/dist/services/localized-text.js +2 -1
  46. package/dist/services/localized-text.js.map +1 -1
  47. package/dist/services/metrics.js.map +1 -1
  48. package/dist/services/render.js.map +1 -1
  49. package/dist/services/send-import.js +7 -3
  50. package/dist/services/send-import.js.map +1 -1
  51. package/dist/services/send.js +4 -3
  52. package/dist/services/send.js.map +1 -1
  53. package/dist/services/starters.js +5 -5
  54. package/dist/services/starters.js.map +1 -1
  55. package/dist/services/template-stats.js +5 -2
  56. package/dist/services/template-stats.js.map +1 -1
  57. package/dist/services/templates.js +6 -2
  58. package/dist/services/templates.js.map +1 -1
  59. package/dist/validation/declarations.schema.d.ts +16 -16
  60. package/dist/validation/declarations.schema.js +2 -2
  61. package/dist/validation/declarations.schema.js.map +1 -1
  62. package/dist/validation/guards.d.ts +4 -0
  63. package/dist/validation/guards.js +19 -0
  64. package/dist/validation/guards.js.map +1 -0
  65. package/dist/validation/send-event-type.schema.d.ts +2 -0
  66. package/dist/validation/send-event-type.schema.js +11 -0
  67. package/dist/validation/send-event-type.schema.js.map +1 -0
  68. package/dist/validation/settings.schema.js +1 -1
  69. package/dist/validation/settings.schema.js.map +1 -1
  70. package/dist/validation/templates.schema.d.ts +19 -18
  71. package/dist/validation/templates.schema.js.map +1 -1
  72. package/frontend-vue/app/build/components/TemplateDrawerFooter.vue +45 -0
  73. package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +102 -0
  74. package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +110 -0
  75. package/frontend-vue/app/build/composables/useDrawerStats.ts +78 -0
  76. package/frontend-vue/app/components/DuplicateModal.vue +3 -11
  77. package/frontend-vue/app/components/FixAddressModal.vue +2 -1
  78. package/frontend-vue/app/components/NewTemplateModal.vue +3 -11
  79. package/frontend-vue/app/components/RealSendModal.vue +6 -17
  80. package/frontend-vue/app/components/RenderingModal.vue +2 -1
  81. package/frontend-vue/app/components/SendDrawer.vue +5 -4
  82. package/frontend-vue/app/components/StarterGallery.vue +3 -11
  83. package/frontend-vue/app/components/TemplateDrawer.vue +40 -269
  84. package/frontend-vue/app/components/TestSendModal.vue +7 -18
  85. package/frontend-vue/app/components/VersionHistory.vue +2 -7
  86. package/frontend-vue/app/components/WebhookStatus.vue +2 -1
  87. package/frontend-vue/app/components/editor/EditorWorkspace.vue +2 -7
  88. package/frontend-vue/build/backend-boundary.ts +122 -0
  89. package/frontend-vue/dms.frontend.build.ts +7 -0
  90. package/frontend-vue/eslint.config.mjs +13 -0
  91. package/frontend-vue/package.json +4 -2
  92. package/frontend-vue/pnpm-lock.yaml +210 -61
  93. package/frontend-vue/tests/backend-boundary.test.ts +203 -0
  94. package/frontend-vue/tests/dnd.test.ts +1 -2
  95. package/frontend-vue/tests/editor-block-list.test.ts +136 -53
  96. package/frontend-vue/tests/helpers/auth-fetch.ts +123 -0
  97. package/frontend-vue/tests/helpers/nuxt-globals.ts +17 -1
  98. package/frontend-vue/tests/new-template-modal.test.ts +41 -6
  99. package/frontend-vue/tests/page-reads.test.ts +26 -6
  100. package/frontend-vue/tests/send-actions.test.ts +0 -1
  101. package/frontend-vue/tests/send-drawer.test.ts +2 -6
  102. package/frontend-vue/tests/send-modals.test.ts +8 -5
  103. package/frontend-vue/tests/sortable-list.test.ts +54 -62
  104. package/frontend-vue/tests/suppression.test.ts +1 -2
  105. package/frontend-vue/tests/template-drawer.test.ts +27 -18
  106. package/frontend-vue/tests/template-gallery.test.ts +7 -5
  107. package/frontend-vue/tests/template-preview-frame.test.ts +2 -5
  108. package/package.json +3 -8
  109. package/skills/dms-mailing/SKILL.md +3 -3
  110. package/skills/dms-mailing/cases.json +49 -0
  111. package/src/crons/jobs.ts +1 -1
  112. package/src/data/body.ts +2 -1
  113. package/src/data/templates-table.ts +12 -7
  114. package/src/db/models/template-versions.model.ts +5 -1
  115. package/src/db/models/templates.model.ts +6 -3
  116. package/src/engine/changes.ts +3 -2
  117. package/src/engine/conditions.ts +2 -4
  118. package/src/engine/fallback-html.ts +13 -13
  119. package/src/engine/formats.ts +6 -6
  120. package/src/engine/paths.ts +4 -3
  121. package/src/engine/system-variables.ts +4 -3
  122. package/src/engine/tokens.ts +1 -2
  123. package/src/pages/sends.ts +5 -3
  124. package/src/pages/templates.ts +1 -1
  125. package/src/pages/toolbar.ts +4 -2
  126. package/src/routes/events.ts +3 -6
  127. package/src/routes/metrics.ts +7 -5
  128. package/src/routes/sends.ts +10 -18
  129. package/src/routes/settings.ts +8 -9
  130. package/src/routes/suppressions.ts +1 -1
  131. package/src/routes/templates.ts +5 -5
  132. package/src/services/attention.ts +4 -3
  133. package/src/services/categories.ts +6 -6
  134. package/src/services/checked-send.ts +17 -19
  135. package/src/services/declarations.ts +3 -5
  136. package/src/services/events.ts +16 -13
  137. package/src/services/localized-text.ts +4 -2
  138. package/src/services/metrics.ts +2 -2
  139. package/src/services/notify.ts +1 -1
  140. package/src/services/render.ts +5 -1
  141. package/src/services/send-import.ts +16 -11
  142. package/src/services/send.ts +7 -7
  143. package/src/services/starters.ts +6 -6
  144. package/src/services/template-stats.ts +11 -4
  145. package/src/services/templates.ts +10 -8
  146. package/src/validation/declarations.schema.ts +2 -2
  147. package/src/validation/guards.ts +25 -0
  148. package/src/validation/send-event-type.schema.ts +10 -0
  149. package/src/validation/settings.schema.ts +7 -2
  150. package/src/validation/templates.schema.ts +2 -1
  151. package/frontend-vue/tests/i18n-editor.test.ts +0 -50
  152. package/frontend-vue/tests/i18n.test.ts +0 -138
@@ -7,7 +7,6 @@ import { useMailingApi } from '../composables/useMailingApi'
7
7
  import { useTemplateContentRead } from '../composables/useMailingReads'
8
8
  import type { SendResult } from '../composables/useMailingApi'
9
9
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
10
- import { apiErrorMessage } from '../utils/api-error'
11
10
  import { firstName, formatCardDate } from '../utils/gallery'
12
11
  import { localeChipClass, missingLocales } from '../utils/locales'
13
12
  import {
@@ -57,10 +56,10 @@ const emit = defineEmits<{ success: [sent?: boolean] }>()
57
56
 
58
57
  const api = useMailingApi()
59
58
  const toast = useToast()
59
+ const { presentError } = useApiErrorPresenter()
60
60
  const { t, locale: uiLocale } = useI18n()
61
61
  const onDate = (date: string): string =>
62
62
  t('dms_mailing.common.on_date', { date })
63
- const { processApiMessage } = useTranslation()
64
63
  const { uniqueLocales } = useUniqueLocales()
65
64
  const overlays = useTemplateDrawer()
66
65
 
@@ -110,10 +109,7 @@ onMounted(async () => {
110
109
  values.value = initialSendValues(fields.value, detail.value.testData)
111
110
  locale.value = detail.value.fallbackLocale
112
111
  } catch (error) {
113
- toast.add({
114
- color: 'error',
115
- title: processApiMessage(apiErrorMessage(error)),
116
- })
112
+ presentError(error, { fallbackKey: 'error.500.description' })
117
113
  } finally {
118
114
  loading.value = false
119
115
  }
@@ -296,15 +292,11 @@ function openSettings(): void {
296
292
  void navigateDms({ path: SETTINGS_PATH })
297
293
  }
298
294
 
299
- function report(results: SendResult[], to: string[]): void {
295
+ function report(results: SendResult[]): void {
300
296
  results
301
297
  .filter((result) => result.error)
302
298
  .forEach((result) =>
303
- toast.add({
304
- color: 'error',
305
- title: to[results.indexOf(result)] ?? result.sendId,
306
- description: processApiMessage(result.error),
307
- }),
299
+ presentError(result.error, { fallbackKey: 'error.500.description' }),
308
300
  )
309
301
  const sent = results.filter((result) => !result.error).length
310
302
  if (!sent) return
@@ -331,13 +323,10 @@ async function submit(): Promise<void> {
331
323
  locale: locale.value || undefined,
332
324
  data: sendData.value,
333
325
  })
334
- report(response.results, to)
326
+ report(response.results)
335
327
  close(true)
336
328
  } catch (error) {
337
- toast.add({
338
- color: 'error',
339
- title: processApiMessage(apiErrorMessage(error)),
340
- })
329
+ presentError(error, { fallbackKey: 'error.500.description' })
341
330
  } finally {
342
331
  sending.value = false
343
332
  }
@@ -45,6 +45,7 @@ const FRAME_WIDTHS: Record<RenderingView, string> = {
45
45
  }
46
46
 
47
47
  const { t, locale } = useI18n()
48
+ const { presentError } = useApiErrorPresenter()
48
49
 
49
50
  const view = ref<RenderingView>('desktop')
50
51
  const rendering = ref<SendRendering | null>(null)
@@ -99,7 +100,7 @@ async function load(): Promise<void> {
99
100
  rendering.value = html
100
101
  } catch (error) {
101
102
  failed.value = true
102
- useApiError(error)
103
+ presentError(error, { fallbackKey: `${KEY_PREFIX}error_description` })
103
104
  } finally {
104
105
  loading.value = false
105
106
  }
@@ -82,6 +82,7 @@ const props = defineProps<SendDrawerProps>()
82
82
 
83
83
  const api = useMailingApi()
84
84
  const toast = useToast()
85
+ const { presentError } = useApiErrorPresenter()
85
86
  const reportReplay = useReplayToast()
86
87
  const { confirm } = useConfirm()
87
88
  const { open: openModal } = useModal()
@@ -236,7 +237,7 @@ async function load(): Promise<void> {
236
237
  try {
237
238
  detail.value = await readSend()
238
239
  } catch (error) {
239
- useApiError(error)
240
+ presentError(error, { fallbackKey: `${KEY_PREFIX}send_unavailable` })
240
241
  } finally {
241
242
  loading.value = false
242
243
  }
@@ -264,8 +265,8 @@ async function loadTracking(): Promise<void> {
264
265
  async function showSend(id: string): Promise<void> {
265
266
  try {
266
267
  await props.showRow?.(id)
267
- } catch {
268
- toast.add({ color: 'error', title: key('send_unavailable') })
268
+ } catch (error) {
269
+ presentError(error, { fallbackKey: `${KEY_PREFIX}send_unavailable` })
269
270
  }
270
271
  }
271
272
 
@@ -363,7 +364,7 @@ async function replay(): Promise<void> {
363
364
  )
364
365
  settle()
365
366
  } catch (error) {
366
- useApiError(error)
367
+ presentError(error, { fallbackKey: 'error.500.description' })
367
368
  } finally {
368
369
  sending.value = false
369
370
  }
@@ -4,11 +4,7 @@ import { useLocalizedDefaults } from '../composables/useLocalizedDefaults'
4
4
  import { useMailingApi } from '../composables/useMailingApi'
5
5
  import { useStarters } from '../composables/useMailingReads'
6
6
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
7
- import {
8
- HTTP_CONFLICT,
9
- apiErrorMessage,
10
- apiErrorStatus,
11
- } from '../utils/api-error'
7
+ import { HTTP_CONFLICT, apiErrorStatus } from '../utils/api-error'
12
8
  import { editorRoute } from '../utils/editor-route'
13
9
  import type { StarterSummary } from '../types/mailing'
14
10
 
@@ -19,9 +15,8 @@ const SKELETON_STARTERS = 4
19
15
  const emit = defineEmits<{ created: [] }>()
20
16
 
21
17
  const api = useMailingApi()
22
- const toast = useToast()
18
+ const { presentError } = useApiErrorPresenter()
23
19
  const { t } = useI18n()
24
- const { processApiMessage } = useTranslation()
25
20
  const overlays = useTemplateDrawer()
26
21
 
27
22
  const localized = useLocalizedDefaults()
@@ -69,10 +64,7 @@ async function createFrom(starter: StarterSummary): Promise<void> {
69
64
  openNewTemplate(starter)
70
65
  return
71
66
  }
72
- toast.add({
73
- color: 'error',
74
- title: processApiMessage(apiErrorMessage(error)),
75
- })
67
+ presentError(error, { fallbackKey: 'error.500.description' })
76
68
  } finally {
77
69
  creatingId.value = null
78
70
  }
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { SENDS_PATH } from '../utils/pages'
2
+ import { useDrawerStats } from '../build/composables/useDrawerStats'
3
+ import TemplateDrawerFooter from '../build/components/TemplateDrawerFooter.vue'
4
+ import TemplateDrawerPerformance from '../build/components/TemplateDrawerPerformance.vue'
5
+ import TemplateDrawerVariables from '../build/components/TemplateDrawerVariables.vue'
3
6
  import { useElementSize } from '@vueuse/core'
4
7
  import {
5
8
  useMailingApi,
@@ -10,11 +13,9 @@ import {
10
13
  useTemplatePerformanceRead,
11
14
  } from '../composables/useMailingReads'
12
15
  import { useTemplateDrawer } from '../composables/useTemplateDrawer'
13
- import { apiErrorMessage } from '../utils/api-error'
14
16
  import { formatCardDate } from '../utils/gallery'
15
17
  import { localeCoverage } from '../utils/locales'
16
18
  import { sendSnippet } from '../utils/send-snippet'
17
- import { variableTypeLabelKey } from '../utils/variable-types'
18
19
  import { validateName } from '../utils/slug'
19
20
  import {
20
21
  validTransitions,
@@ -66,14 +67,8 @@ interface LifecycleSpec {
66
67
  confirmColor?: ConfirmTone
67
68
  }
68
69
 
69
- /** The send log reads these: `?template=` narrows it, `?view=` opens a tab. */
70
- const SENDS_TEMPLATE_KEY = 'template'
71
- const SENDS_VIEW_KEY = 'view'
72
- const PROBLEMS_VIEW = 'problems'
73
70
  const PREVIEW_WIDTH = 600
74
71
  const PREVIEW_FALLBACK_SCALE = 0.72
75
- const OPEN_RATE_DIGITS = 1
76
- const PERCENT = 100
77
72
  const DEFAULT_VARIABLE_TYPE: VariableType = 'string'
78
73
 
79
74
  const STATUS_TONES: Record<TemplateStatus, string> = {
@@ -88,9 +83,9 @@ defineEmits<{ success: [changed?: boolean] }>()
88
83
 
89
84
  const api = useMailingApi()
90
85
  const toast = useToast()
86
+ const { presentError } = useApiErrorPresenter()
91
87
  const { confirm } = useConfirm()
92
88
  const { t, locale: uiLocale } = useI18n()
93
- const { processApiMessage } = useTranslation()
94
89
  const { uniqueLocales } = useUniqueLocales()
95
90
  const overlays = useTemplateDrawer()
96
91
 
@@ -142,10 +137,7 @@ async function load(): Promise<void> {
142
137
  performance.value = stats
143
138
  locale.value ||= content.fallbackLocale
144
139
  } catch (error) {
145
- toast.add({
146
- color: 'error',
147
- title: processApiMessage(apiErrorMessage(error)),
148
- })
140
+ presentError(error, { fallbackKey: 'error.500.description' })
149
141
  } finally {
150
142
  loading.value = false
151
143
  }
@@ -261,60 +253,12 @@ const versionLabel = computed(() =>
261
253
  : t('dms_mailing.templates.drawer.never_published'),
262
254
  )
263
255
 
264
- const formatNumber = (value: number) =>
265
- new Intl.NumberFormat(uiLocale.value, {
266
- maximumFractionDigits: OPEN_RATE_DIGITS,
267
- }).format(value)
268
-
269
- /** A rate in percent, written the reader's way ("20.8%", "20,8 %"). */
270
- const formatPercent = (value: number) =>
271
- new Intl.NumberFormat(uiLocale.value, {
272
- style: 'percent',
273
- maximumFractionDigits: OPEN_RATE_DIGITS,
274
- }).format(value / PERCENT)
275
-
276
- /** The send log narrowed to this template, on one of its tabs if given. */
277
- function sendLogRoute(view?: string) {
278
- return {
279
- path: SENDS_PATH,
280
- query: {
281
- [SENDS_TEMPLATE_KEY]: template.value?.slug ?? '',
282
- ...(view ? { [SENDS_VIEW_KEY]: view } : {}),
283
- },
284
- }
285
- }
286
-
287
- function sendLogUrl(view?: string): string {
288
- const { path, query } = sendLogRoute(view)
289
- return `${path}?${new URLSearchParams(query).toString()}`
290
- }
291
-
292
- const hasSends = computed(() => Boolean(performance.value?.sends))
293
-
294
- const stats = computed(() => [
295
- {
296
- id: 'sends',
297
- eyebrow: t('dms_mailing.templates.drawer.stats_sends'),
298
- value: formatNumber(performance.value?.sends ?? 0),
299
- },
300
- {
301
- id: 'open',
302
- eyebrow: t('dms_mailing.templates.drawer.stats_open'),
303
- value: performance.value?.sends
304
- ? formatPercent(performance.value.openRate)
305
- : '—',
306
- },
307
- {
308
- id: 'problems',
309
- eyebrow: t('dms_mailing.templates.drawer.stats_problems'),
310
- value: formatNumber(performance.value?.problems ?? 0),
311
- detail: performance.value?.problems
312
- ? t('dms_mailing.templates.drawer.problems_detail')
313
- : undefined,
314
- detailTone: 'error' as const,
315
- to: performance.value?.problems ? sendLogUrl(PROBLEMS_VIEW) : undefined,
316
- },
317
- ])
256
+ const { formatNumber, sendLogRoute, hasSends, stats } = useDrawerStats(
257
+ template,
258
+ performance,
259
+ t,
260
+ uiLocale,
261
+ )
318
262
 
319
263
  const snippet = computed(() =>
320
264
  sendSnippet({
@@ -363,10 +307,7 @@ async function saveDetails(changes: Partial<TemplateDetails>): Promise<void> {
363
307
  })
364
308
  reloadInPlace()
365
309
  } catch (error) {
366
- toast.add({
367
- color: 'error',
368
- title: processApiMessage(apiErrorMessage(error)),
369
- })
310
+ presentError(error, { fallbackKey: 'error.500.description' })
370
311
  }
371
312
  }
372
313
 
@@ -424,10 +365,7 @@ async function runLifecycle(action: LifecycleAction): Promise<void> {
424
365
  })
425
366
  settle()
426
367
  } catch (error) {
427
- toast.add({
428
- color: 'error',
429
- title: processApiMessage(apiErrorMessage(error)),
430
- })
368
+ presentError(error, { fallbackKey: 'error.500.description' })
431
369
  }
432
370
  }
433
371
 
@@ -472,10 +410,7 @@ async function declareVariable(path: string): Promise<void> {
472
410
  detail.value.variables = response.variables
473
411
  detail.value.revision = response.revision
474
412
  } catch (error) {
475
- toast.add({
476
- color: 'error',
477
- title: processApiMessage(apiErrorMessage(error)),
478
- })
413
+ presentError(error, { fallbackKey: 'error.500.description' })
479
414
  reloadInPlace()
480
415
  }
481
416
  }
@@ -499,10 +434,7 @@ async function restoreVersion(version: number): Promise<void> {
499
434
  ],
500
435
  })
501
436
  } catch (error) {
502
- toast.add({
503
- color: 'error',
504
- title: processApiMessage(apiErrorMessage(error)),
505
- })
437
+ presentError(error, { fallbackKey: 'error.500.description' })
506
438
  }
507
439
  reloadInPlace()
508
440
  }
@@ -701,172 +633,26 @@ function onResolved(preview: PreviewResponse): void {
701
633
  </div>
702
634
 
703
635
  <div v-show="tab === 'variables'" class="flex flex-col gap-4 py-4">
704
- <div class="flex flex-col gap-1">
705
- <DmsEyebrow
706
- :label="t('dms_mailing.templates.drawer.declared_detected')"
707
- />
708
- <p v-if="!variables.length" class="text-muted text-[12.5px]">
709
- {{ t('dms_mailing.templates.drawer.no_variables') }}
710
- </p>
711
- <div
712
- v-for="variable in variables"
713
- :key="variable.path"
714
- class="border-default flex min-h-9 items-center gap-2 border-b last:border-b-0"
715
- >
716
- <code
717
- class="truncate font-mono text-[12px]"
718
- :class="
719
- variable.usage === 'unused' ? 'text-dimmed' : 'text-toned'
720
- "
721
- >
722
- {{ variable.path }}
723
- </code>
724
- <span
725
- v-if="variable.required && variable.usage !== 'detected'"
726
- class="text-error"
727
- :title="t('dms_mailing.templates.drawer.required')"
728
- >
729
- *
730
- </span>
731
- <DmsStatusPill
732
- v-if="variable.usage === 'detected'"
733
- tone="warning"
734
- size="sm"
735
- dot="none"
736
- :label="t('dms_mailing.templates.drawer.not_declared')"
737
- />
738
- <DmsStatusPill
739
- v-if="variable.usage === 'unused'"
740
- tone="neutral"
741
- size="sm"
742
- dot="none"
743
- :label="t('dms_mailing.templates.drawer.unused')"
744
- />
745
- <UButton
746
- v-if="variable.usage === 'detected' && !isLive"
747
- class="ml-auto"
748
- color="neutral"
749
- variant="ghost"
750
- size="xs"
751
- :label="t('dms_mailing.templates.drawer.declare')"
752
- @click="declareVariable(variable.path)"
753
- />
754
- <span
755
- v-else-if="variable.usage !== 'detected'"
756
- class="text-dimmed ml-auto font-mono text-[11px]"
757
- >
758
- {{ t(variableTypeLabelKey(variable.type)) }}
759
- </span>
760
- </div>
761
- </div>
762
- <DmsBanner
763
- v-if="hasDraftDeclarations"
764
- tone="info"
765
- size="sm"
766
- icon="i-ph-brackets-curly"
767
- :title="t('dms_mailing.templates.drawer.draft_declarations')"
768
- :description="
769
- t('dms_mailing.templates.drawer.draft_declarations_description')
770
- "
771
- >
772
- <template #actions>
773
- <UButton
774
- color="neutral"
775
- variant="outline"
776
- size="xs"
777
- :label="t('dms_mailing.templates.actions.open_editor')"
778
- @click="openEditor()"
779
- />
780
- </template>
781
- </DmsBanner>
782
- <MailingSystemVariables :variables="systemVars" />
783
- <div class="flex flex-col gap-1.5">
784
- <div class="flex items-center justify-between">
785
- <DmsEyebrow
786
- :label="t('dms_mailing.templates.drawer.call_from_code')"
787
- />
788
- <DmsCopyButton :value="snippet" />
789
- </div>
790
- <pre
791
- class="bg-elevated text-toned overflow-x-auto rounded-md p-3 font-mono text-[11.5px] leading-relaxed"
792
- >{{ snippet }}</pre>
793
- </div>
636
+ <TemplateDrawerVariables
637
+ :variables="variables"
638
+ :is-live="isLive"
639
+ :has-draft-declarations="hasDraftDeclarations"
640
+ :system-vars="systemVars"
641
+ :snippet="snippet"
642
+ :declare-variable="declareVariable"
643
+ :open-editor="openEditor"
644
+ />
794
645
  </div>
795
646
 
796
647
  <div v-show="tab === 'performance'" class="flex flex-col gap-4 py-4">
797
- <div class="flex flex-col gap-2">
798
- <div class="flex items-center justify-between">
799
- <DmsEyebrow
800
- :label="t('dms_mailing.templates.drawer.last_30_days')"
801
- />
802
- <UButton
803
- variant="link"
804
- size="xs"
805
- class="p-0"
806
- :label="t('dms_mailing.templates.drawer.open_send_log')"
807
- @click="openSendLog"
808
- />
809
- </div>
810
- <DmsStatGroup
811
- layout="joined"
812
- :columns="3"
813
- :items="stats"
814
- :loading="loading"
815
- />
816
- </div>
817
- <div class="flex flex-col gap-1">
818
- <DmsEyebrow :label="t('dms_mailing.templates.drawer.called_from')" />
819
- <p
820
- v-if="!hasSends || !performance?.sources.length"
821
- class="text-muted text-[12.5px]"
822
- >
823
- {{ t('dms_mailing.templates.drawer.no_sources') }}
824
- </p>
825
- <DmsListRow
826
- v-for="source in performance?.sources ?? []"
827
- :key="source.source"
828
- icon="i-ph-code"
829
- size="sm"
830
- mono
831
- :title="
832
- source.source || t('dms_mailing.templates.drawer.unknown_source')
833
- "
834
- :trailing="
835
- t(
836
- 'dms_mailing.templates.drawer.sends_count',
837
- { count: formatNumber(source.count) },
838
- source.count,
839
- )
840
- "
841
- />
842
- </div>
843
- <div class="flex flex-col gap-1">
844
- <DmsEyebrow :label="t('dms_mailing.templates.drawer.fallbacks')" />
845
- <p v-if="!hasSends" class="text-muted text-[12.5px]">
846
- {{ t('dms_mailing.templates.drawer.no_sources') }}
847
- </p>
848
- <p
849
- v-else-if="!performance?.fallbacks.length"
850
- class="text-muted text-[12.5px]"
851
- >
852
- {{ t('dms_mailing.templates.drawer.no_fallbacks') }}
853
- </p>
854
- <DmsListRow
855
- v-for="fallback in performance?.fallbacks ?? []"
856
- :key="`${fallback.requested}-${fallback.used}`"
857
- icon="i-ph-translate"
858
- tone="warning"
859
- size="sm"
860
- :title="`${fallback.requested.toUpperCase()} → ${fallback.used.toUpperCase()}`"
861
- :trailing="
862
- t(
863
- 'dms_mailing.templates.drawer.sends_count',
864
- { count: formatNumber(fallback.count) },
865
- fallback.count,
866
- )
867
- "
868
- />
869
- </div>
648
+ <TemplateDrawerPerformance
649
+ :stats="stats"
650
+ :loading="loading"
651
+ :has-sends="hasSends"
652
+ :performance="performance"
653
+ :open-send-log="openSendLog"
654
+ :format-number="formatNumber"
655
+ />
870
656
  </div>
871
657
 
872
658
  <div v-if="tab === 'history'" class="flex flex-col gap-2 py-4">
@@ -882,28 +668,13 @@ function onResolved(preview: PreviewResponse): void {
882
668
  />
883
669
  </div>
884
670
 
885
- <DmsContainerFooter data-template-footer>
886
- <UButton
887
- icon="i-ph-flask"
888
- color="neutral"
889
- variant="ghost"
890
- :label="t('dms_mailing.templates.card.test')"
891
- @click="overlays.openTestSend(templateId, { locale })"
892
- />
893
- <UButton
894
- class="ml-auto"
895
- icon="i-ph-copy"
896
- color="neutral"
897
- variant="outline"
898
- :label="t('dms_mailing.templates.actions.duplicate')"
899
- @click="overlays.openDuplicate(templateId, { rowData: template })"
900
- />
901
- <UButton
902
- icon="i-ph-pencil-simple"
903
- :label="t('dms_mailing.templates.actions.open_editor')"
904
- @click="openEditor(locale)"
905
- />
906
- </DmsContainerFooter>
671
+ <TemplateDrawerFooter
672
+ :template-id="templateId"
673
+ :locale="locale"
674
+ :template="template"
675
+ :overlays="overlays"
676
+ :open-editor="openEditor"
677
+ />
907
678
  </template>
908
679
  </div>
909
680
  </template>
@@ -4,7 +4,6 @@ import FormRows from '../build/components/FormRows.vue'
4
4
  import { useMailingApi } from '../composables/useMailingApi'
5
5
  import { useTemplateContentRead } from '../composables/useMailingReads'
6
6
  import type { SendResult, TestSendInput } from '../composables/useMailingApi'
7
- import { apiErrorMessage } from '../utils/api-error'
8
7
  import { sendLogRoute } from '../utils/send-link'
9
8
  import { reasonKey, type SuppressionSummary } from '../utils/suppression'
10
9
  import {
@@ -49,8 +48,8 @@ const emit = defineEmits<{ success: [sent?: boolean] }>()
49
48
 
50
49
  const api = useMailingApi()
51
50
  const toast = useToast()
51
+ const { presentError } = useApiErrorPresenter()
52
52
  const { t, locale: uiLocale } = useI18n()
53
- const { processApiMessage } = useTranslation()
54
53
  const { user } = useCurrentUser()
55
54
  const { uniqueLocales } = useUniqueLocales()
56
55
 
@@ -70,10 +69,7 @@ onMounted(async () => {
70
69
  detail.value = await readContent()
71
70
  locale.value ||= detail.value.fallbackLocale
72
71
  } catch (error) {
73
- toast.add({
74
- color: 'error',
75
- title: processApiMessage(apiErrorMessage(error)),
76
- })
72
+ presentError(error, { fallbackKey: 'error.500.description' })
77
73
  }
78
74
  })
79
75
 
@@ -154,14 +150,10 @@ function contentOverride(): Pick<TestSendInput, 'content' | 'version'> {
154
150
  return {}
155
151
  }
156
152
 
157
- function reportFailures(results: SendResult[], to: string[]): void {
158
- results.forEach((result, index) => {
153
+ function reportFailures(results: SendResult[]): void {
154
+ results.forEach((result) => {
159
155
  if (!result.error) return
160
- toast.add({
161
- color: 'error',
162
- title: to[index] ?? result.sendId,
163
- description: processApiMessage(result.error),
164
- })
156
+ presentError(result.error, { fallbackKey: 'error.500.description' })
165
157
  })
166
158
  const sent = results.filter((result) => !result.error).length
167
159
  if (sent) {
@@ -213,14 +205,11 @@ async function submit(): Promise<void> {
213
205
  ...contentOverride(),
214
206
  data: DATA_SETS[dataChoice.value](),
215
207
  })
216
- reportFailures(response.results, to)
208
+ reportFailures(response.results)
217
209
  warnOwnSuppression(response.ownAddressSuppressed)
218
210
  close(true)
219
211
  } catch (error) {
220
- toast.add({
221
- color: 'error',
222
- title: processApiMessage(apiErrorMessage(error)),
223
- })
212
+ presentError(error, { fallbackKey: 'error.500.description' })
224
213
  } finally {
225
214
  sending.value = false
226
215
  }
@@ -2,7 +2,6 @@
2
2
  import type { Component } from 'vue'
3
3
  import { useTemplateVersionsRead } from '../composables/useMailingReads'
4
4
  import type { TemplateStatus, TemplateVersionSummary } from '../types/mailing'
5
- import { apiErrorMessage } from '../utils/api-error'
6
5
  import {
7
6
  changeSummary,
8
7
  formatVersionDate,
@@ -32,10 +31,9 @@ const props = withDefaults(defineProps<VersionHistoryProps>(), {
32
31
  })
33
32
 
34
33
  const modal = useModal()
35
- const toast = useToast()
34
+ const { presentError } = useApiErrorPresenter()
36
35
  const { confirm } = useConfirm()
37
36
  const { t, locale: uiLocale } = useI18n()
38
- const { processApiMessage } = useTranslation()
39
37
  const previewComponent = resolveComponent(PREVIEW_COMPONENT) as Component
40
38
 
41
39
  const readVersions = useTemplateVersionsRead(() => props.templateId)
@@ -47,10 +45,7 @@ async function load(): Promise<void> {
47
45
  try {
48
46
  versions.value = (await readVersions()).versions
49
47
  } catch (error) {
50
- toast.add({
51
- color: 'error',
52
- title: processApiMessage(apiErrorMessage(error)),
53
- })
48
+ presentError(error, { fallbackKey: 'error.500.description' })
54
49
  } finally {
55
50
  loading.value = false
56
51
  }
@@ -35,6 +35,7 @@ const api = useMailingApi()
35
35
  const toast = useToast()
36
36
  const { t, locale } = useI18n()
37
37
  const { dateTimeFormat } = useRegionalFormat()
38
+ const { presentError } = useApiErrorPresenter()
38
39
 
39
40
  const read = useWebhookStatus()
40
41
  /** The status read, `null` when the read failed. */
@@ -84,7 +85,7 @@ async function sendTest(): Promise<void> {
84
85
  : undefined,
85
86
  })
86
87
  } catch (error) {
87
- useApiError(error)
88
+ presentError(error, { fallbackKey: 'error.500.description' })
88
89
  } finally {
89
90
  isTesting.value = false
90
91
  }