@antelopejs/dms-mailing 0.6.0 → 0.7.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 (187) hide show
  1. package/dist/constants.d.ts +1 -0
  2. package/dist/constants.js +2 -1
  3. package/dist/constants.js.map +1 -1
  4. package/dist/data/templates-table.d.ts +4 -1
  5. package/dist/data/templates-table.js +55 -13
  6. package/dist/data/templates-table.js.map +1 -1
  7. package/dist/data-types/categories-type.d.ts +5 -5
  8. package/dist/db/models/templates.model.d.ts +10 -1
  9. package/dist/db/models/templates.model.js +27 -1
  10. package/dist/db/models/templates.model.js.map +1 -1
  11. package/dist/db/tables/templates.table.d.ts +7 -1
  12. package/dist/db/tables/templates.table.js +3 -1
  13. package/dist/db/tables/templates.table.js.map +1 -1
  14. package/dist/engine/formats.d.ts +13 -0
  15. package/dist/engine/formats.js +85 -0
  16. package/dist/engine/formats.js.map +1 -0
  17. package/dist/engine/index.d.ts +1 -0
  18. package/dist/engine/index.js +1 -0
  19. package/dist/engine/index.js.map +1 -1
  20. package/dist/engine/resolve.d.ts +2 -1
  21. package/dist/engine/resolve.js +14 -7
  22. package/dist/engine/resolve.js.map +1 -1
  23. package/dist/engine/tokens.d.ts +2 -1
  24. package/dist/engine/tokens.js +3 -5
  25. package/dist/engine/tokens.js.map +1 -1
  26. package/dist/frontend-module.d.ts +2 -0
  27. package/dist/frontend-module.js +24 -0
  28. package/dist/frontend-module.js.map +1 -0
  29. package/dist/hooks/tenant-export.js +4 -1
  30. package/dist/hooks/tenant-export.js.map +1 -1
  31. package/dist/implementations/dms-mailing/index.d.ts +1 -0
  32. package/dist/implementations/dms-mailing/index.js +3 -1
  33. package/dist/implementations/dms-mailing/index.js.map +1 -1
  34. package/dist/index.js +2 -19
  35. package/dist/index.js.map +1 -1
  36. package/dist/pages/overview.d.ts +1 -1
  37. package/dist/pages/overview.js +11 -13
  38. package/dist/pages/overview.js.map +1 -1
  39. package/dist/pages/sends.d.ts +1 -1
  40. package/dist/pages/sends.js +16 -8
  41. package/dist/pages/sends.js.map +1 -1
  42. package/dist/pages/templates.js +3 -8
  43. package/dist/pages/templates.js.map +1 -1
  44. package/dist/pages/toolbar.d.ts +12 -0
  45. package/dist/pages/toolbar.js +41 -0
  46. package/dist/pages/toolbar.js.map +1 -0
  47. package/dist/realtime.d.ts +2 -2
  48. package/dist/realtime.js +8 -8
  49. package/dist/realtime.js.map +1 -1
  50. package/dist/routes/metrics.d.ts +9 -2
  51. package/dist/routes/metrics.js +90 -35
  52. package/dist/routes/metrics.js.map +1 -1
  53. package/dist/routes/period.d.ts +1 -0
  54. package/dist/routes/period.js +19 -0
  55. package/dist/routes/period.js.map +1 -1
  56. package/dist/routes/sends.js +6 -3
  57. package/dist/routes/sends.js.map +1 -1
  58. package/dist/routes/settings.js +6 -7
  59. package/dist/routes/settings.js.map +1 -1
  60. package/dist/routes/templates.d.ts +8 -4
  61. package/dist/routes/templates.js +40 -15
  62. package/dist/routes/templates.js.map +1 -1
  63. package/dist/services/categories.d.ts +2 -1
  64. package/dist/services/categories.js +9 -0
  65. package/dist/services/categories.js.map +1 -1
  66. package/dist/services/declarations.js +2 -2
  67. package/dist/services/declarations.js.map +1 -1
  68. package/dist/services/events.js +1 -1
  69. package/dist/services/events.js.map +1 -1
  70. package/dist/services/localized-text.d.ts +7 -0
  71. package/dist/services/localized-text.js +28 -0
  72. package/dist/services/localized-text.js.map +1 -0
  73. package/dist/services/send-stat-items.js +34 -12
  74. package/dist/services/send-stat-items.js.map +1 -1
  75. package/dist/services/send-stats.d.ts +7 -0
  76. package/dist/services/send-stats.js +17 -5
  77. package/dist/services/send-stats.js.map +1 -1
  78. package/dist/services/send.d.ts +2 -0
  79. package/dist/services/send.js +11 -12
  80. package/dist/services/send.js.map +1 -1
  81. package/dist/services/sender-declaration.d.ts +2 -0
  82. package/dist/services/sender-declaration.js +18 -0
  83. package/dist/services/sender-declaration.js.map +1 -0
  84. package/dist/services/starters.d.ts +2 -2
  85. package/dist/services/starters.js +1 -10
  86. package/dist/services/starters.js.map +1 -1
  87. package/dist/services/templates.d.ts +11 -5
  88. package/dist/services/templates.js +24 -6
  89. package/dist/services/templates.js.map +1 -1
  90. package/dist/services/webhook-status.d.ts +2 -1
  91. package/dist/services/webhook-status.js +6 -5
  92. package/dist/services/webhook-status.js.map +1 -1
  93. package/dist/types/settings.d.ts +6 -0
  94. package/dist/types/settings.js +25 -5
  95. package/dist/types/settings.js.map +1 -1
  96. package/dist/validation/declarations.schema.d.ts +25 -7
  97. package/dist/validation/declarations.schema.js +16 -3
  98. package/dist/validation/declarations.schema.js.map +1 -1
  99. package/dist/validation/settings.schema.d.ts +25 -25
  100. package/dist/validation/settings.schema.js +30 -6
  101. package/dist/validation/settings.schema.js.map +1 -1
  102. package/dist/validation/templates.schema.d.ts +13 -0
  103. package/dist/validation/templates.schema.js +6 -1
  104. package/dist/validation/templates.schema.js.map +1 -1
  105. package/frontend-vue/app/components/CategoriesInput.vue +15 -13
  106. package/frontend-vue/app/components/FunnelCard.vue +46 -3
  107. package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
  108. package/frontend-vue/app/components/OverviewGate.vue +0 -8
  109. package/frontend-vue/app/components/ProviderPill.vue +50 -0
  110. package/frontend-vue/app/components/SendDrawer.vue +206 -183
  111. package/frontend-vue/app/components/StarterGallery.vue +1 -1
  112. package/frontend-vue/app/components/TemplateCard.vue +4 -1
  113. package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
  114. package/frontend-vue/app/components/TemplateGallery.vue +57 -17
  115. package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
  116. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
  117. package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
  118. package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
  119. package/frontend-vue/app/composables/useMailingApi.ts +3 -3
  120. package/frontend-vue/app/composables/useMailingReads.ts +2 -2
  121. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
  122. package/frontend-vue/app/types/mailing.ts +18 -0
  123. package/frontend-vue/app/utils/categories.ts +58 -49
  124. package/frontend-vue/app/utils/funnel.ts +25 -0
  125. package/frontend-vue/app/utils/period.ts +0 -7
  126. package/frontend-vue/app/utils/starters.ts +3 -1
  127. package/frontend-vue/app/utils/tokens.ts +6 -1
  128. package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
  129. package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
  130. package/frontend-vue/package.json +1 -1
  131. package/frontend-vue/tests/categories-input.test.ts +4 -10
  132. package/frontend-vue/tests/categories.test.ts +55 -17
  133. package/frontend-vue/tests/conditions.test.ts +1 -4
  134. package/frontend-vue/tests/funnel-card.test.ts +27 -0
  135. package/frontend-vue/tests/funnel.test.ts +25 -0
  136. package/frontend-vue/tests/localized-defaults.test.ts +0 -29
  137. package/frontend-vue/tests/new-template-modal.test.ts +1 -1
  138. package/frontend-vue/tests/overview-gate.test.ts +1 -24
  139. package/frontend-vue/tests/page-reads.test.ts +9 -25
  140. package/frontend-vue/tests/period.test.ts +0 -12
  141. package/frontend-vue/tests/replay-toast.test.ts +2 -1
  142. package/frontend-vue/tests/send-drawer.test.ts +123 -0
  143. package/frontend-vue/tests/template-drawer.test.ts +1 -1
  144. package/frontend-vue/tests/template-gallery.test.ts +90 -3
  145. package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
  146. package/package.json +6 -5
  147. package/skills/dms-mailing/SKILL.md +17 -4
  148. package/src/constants.ts +2 -0
  149. package/src/data/sends-table.ts +1 -1
  150. package/src/data/templates-table.ts +123 -19
  151. package/src/db/models/templates.model.ts +61 -3
  152. package/src/db/tables/templates.table.ts +10 -1
  153. package/src/engine/formats.ts +124 -0
  154. package/src/engine/index.ts +1 -0
  155. package/src/engine/resolve.ts +18 -7
  156. package/src/engine/tokens.ts +7 -5
  157. package/src/frontend-module.ts +36 -0
  158. package/src/hooks/tenant-export.ts +8 -1
  159. package/src/implementations/dms-mailing/index.ts +2 -0
  160. package/src/index.ts +2 -23
  161. package/src/pages/overview.ts +20 -16
  162. package/src/pages/sends.ts +27 -11
  163. package/src/pages/templates.ts +4 -8
  164. package/src/pages/toolbar.ts +77 -0
  165. package/src/realtime.ts +17 -8
  166. package/src/routes/metrics.ts +158 -45
  167. package/src/routes/period.ts +32 -0
  168. package/src/routes/sends.ts +13 -4
  169. package/src/routes/settings.ts +11 -8
  170. package/src/routes/templates.ts +72 -26
  171. package/src/services/categories.ts +14 -1
  172. package/src/services/declarations.ts +4 -3
  173. package/src/services/events.ts +1 -1
  174. package/src/services/localized-text.ts +53 -0
  175. package/src/services/send-stat-items.ts +60 -13
  176. package/src/services/send-stats.ts +37 -5
  177. package/src/services/send.ts +19 -20
  178. package/src/services/sender-declaration.ts +27 -0
  179. package/src/services/starters.ts +3 -15
  180. package/src/services/templates.ts +50 -19
  181. package/src/services/webhook-status.ts +3 -3
  182. package/src/types/settings.ts +35 -5
  183. package/src/validation/declarations.schema.ts +29 -3
  184. package/src/validation/settings.schema.ts +56 -14
  185. package/src/validation/templates.schema.ts +10 -0
  186. package/frontend-vue/app/components/PageHeader.vue +0 -94
  187. package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
@@ -267,7 +267,7 @@ async function submit(): Promise<void> {
267
267
  if (!canSubmit.value) return
268
268
  saving.value = true
269
269
  try {
270
- const [id] = await api.createTemplate(creationInput())
270
+ const { _id: id } = await api.createTemplate(creationInput())
271
271
  close(true)
272
272
  if (id) await navigateDms(editorRoute(id))
273
273
  } catch (error) {
@@ -330,7 +330,9 @@ async function submit(): Promise<void> {
330
330
  :ui="{ base: 'font-mono' }"
331
331
  :trailing-icon="slugIssue ? undefined : 'i-ph-check-circle'"
332
332
  :aria-invalid="Boolean(slugError)"
333
- @update:model-value="(value: unknown) => onSlugInput(String(value))"
333
+ @update:model-value="
334
+ (value: unknown) => onSlugInput(String(value))
335
+ "
334
336
  />
335
337
  <UBadge
336
338
  color="neutral"
@@ -36,13 +36,6 @@ const { t } = useI18n()
36
36
  const { open: openModal } = useModal()
37
37
 
38
38
  const { data: facts, error, status, refresh } = useSetupFacts()
39
- /**
40
- * The header's period selector sets up once the dashboard has: a server
41
- * render, which renders the dashboard once the facts are in, would otherwise
42
- * set the selector up first and render the cards with a period the browser's
43
- * cards do not have yet as they hydrate.
44
- */
45
- const DashboardRendered = holdPageHeaderActions()
46
39
  const isResumed = ref(false)
47
40
 
48
41
  /** A failure shows once its retry is over; the facts read last stay meanwhile. */
@@ -207,5 +200,4 @@ function createTemplate(): void {
207
200
 
208
201
  <slot v-if="showsDashboard" />
209
202
  </div>
210
- <DashboardRendered />
211
203
  </template>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { useProviderInfo } from '../composables/useMailingReads'
3
+ import type { MailingComponentProps } from '../types/component'
4
+ import type { ProviderInfo } from '../types/mailing'
5
+
6
+ interface ProviderPillProps extends MailingComponentProps {
7
+ showProvider?: boolean
8
+ }
9
+
10
+ defineOptions({ inheritAttrs: false })
11
+
12
+ const props = withDefaults(defineProps<ProviderPillProps>(), {
13
+ showProvider: true,
14
+ })
15
+
16
+ const { t } = useI18n()
17
+
18
+ /** Shown as "no provider" when the read fails. */
19
+ const NO_PROVIDER: ProviderInfo = { name: '', connected: false, features: null }
20
+
21
+ const providerRead = useProviderInfo(() => props.showProvider)
22
+ const provider = computed<ProviderInfo | null>(() =>
23
+ providerRead.error.value ? NO_PROVIDER : providerRead.data.value,
24
+ )
25
+
26
+ /** The leading child of the page toolbar: is a provider carrying the mail. */
27
+ const pill = computed(() =>
28
+ provider.value?.connected
29
+ ? {
30
+ tone: 'success',
31
+ dot: 'live',
32
+ label: t('dms_mailing.page_header.provider_connected', {
33
+ name: provider.value.name,
34
+ }),
35
+ }
36
+ : {
37
+ tone: 'warning',
38
+ dot: 'static',
39
+ label: t('dms_mailing.page_header.no_provider'),
40
+ },
41
+ )
42
+ </script>
43
+
44
+ <template>
45
+ <DmsStatusPill
46
+ v-if="props.showProvider && provider"
47
+ v-bind="pill"
48
+ :title="t('dms_mailing.page_header.provider_hint')"
49
+ />
50
+ </template>
@@ -412,208 +412,231 @@ onMounted(() => {
412
412
 
413
413
  <template>
414
414
  <!--
415
- As tall as the side panel's body: the send scrolls inside, the actions
416
- stay under it at the bottom, with nothing passing beneath them.
415
+ The drawer's body is the scroll container and sets the inset: the send
416
+ adds no padding of its own. It grows in the body's flex column, so the
417
+ footer sits at the bottom of the drawer, and sticks there once the send
418
+ scrolls.
417
419
  -->
418
- <div class="flex h-full min-h-0 flex-col">
419
- <USkeleton v-if="loading && !send" class="m-5 h-64" />
420
+ <div class="@container flex flex-1 flex-col gap-6" data-send-drawer>
421
+ <USkeleton v-if="loading && !send" class="h-64" />
420
422
 
421
423
  <template v-else-if="send">
422
- <div class="min-h-0 flex-1 overflow-y-auto">
423
- <header
424
- class="border-default flex flex-col gap-1.5 border-b px-5 pb-4 pt-1"
424
+ <header class="border-default flex flex-col gap-1.5 border-b pb-4">
425
+ <div class="flex items-center gap-1">
426
+ <DmsEyebrow
427
+ class="min-w-0 flex-1"
428
+ truncate
429
+ :label="key('eyebrow', { id: shortSendId(send._id) })"
430
+ />
431
+ <UButton
432
+ icon="i-ph-link"
433
+ color="neutral"
434
+ variant="ghost"
435
+ size="sm"
436
+ :aria-label="key('copy_link')"
437
+ @click="copyLink"
438
+ />
439
+ </div>
440
+ <h3
441
+ class="text-highlighted flex flex-wrap items-center gap-2 text-lg font-semibold"
425
442
  >
426
- <div class="flex items-center gap-1">
427
- <DmsEyebrow
428
- class="min-w-0 flex-1"
429
- truncate
430
- :label="key('eyebrow', { id: shortSendId(send._id) })"
431
- />
443
+ <span class="min-w-0 truncate">
444
+ {{ send.recipientName || send.recipientEmail }}
445
+ </span>
446
+ <DmsStatusPill
447
+ :tone="SEND_STATUS_TONES[send.status]"
448
+ :label="t(`dms_mailing.sends.status.${send.status}`)"
449
+ />
450
+ <DmsStatusPill
451
+ v-if="send.isTest"
452
+ tone="warning"
453
+ dot="none"
454
+ size="sm"
455
+ uppercase
456
+ :label="key('test')"
457
+ />
458
+ </h3>
459
+ <!-- The address gives way first: the date drops to its own line
460
+ rather than being cut off with it. -->
461
+ <p
462
+ class="text-muted flex flex-wrap gap-x-1.5 text-[12.5px]"
463
+ data-send-recipient
464
+ >
465
+ <span class="min-w-0 truncate font-mono" :title="send.recipientEmail">
466
+ {{ send.recipientEmail }}
467
+ </span>
468
+ <span class="whitespace-nowrap">
469
+ · {{ dateOf(send.createdAt, DATE_FORMAT) }}
470
+ </span>
471
+ </p>
472
+ </header>
473
+
474
+ <section class="flex flex-col gap-3">
475
+ <!-- Three short values: side by side from a 28rem group (a 600px
476
+ drawer) rather than the DmsStatGroup's own 36rem (dms#399). -->
477
+ <DmsStatGroup
478
+ layout="joined"
479
+ :columns="3"
480
+ columns-from="md"
481
+ :items="stats"
482
+ />
483
+ <DmsBanner
484
+ v-if="problem"
485
+ :tone="isResolved ? 'info' : problemTone(problem.kind)"
486
+ :size="isResolved ? 'sm' : undefined"
487
+ :icon="problem.icon"
488
+ :title="t(problem.titleKey, problem.params)"
489
+ :description="
490
+ isResolved ? undefined : t(problem.descriptionKey, problem.params)
491
+ "
492
+ data-problem-banner
493
+ />
494
+ <DmsBanner
495
+ v-if="suppression"
496
+ :tone="suppression.tone"
497
+ size="sm"
498
+ :icon="suppression.icon"
499
+ :title="t(suppression.titleKey, suppression.params)"
500
+ >
501
+ <template #description>
502
+ <span
503
+ class="text-highlighted block truncate font-mono text-[12px]"
504
+ :title="suppression.params.email"
505
+ data-suppressed-address
506
+ >
507
+ {{ suppression.params.email }}
508
+ </span>
509
+ {{ t(suppression.descriptionKey, suppression.params) }}
510
+ </template>
511
+ <template v-if="suppression.hasListLink" #actions>
432
512
  <UButton
433
- icon="i-ph-link"
513
+ size="xs"
434
514
  color="neutral"
435
- variant="ghost"
436
- size="sm"
437
- :aria-label="key('copy_link')"
438
- @click="copyLink"
439
- />
440
- </div>
441
- <h3
442
- class="text-highlighted flex flex-wrap items-center gap-2 text-lg font-semibold"
443
- >
444
- <span class="min-w-0 truncate">
445
- {{ send.recipientName || send.recipientEmail }}
446
- </span>
447
- <DmsStatusPill
448
- :tone="SEND_STATUS_TONES[send.status]"
449
- :label="t(`dms_mailing.sends.status.${send.status}`)"
515
+ variant="outline"
516
+ trailing-icon="i-ph-arrow-right"
517
+ :label="t('dms_mailing.suppressions.notice.open_list')"
518
+ @click="openSuppressionList"
450
519
  />
451
- <DmsStatusPill
452
- v-if="send.isTest"
453
- tone="warning"
454
- dot="none"
455
- size="sm"
456
- uppercase
457
- :label="key('test')"
458
- />
459
- </h3>
460
- <p class="text-muted truncate text-[12.5px]">
461
- <span class="font-mono">{{ send.recipientEmail }}</span>
462
- · {{ dateOf(send.createdAt, DATE_FORMAT) }}
463
- </p>
464
- </header>
465
-
466
- <div class="flex flex-1 flex-col gap-6 p-5">
467
- <section class="flex flex-col gap-3">
468
- <DmsStatGroup layout="joined" :columns="3" :items="stats" />
469
- <DmsBanner
470
- v-if="problem"
471
- :tone="isResolved ? 'info' : problemTone(problem.kind)"
472
- :size="isResolved ? 'sm' : undefined"
473
- :icon="problem.icon"
474
- :title="t(problem.titleKey, problem.params)"
475
- :description="
476
- isResolved
477
- ? undefined
478
- : t(problem.descriptionKey, problem.params)
479
- "
480
- data-problem-banner
520
+ </template>
521
+ </DmsBanner>
522
+ <DmsBanner
523
+ v-for="link in linkedSends"
524
+ :key="link.id"
525
+ tone="info"
526
+ size="sm"
527
+ icon="i-ph-arrow-clockwise"
528
+ :title="link.text"
529
+ >
530
+ <template #actions>
531
+ <UButton
532
+ v-if="showRow"
533
+ size="xs"
534
+ color="neutral"
535
+ variant="outline"
536
+ trailing-icon="i-ph-arrow-right"
537
+ :label="key('see_send')"
538
+ @click="void showSend(link.id)"
481
539
  />
482
- <DmsBanner
483
- v-if="suppression"
484
- :tone="suppression.tone"
485
- size="sm"
486
- :icon="suppression.icon"
487
- :title="t(suppression.titleKey, suppression.params)"
540
+ </template>
541
+ </DmsBanner>
542
+ </section>
543
+
544
+ <!--
545
+ Side by side once the drawer is resized wide:
546
+ both are short lists, and the data sent below keeps the full width.
547
+ -->
548
+ <div class="@2xl:grid-cols-2 grid gap-6" data-send-details-grid>
549
+ <section class="flex min-w-0 flex-col gap-2">
550
+ <DmsEyebrow :label="key('timeline')" />
551
+ <ol class="flex flex-col">
552
+ <li
553
+ v-for="item in timeline"
554
+ :key="item.id"
555
+ :class="item.isPending && 'opacity-50'"
488
556
  >
489
- <template #description>
557
+ <DmsActivityItem
558
+ :icon="item.icon"
559
+ :icon-color="item.isPending ? 'neutral' : item.tone"
560
+ :subtitle="
561
+ item.isPending ? key('pending') : item.detail || undefined
562
+ "
563
+ :mono="item.isDetailMono && !item.isPending"
564
+ :trailing="stepTime(item)"
565
+ :interactive="false"
566
+ wrap
567
+ >
490
568
  <span
491
- class="text-highlighted block truncate font-mono text-[12px]"
492
- :title="suppression.params.email"
493
- data-suppressed-address
569
+ :class="
570
+ item.tone === 'error' && !item.isPending
571
+ ? 'text-error'
572
+ : undefined
573
+ "
494
574
  >
495
- {{ suppression.params.email }}
575
+ {{ stepTitle(item) }}
496
576
  </span>
497
- {{ t(suppression.descriptionKey, suppression.params) }}
498
- </template>
499
- <template v-if="suppression.hasListLink" #actions>
500
- <UButton
501
- size="xs"
502
- color="neutral"
503
- variant="outline"
504
- trailing-icon="i-ph-arrow-right"
505
- :label="t('dms_mailing.suppressions.notice.open_list')"
506
- @click="openSuppressionList"
507
- />
508
- </template>
509
- </DmsBanner>
510
- <DmsBanner
511
- v-for="link in linkedSends"
512
- :key="link.id"
513
- tone="info"
577
+ </DmsActivityItem>
578
+ </li>
579
+ </ol>
580
+ </section>
581
+
582
+ <section class="flex min-w-0 flex-col gap-2">
583
+ <DmsEyebrow :label="key('origin')" />
584
+ <div
585
+ class="border-default divide-default divide-y overflow-hidden rounded-lg border"
586
+ >
587
+ <DmsListRow
588
+ :to="editorLink"
589
+ icon="i-ph-envelope-simple"
590
+ icon-size="2xs"
514
591
  size="sm"
515
- icon="i-ph-arrow-clockwise"
516
- :title="link.text"
592
+ :title="originTitle"
517
593
  >
518
- <template #actions>
519
- <UButton
520
- v-if="showRow"
521
- size="xs"
522
- color="neutral"
523
- variant="outline"
524
- trailing-icon="i-ph-arrow-right"
525
- :label="key('see_send')"
526
- @click="void showSend(link.id)"
594
+ <template #trailing>
595
+ <UIcon
596
+ name="i-ph-arrow-square-out"
597
+ class="text-dimmed size-4"
598
+ aria-hidden="true"
527
599
  />
528
600
  </template>
529
- </DmsBanner>
530
- </section>
531
-
532
- <section class="flex flex-col gap-2">
533
- <DmsEyebrow :label="key('timeline')" />
534
- <ol class="flex flex-col">
535
- <li
536
- v-for="item in timeline"
537
- :key="item.id"
538
- :class="item.isPending && 'opacity-50'"
539
- >
540
- <DmsActivityItem
541
- :icon="item.icon"
542
- :icon-color="item.isPending ? 'neutral' : item.tone"
543
- :subtitle="
544
- item.isPending ? key('pending') : item.detail || undefined
545
- "
546
- :mono="item.isDetailMono && !item.isPending"
547
- :trailing="stepTime(item)"
548
- :interactive="false"
549
- >
550
- <span
551
- :class="
552
- item.tone === 'error' && !item.isPending
553
- ? 'text-error'
554
- : undefined
555
- "
556
- >
557
- {{ stepTitle(item) }}
558
- </span>
559
- </DmsActivityItem>
560
- </li>
561
- </ol>
562
- </section>
563
-
564
- <section class="flex flex-col gap-2">
565
- <DmsEyebrow :label="key('origin')" />
566
- <div
567
- class="border-default divide-default divide-y overflow-hidden rounded-lg border"
601
+ </DmsListRow>
602
+ <!-- The "source" label goes under the value: beside it, a dotted
603
+ name like `automation.overdue-reminders` broke at its hyphen
604
+ against the label in a narrow drawer. -->
605
+ <DmsListRow
606
+ v-if="send.source"
607
+ icon="i-ph-code"
608
+ icon-size="2xs"
609
+ size="sm"
610
+ :meta="[key('source')]"
611
+ data-send-source
568
612
  >
569
- <DmsListRow
570
- :to="editorLink"
571
- icon="i-ph-envelope-simple"
572
- icon-size="2xs"
573
- size="sm"
574
- :title="originTitle"
575
- >
576
- <template #trailing>
577
- <UIcon
578
- name="i-ph-arrow-square-out"
579
- class="text-dimmed size-4"
580
- aria-hidden="true"
581
- />
582
- </template>
583
- </DmsListRow>
584
- <DmsListRow
585
- v-if="send.source"
586
- icon="i-ph-code"
587
- icon-size="2xs"
588
- size="sm"
589
- :trailing="key('source')"
613
+ <span
614
+ class="min-w-0 font-mono text-[12.5px] [overflow-wrap:anywhere]"
590
615
  >
591
- <span class="font-mono text-[12.5px]">{{ send.source }}</span>
592
- </DmsListRow>
593
- </div>
594
- </section>
595
-
596
- <section class="flex flex-col gap-2">
597
- <div class="flex items-center justify-between gap-2">
598
- <DmsEyebrow :label="key('payload')" />
599
- <UButton
600
- v-if="payload"
601
- size="xs"
602
- variant="link"
603
- :label="key('copy_json')"
604
- @click="copyPayload"
605
- />
606
- </div>
607
- <pre
608
- class="bg-elevated/50 border-default text-toned max-h-72 overflow-auto rounded-lg border p-3 font-mono text-[11.5px] leading-relaxed"
609
- >{{ payload || '{}' }}</pre>
610
- </section>
611
- </div>
616
+ {{ send.source }}
617
+ </span>
618
+ </DmsListRow>
619
+ </div>
620
+ </section>
612
621
  </div>
613
622
 
614
- <footer
615
- class="bg-default border-default flex shrink-0 items-center gap-2 border-t px-5 py-3"
616
- >
623
+ <section class="flex flex-col gap-2">
624
+ <div class="flex items-center justify-between gap-2">
625
+ <DmsEyebrow :label="key('payload')" />
626
+ <UButton
627
+ v-if="payload"
628
+ size="xs"
629
+ variant="link"
630
+ :label="key('copy_json')"
631
+ @click="copyPayload"
632
+ />
633
+ </div>
634
+ <pre
635
+ class="bg-elevated/50 border-default text-toned max-h-72 overflow-auto rounded-lg border p-3 font-mono text-[11.5px] leading-relaxed"
636
+ >{{ payload || '{}' }}</pre>
637
+ </section>
638
+
639
+ <DmsContainerFooter data-send-footer>
617
640
  <UButton
618
641
  v-for="(action, index) in actions"
619
642
  :key="action.id"
@@ -628,7 +651,7 @@ onMounted(() => {
628
651
  :loading="action.id === 'send_again' && sending"
629
652
  @click="ACTION_HANDLERS[action.id]()"
630
653
  />
631
- </footer>
654
+ </DmsContainerFooter>
632
655
  </template>
633
656
  </div>
634
657
  </template>
@@ -56,7 +56,7 @@ async function createFrom(starter: StarterSummary): Promise<void> {
56
56
  if (creatingId.value) return
57
57
  creatingId.value = starter.id
58
58
  try {
59
- const [id] = await api.createTemplate({
59
+ const { _id: id } = await api.createTemplate({
60
60
  name: starter.name,
61
61
  slug: starter.slug,
62
62
  category: knownCategory(starter.category),
@@ -17,6 +17,9 @@ import type {
17
17
  TemplateStats,
18
18
  TemplateStatus,
19
19
  } from '../types/mailing'
20
+ // Imported rather than resolved by name: every module component is a lazy
21
+ // chunk, and a card would first render without its thumbnail.
22
+ import TemplatePreviewFrame from './TemplatePreviewFrame.vue'
20
23
 
21
24
  interface TemplateCardProps {
22
25
  template: TemplateRow
@@ -172,7 +175,7 @@ function chipLabel(code: string, present: boolean): string {
172
175
  class="bg-elevated/40 relative flex h-[172px] justify-center overflow-hidden bg-[radial-gradient(var(--ui-border)_1px,transparent_1px)] bg-[length:14px_14px] pt-4"
173
176
  >
174
177
  <div class="pointer-events-none rounded-t-md shadow-sm">
175
- <MailingTemplatePreviewFrame
178
+ <TemplatePreviewFrame
176
179
  :template-id="template._id"
177
180
  :version="previewVersion"
178
181
  :width="PREVIEW_WIDTH"
@@ -513,7 +513,7 @@ function onResolved(preview: PreviewResponse): void {
513
513
  </script>
514
514
 
515
515
  <template>
516
- <div class="flex w-[30rem] max-w-full flex-col">
516
+ <div class="flex w-[30rem] max-w-full flex-1 flex-col">
517
517
  <USkeleton v-if="loading && !template" class="h-64 w-full" />
518
518
 
519
519
  <template v-else-if="template">
@@ -882,9 +882,7 @@ function onResolved(preview: PreviewResponse): void {
882
882
  />
883
883
  </div>
884
884
 
885
- <div
886
- class="border-default bg-default sticky bottom-0 -mx-1 mt-auto flex items-center gap-2 border-t px-1 py-3"
887
- >
885
+ <DmsContainerFooter data-template-footer>
888
886
  <UButton
889
887
  icon="i-ph-flask"
890
888
  color="neutral"
@@ -905,7 +903,7 @@ function onResolved(preview: PreviewResponse): void {
905
903
  :label="t('dms_mailing.templates.actions.open_editor')"
906
904
  @click="openEditor(locale)"
907
905
  />
908
- </div>
906
+ </DmsContainerFooter>
909
907
  </template>
910
908
  </div>
911
909
  </template>