@antelopejs/dms-mailing 0.3.4 → 0.4.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.
Files changed (707) hide show
  1. package/README.md +232 -22
  2. package/dist/automation/email-event-trigger.js +2 -2
  3. package/dist/automation/email-event-trigger.js.map +1 -1
  4. package/dist/automation/send-template-action.js +2 -2
  5. package/dist/automation/send-template-action.js.map +1 -1
  6. package/dist/config.js +3 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.d.ts +10 -1
  9. package/dist/constants.js +15 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/crons/attention.d.ts +3 -0
  12. package/dist/crons/attention.js +19 -0
  13. package/dist/crons/attention.js.map +1 -0
  14. package/dist/crons/index.d.ts +2 -0
  15. package/dist/crons/index.js +2 -0
  16. package/dist/crons/index.js.map +1 -1
  17. package/dist/crons/jobs.d.ts +12 -0
  18. package/dist/crons/jobs.js +27 -0
  19. package/dist/crons/jobs.js.map +1 -0
  20. package/dist/crons/retention.d.ts +2 -4
  21. package/dist/crons/retention.js +7 -22
  22. package/dist/crons/retention.js.map +1 -1
  23. package/dist/data/displays.d.ts +5 -0
  24. package/dist/data/displays.js +23 -0
  25. package/dist/data/displays.js.map +1 -0
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.js +1 -0
  28. package/dist/data/index.js.map +1 -1
  29. package/dist/data/locale-fallback.d.ts +3 -0
  30. package/dist/data/locale-fallback.js +20 -0
  31. package/dist/data/locale-fallback.js.map +1 -0
  32. package/dist/data/read-only-routes.d.ts +7 -0
  33. package/dist/data/read-only-routes.js +13 -0
  34. package/dist/data/read-only-routes.js.map +1 -0
  35. package/dist/data/sends-table.d.ts +19 -5
  36. package/dist/data/sends-table.js +185 -17
  37. package/dist/data/sends-table.js.map +1 -1
  38. package/dist/data/suppressions-table.d.ts +24 -0
  39. package/dist/data/suppressions-table.js +120 -0
  40. package/dist/data/suppressions-table.js.map +1 -0
  41. package/dist/data/templates-table.d.ts +20 -2
  42. package/dist/data/templates-table.js +188 -31
  43. package/dist/data/templates-table.js.map +1 -1
  44. package/dist/data-types/categories-type.d.ts +11 -13
  45. package/dist/data-types/categories-type.js +3 -3
  46. package/dist/data-types/categories-type.js.map +1 -1
  47. package/dist/data-types/category-type.js +1 -1
  48. package/dist/data-types/category-type.js.map +1 -1
  49. package/dist/db/models/index.d.ts +3 -0
  50. package/dist/db/models/index.js +3 -0
  51. package/dist/db/models/index.js.map +1 -1
  52. package/dist/db/models/sends.model.d.ts +12 -1
  53. package/dist/db/models/sends.model.js +64 -0
  54. package/dist/db/models/sends.model.js.map +1 -1
  55. package/dist/db/models/settings.model.d.ts +0 -1
  56. package/dist/db/models/settings.model.js +0 -7
  57. package/dist/db/models/settings.model.js.map +1 -1
  58. package/dist/db/models/suppressions.model.d.ts +28 -0
  59. package/dist/db/models/suppressions.model.js +46 -0
  60. package/dist/db/models/suppressions.model.js.map +1 -0
  61. package/dist/db/models/template-versions.model.d.ts +24 -0
  62. package/dist/db/models/template-versions.model.js +17 -0
  63. package/dist/db/models/template-versions.model.js.map +1 -0
  64. package/dist/db/models/templates.model.d.ts +2 -0
  65. package/dist/db/models/templates.model.js +3 -0
  66. package/dist/db/models/templates.model.js.map +1 -1
  67. package/dist/db/models/webhook-status.model.d.ts +24 -0
  68. package/dist/db/models/webhook-status.model.js +21 -0
  69. package/dist/db/models/webhook-status.model.js.map +1 -0
  70. package/dist/db/tables/index.d.ts +3 -0
  71. package/dist/db/tables/index.js +3 -0
  72. package/dist/db/tables/index.js.map +1 -1
  73. package/dist/db/tables/send-events.table.d.ts +2 -2
  74. package/dist/db/tables/send-events.table.js.map +1 -1
  75. package/dist/db/tables/sends.table.d.ts +10 -2
  76. package/dist/db/tables/sends.table.js +34 -1
  77. package/dist/db/tables/sends.table.js.map +1 -1
  78. package/dist/db/tables/suppressions.table.d.ts +12 -0
  79. package/dist/db/tables/suppressions.table.js +53 -0
  80. package/dist/db/tables/suppressions.table.js.map +1 -0
  81. package/dist/db/tables/template-versions.table.d.ts +13 -0
  82. package/dist/db/tables/template-versions.table.js +59 -0
  83. package/dist/db/tables/template-versions.table.js.map +1 -0
  84. package/dist/db/tables/templates.table.d.ts +11 -0
  85. package/dist/db/tables/templates.table.js +45 -0
  86. package/dist/db/tables/templates.table.js.map +1 -1
  87. package/dist/db/tables/webhook-status.table.d.ts +14 -0
  88. package/dist/db/tables/webhook-status.table.js +58 -0
  89. package/dist/db/tables/webhook-status.table.js.map +1 -0
  90. package/dist/engine/changes.d.ts +12 -0
  91. package/dist/engine/changes.js +107 -0
  92. package/dist/engine/changes.js.map +1 -0
  93. package/dist/engine/conditions.js +0 -2
  94. package/dist/engine/conditions.js.map +1 -1
  95. package/dist/engine/coverage.d.ts +1 -0
  96. package/dist/engine/coverage.js +6 -2
  97. package/dist/engine/coverage.js.map +1 -1
  98. package/dist/engine/fallback-html.d.ts +1 -1
  99. package/dist/engine/fallback-html.js +51 -34
  100. package/dist/engine/fallback-html.js.map +1 -1
  101. package/dist/engine/index.d.ts +3 -0
  102. package/dist/engine/index.js +3 -0
  103. package/dist/engine/index.js.map +1 -1
  104. package/dist/engine/plain-text.d.ts +3 -0
  105. package/dist/engine/plain-text.js +60 -0
  106. package/dist/engine/plain-text.js.map +1 -0
  107. package/dist/engine/resolve.d.ts +2 -1
  108. package/dist/engine/resolve.js +17 -9
  109. package/dist/engine/resolve.js.map +1 -1
  110. package/dist/engine/system-variables.d.ts +10 -0
  111. package/dist/engine/system-variables.js +70 -0
  112. package/dist/engine/system-variables.js.map +1 -0
  113. package/dist/engine/tokens.d.ts +2 -1
  114. package/dist/engine/tokens.js +6 -2
  115. package/dist/engine/tokens.js.map +1 -1
  116. package/dist/hooks/tenant-export.js +13 -1
  117. package/dist/hooks/tenant-export.js.map +1 -1
  118. package/dist/implementations/dms-mailing/index.d.ts +1 -1
  119. package/dist/implementations/dms-mailing/index.js +2 -2
  120. package/dist/implementations/dms-mailing/index.js.map +1 -1
  121. package/dist/index.js +13 -11
  122. package/dist/index.js.map +1 -1
  123. package/dist/pages/editor.js +9 -2
  124. package/dist/pages/editor.js.map +1 -1
  125. package/dist/pages/index.d.ts +1 -0
  126. package/dist/pages/index.js +1 -0
  127. package/dist/pages/index.js.map +1 -1
  128. package/dist/pages/module.d.ts +2 -0
  129. package/dist/pages/module.js +26 -2
  130. package/dist/pages/module.js.map +1 -1
  131. package/dist/pages/overview.d.ts +2 -6
  132. package/dist/pages/overview.js +109 -52
  133. package/dist/pages/overview.js.map +1 -1
  134. package/dist/pages/sends.d.ts +3 -4
  135. package/dist/pages/sends.js +197 -60
  136. package/dist/pages/sends.js.map +1 -1
  137. package/dist/pages/settings/form.js +114 -53
  138. package/dist/pages/settings/form.js.map +1 -1
  139. package/dist/pages/settings/page.d.ts +0 -1
  140. package/dist/pages/settings/page.js +8 -11
  141. package/dist/pages/settings/page.js.map +1 -1
  142. package/dist/pages/suppressions.d.ts +9 -0
  143. package/dist/pages/suppressions.js +151 -0
  144. package/dist/pages/suppressions.js.map +1 -0
  145. package/dist/pages/templates.d.ts +0 -1
  146. package/dist/pages/templates.js +115 -41
  147. package/dist/pages/templates.js.map +1 -1
  148. package/dist/permissions.js +2 -2
  149. package/dist/permissions.js.map +1 -1
  150. package/dist/quick-actions.js +7 -4
  151. package/dist/quick-actions.js.map +1 -1
  152. package/dist/routes/events.d.ts +8 -5
  153. package/dist/routes/events.js +55 -24
  154. package/dist/routes/events.js.map +1 -1
  155. package/dist/routes/index.d.ts +2 -0
  156. package/dist/routes/index.js +2 -0
  157. package/dist/routes/index.js.map +1 -1
  158. package/dist/routes/metrics.d.ts +60 -23
  159. package/dist/routes/metrics.js +231 -83
  160. package/dist/routes/metrics.js.map +1 -1
  161. package/dist/routes/period.d.ts +2 -1
  162. package/dist/routes/period.js +9 -3
  163. package/dist/routes/period.js.map +1 -1
  164. package/dist/routes/sends.d.ts +49 -8
  165. package/dist/routes/sends.js +239 -24
  166. package/dist/routes/sends.js.map +1 -1
  167. package/dist/routes/settings.d.ts +44 -5
  168. package/dist/routes/settings.js +190 -15
  169. package/dist/routes/settings.js.map +1 -1
  170. package/dist/routes/suppressions.d.ts +16 -0
  171. package/dist/routes/suppressions.js +92 -0
  172. package/dist/routes/suppressions.js.map +1 -0
  173. package/dist/routes/template-versions.d.ts +28 -0
  174. package/dist/routes/template-versions.js +87 -0
  175. package/dist/routes/template-versions.js.map +1 -0
  176. package/dist/routes/templates.d.ts +60 -6
  177. package/dist/routes/templates.js +204 -35
  178. package/dist/routes/templates.js.map +1 -1
  179. package/dist/services/attention.d.ts +19 -6
  180. package/dist/services/attention.js +177 -12
  181. package/dist/services/attention.js.map +1 -1
  182. package/dist/services/catalog.d.ts +3 -0
  183. package/dist/services/catalog.js +25 -0
  184. package/dist/services/catalog.js.map +1 -0
  185. package/dist/services/categories.d.ts +8 -32
  186. package/dist/services/categories.js +14 -25
  187. package/dist/services/categories.js.map +1 -1
  188. package/dist/services/checked-send.d.ts +9 -0
  189. package/dist/services/checked-send.js +54 -0
  190. package/dist/services/checked-send.js.map +1 -0
  191. package/dist/services/events.d.ts +5 -0
  192. package/dist/services/events.js +102 -9
  193. package/dist/services/events.js.map +1 -1
  194. package/dist/services/metrics.d.ts +17 -6
  195. package/dist/services/metrics.js +57 -18
  196. package/dist/services/metrics.js.map +1 -1
  197. package/dist/services/notify.d.ts +18 -10
  198. package/dist/services/notify.js +82 -37
  199. package/dist/services/notify.js.map +1 -1
  200. package/dist/services/provider-banner.d.ts +14 -0
  201. package/dist/services/provider-banner.js +115 -0
  202. package/dist/services/provider-banner.js.map +1 -0
  203. package/dist/services/render.js.map +1 -1
  204. package/dist/services/send-stat-items.d.ts +7 -0
  205. package/dist/services/send-stat-items.js +127 -0
  206. package/dist/services/send-stat-items.js.map +1 -0
  207. package/dist/services/send-stats.d.ts +23 -0
  208. package/dist/services/send-stats.js +63 -0
  209. package/dist/services/send-stats.js.map +1 -0
  210. package/dist/services/send.d.ts +18 -3
  211. package/dist/services/send.js +81 -17
  212. package/dist/services/send.js.map +1 -1
  213. package/dist/services/settings.d.ts +5 -0
  214. package/dist/services/settings.js +28 -7
  215. package/dist/services/settings.js.map +1 -1
  216. package/dist/services/starters.d.ts +15 -0
  217. package/dist/services/starters.js +264 -0
  218. package/dist/services/starters.js.map +1 -0
  219. package/dist/services/suppressions.d.ts +20 -0
  220. package/dist/services/suppressions.js +53 -0
  221. package/dist/services/suppressions.js.map +1 -0
  222. package/dist/services/template-attention.d.ts +4 -0
  223. package/dist/services/template-attention.js +43 -0
  224. package/dist/services/template-attention.js.map +1 -0
  225. package/dist/services/template-stats.d.ts +27 -0
  226. package/dist/services/template-stats.js +59 -0
  227. package/dist/services/template-stats.js.map +1 -0
  228. package/dist/services/templates.d.ts +31 -4
  229. package/dist/services/templates.js +240 -21
  230. package/dist/services/templates.js.map +1 -1
  231. package/dist/services/webhook-status.d.ts +27 -0
  232. package/dist/services/webhook-status.js +134 -0
  233. package/dist/services/webhook-status.js.map +1 -0
  234. package/dist/types/blocks.d.ts +1 -0
  235. package/dist/types/index.d.ts +2 -0
  236. package/dist/types/index.js +2 -0
  237. package/dist/types/index.js.map +1 -1
  238. package/dist/types/sends.d.ts +10 -1
  239. package/dist/types/sends.js +35 -2
  240. package/dist/types/sends.js.map +1 -1
  241. package/dist/types/suppressions.d.ts +9 -0
  242. package/dist/types/suppressions.js +10 -0
  243. package/dist/types/suppressions.js.map +1 -0
  244. package/dist/types/webhook.d.ts +17 -0
  245. package/dist/types/webhook.js +3 -0
  246. package/dist/types/webhook.js.map +1 -0
  247. package/dist/validation/settings.schema.d.ts +144 -9
  248. package/dist/validation/settings.schema.js +113 -16
  249. package/dist/validation/settings.schema.js.map +1 -1
  250. package/dist/validation/templates.schema.d.ts +137 -10
  251. package/dist/validation/templates.schema.js +28 -2
  252. package/dist/validation/templates.schema.js.map +1 -1
  253. package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
  254. package/frontend-vue/app/build/components/FormRows.vue +26 -0
  255. package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
  256. package/frontend-vue/app/components/CategoriesInput.vue +323 -77
  257. package/frontend-vue/app/components/CategoryInput.vue +13 -14
  258. package/frontend-vue/app/components/CategoryLabel.vue +34 -0
  259. package/frontend-vue/app/components/DuplicateModal.vue +201 -0
  260. package/frontend-vue/app/components/Editor.vue +55 -20
  261. package/frontend-vue/app/components/FixAddressModal.vue +133 -0
  262. package/frontend-vue/app/components/FunnelCard.vue +146 -43
  263. package/frontend-vue/app/components/LocaleSelect.vue +98 -0
  264. package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
  265. package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
  266. package/frontend-vue/app/components/OverviewGate.vue +220 -0
  267. package/frontend-vue/app/components/PageHeader.vue +98 -0
  268. package/frontend-vue/app/components/RealSendModal.vue +555 -0
  269. package/frontend-vue/app/components/RecipientsField.vue +41 -0
  270. package/frontend-vue/app/components/RenderingModal.vue +189 -0
  271. package/frontend-vue/app/components/RetentionInput.vue +212 -0
  272. package/frontend-vue/app/components/SendDrawer.vue +542 -209
  273. package/frontend-vue/app/components/StarterGallery.vue +153 -0
  274. package/frontend-vue/app/components/SystemVariables.vue +63 -0
  275. package/frontend-vue/app/components/TemplateCard.vue +218 -57
  276. package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
  277. package/frontend-vue/app/components/TemplateGallery.vue +312 -56
  278. package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
  279. package/frontend-vue/app/components/TestSendModal.vue +266 -95
  280. package/frontend-vue/app/components/TokenText.vue +61 -10
  281. package/frontend-vue/app/components/VariablePath.vue +20 -0
  282. package/frontend-vue/app/components/VersionHistory.vue +196 -0
  283. package/frontend-vue/app/components/VersionPreview.vue +164 -0
  284. package/frontend-vue/app/components/WebhookStatus.vue +130 -0
  285. package/frontend-vue/app/components/blocks/Button.vue +1 -1
  286. package/frontend-vue/app/components/blocks/Code.vue +1 -1
  287. package/frontend-vue/app/components/blocks/Footer.vue +56 -7
  288. package/frontend-vue/app/components/blocks/Heading.vue +1 -1
  289. package/frontend-vue/app/components/blocks/Hero.vue +13 -7
  290. package/frontend-vue/app/components/blocks/List.vue +50 -6
  291. package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
  292. package/frontend-vue/app/components/blocks/Total.vue +3 -3
  293. package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
  294. package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
  295. package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
  296. package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
  297. package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
  298. package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
  299. package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
  300. package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
  301. package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
  302. package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
  303. package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
  304. package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
  305. package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
  306. package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
  307. package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
  308. package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
  309. package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
  310. package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
  311. package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
  312. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
  313. package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
  314. package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
  315. package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
  316. package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
  317. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
  318. package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
  319. package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
  320. package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
  321. package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
  322. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
  323. package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
  324. package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
  325. package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
  326. package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
  327. package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
  328. package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
  329. package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
  330. package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
  331. package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
  332. package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
  333. package/frontend-vue/app/composables/useBlockContext.ts +16 -0
  334. package/frontend-vue/app/composables/useEditorActions.ts +381 -0
  335. package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
  336. package/frontend-vue/app/composables/useEditorContext.ts +30 -23
  337. package/frontend-vue/app/composables/useEditorSession.ts +299 -0
  338. package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
  339. package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
  340. package/frontend-vue/app/composables/useMailingApi.ts +142 -27
  341. package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
  342. package/frontend-vue/app/composables/useReplayToast.ts +72 -0
  343. package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
  344. package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
  345. package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
  346. package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
  347. package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
  348. package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
  349. package/frontend-vue/app/plugins/column-displays.ts +65 -0
  350. package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
  351. package/frontend-vue/app/types/mailing.ts +193 -18
  352. package/frontend-vue/app/utils/api-error.ts +33 -0
  353. package/frontend-vue/app/utils/autocomplete.ts +120 -0
  354. package/frontend-vue/app/utils/blocks/components.ts +10 -10
  355. package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
  356. package/frontend-vue/app/utils/blocks/factory.ts +199 -38
  357. package/frontend-vue/app/utils/blocks/fields.ts +39 -19
  358. package/frontend-vue/app/utils/blocks/index.ts +1 -0
  359. package/frontend-vue/app/utils/blocks/outline.ts +91 -0
  360. package/frontend-vue/app/utils/blocks/tree.ts +114 -2
  361. package/frontend-vue/app/utils/categories.ts +201 -15
  362. package/frontend-vue/app/utils/changes.ts +152 -0
  363. package/frontend-vue/app/utils/conditions.ts +25 -19
  364. package/frontend-vue/app/utils/editor-checks.ts +266 -0
  365. package/frontend-vue/app/utils/editor-route.ts +43 -4
  366. package/frontend-vue/app/utils/funnel.ts +74 -0
  367. package/frontend-vue/app/utils/gallery.ts +179 -1
  368. package/frontend-vue/app/utils/history.ts +66 -0
  369. package/frontend-vue/app/utils/lifecycle.ts +185 -0
  370. package/frontend-vue/app/utils/locales.ts +107 -2
  371. package/frontend-vue/app/utils/overview-setup.ts +83 -0
  372. package/frontend-vue/app/utils/pages.ts +5 -0
  373. package/frontend-vue/app/utils/paths.ts +51 -15
  374. package/frontend-vue/app/utils/period.ts +62 -22
  375. package/frontend-vue/app/utils/problems.ts +181 -0
  376. package/frontend-vue/app/utils/recipients.ts +49 -0
  377. package/frontend-vue/app/utils/rendering-title.ts +23 -0
  378. package/frontend-vue/app/utils/send-actions.ts +207 -0
  379. package/frontend-vue/app/utils/send-link.ts +28 -0
  380. package/frontend-vue/app/utils/send-snippet.ts +97 -0
  381. package/frontend-vue/app/utils/send-stats.ts +24 -0
  382. package/frontend-vue/app/utils/send-values.ts +156 -0
  383. package/frontend-vue/app/utils/shortcuts.ts +113 -0
  384. package/frontend-vue/app/utils/slug.ts +82 -0
  385. package/frontend-vue/app/utils/starters.ts +23 -0
  386. package/frontend-vue/app/utils/suppression.ts +112 -0
  387. package/frontend-vue/app/utils/system-variables.ts +165 -0
  388. package/frontend-vue/app/utils/template-attention.ts +104 -0
  389. package/frontend-vue/app/utils/timeline.ts +221 -19
  390. package/frontend-vue/app/utils/tokens.ts +115 -13
  391. package/frontend-vue/app/utils/variable-types.ts +23 -0
  392. package/frontend-vue/app/utils/variables.ts +208 -1
  393. package/frontend-vue/app/utils/version-history.ts +53 -0
  394. package/frontend-vue/app/utils/webhook-status.ts +170 -0
  395. package/frontend-vue/dms.frontend.ts +19 -11
  396. package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
  397. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
  398. package/frontend-vue/package.json +2 -2
  399. package/frontend-vue/pnpm-lock.yaml +46 -161
  400. package/frontend-vue/tests/api-error.test.ts +27 -0
  401. package/frontend-vue/tests/autocomplete.test.ts +84 -0
  402. package/frontend-vue/tests/block-components.test.ts +0 -3
  403. package/frontend-vue/tests/block-fields.test.ts +12 -2
  404. package/frontend-vue/tests/blocks-factory.test.ts +46 -0
  405. package/frontend-vue/tests/blocks-outline.test.ts +102 -0
  406. package/frontend-vue/tests/blocks-tree.test.ts +28 -1
  407. package/frontend-vue/tests/categories-input.test.ts +140 -0
  408. package/frontend-vue/tests/categories.test.ts +148 -15
  409. package/frontend-vue/tests/changes.test.ts +136 -0
  410. package/frontend-vue/tests/column-displays.test.ts +73 -0
  411. package/frontend-vue/tests/conditions.test.ts +49 -21
  412. package/frontend-vue/tests/dnd.test.ts +10 -0
  413. package/frontend-vue/tests/editor-block-list.test.ts +25 -2
  414. package/frontend-vue/tests/editor-checks.test.ts +178 -0
  415. package/frontend-vue/tests/editor-route.test.ts +37 -9
  416. package/frontend-vue/tests/editor-session.test.ts +207 -0
  417. package/frontend-vue/tests/footer-block.test.ts +89 -0
  418. package/frontend-vue/tests/funnel-card.test.ts +117 -0
  419. package/frontend-vue/tests/funnel.test.ts +81 -0
  420. package/frontend-vue/tests/gallery.test.ts +124 -2
  421. package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
  422. package/frontend-vue/tests/history.test.ts +48 -0
  423. package/frontend-vue/tests/i18n-editor.test.ts +37 -8
  424. package/frontend-vue/tests/i18n.test.ts +55 -0
  425. package/frontend-vue/tests/lifecycle.test.ts +145 -0
  426. package/frontend-vue/tests/locales.test.ts +60 -0
  427. package/frontend-vue/tests/localized-defaults.test.ts +54 -0
  428. package/frontend-vue/tests/new-template-modal.test.ts +211 -0
  429. package/frontend-vue/tests/overview-gate.test.ts +147 -0
  430. package/frontend-vue/tests/overview-setup.test.ts +78 -0
  431. package/frontend-vue/tests/paths.test.ts +23 -0
  432. package/frontend-vue/tests/period.test.ts +59 -10
  433. package/frontend-vue/tests/problems.test.ts +153 -0
  434. package/frontend-vue/tests/recipients.test.ts +47 -0
  435. package/frontend-vue/tests/rendering-title.test.ts +24 -0
  436. package/frontend-vue/tests/replay-toast.test.ts +67 -0
  437. package/frontend-vue/tests/retention-input.test.ts +130 -0
  438. package/frontend-vue/tests/send-actions.test.ts +128 -0
  439. package/frontend-vue/tests/send-modals.test.ts +416 -0
  440. package/frontend-vue/tests/send-snippet.test.ts +51 -0
  441. package/frontend-vue/tests/send-stats.test.ts +12 -0
  442. package/frontend-vue/tests/send-values.test.ts +119 -0
  443. package/frontend-vue/tests/shortcuts.test.ts +76 -0
  444. package/frontend-vue/tests/slug.test.ts +88 -0
  445. package/frontend-vue/tests/suppression.test.ts +159 -0
  446. package/frontend-vue/tests/system-variables.test.ts +133 -0
  447. package/frontend-vue/tests/template-attention.test.ts +123 -0
  448. package/frontend-vue/tests/template-drawer.test.ts +450 -0
  449. package/frontend-vue/tests/template-editor.test.ts +218 -9
  450. package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
  451. package/frontend-vue/tests/template-gallery.test.ts +371 -0
  452. package/frontend-vue/tests/template-overlays.test.ts +125 -0
  453. package/frontend-vue/tests/timeline.test.ts +175 -20
  454. package/frontend-vue/tests/tokens.test.ts +79 -1
  455. package/frontend-vue/tests/variable-path.test.ts +23 -0
  456. package/frontend-vue/tests/variable-types.test.ts +20 -0
  457. package/frontend-vue/tests/variables.test.ts +90 -2
  458. package/frontend-vue/tests/version-history.test.ts +41 -0
  459. package/frontend-vue/tests/webhook-status.test.ts +108 -0
  460. package/frontend-vue/vitest.config.ts +4 -1
  461. package/package.json +14 -12
  462. package/src/automation/email-event-trigger.ts +3 -3
  463. package/src/automation/send-template-action.ts +7 -5
  464. package/src/config.ts +3 -1
  465. package/src/constants.ts +18 -2
  466. package/src/crons/attention.ts +21 -0
  467. package/src/crons/index.ts +2 -0
  468. package/src/crons/jobs.ts +35 -0
  469. package/src/crons/retention.ts +7 -22
  470. package/src/data/displays.ts +18 -0
  471. package/src/data/index.ts +1 -0
  472. package/src/data/locale-fallback.ts +26 -0
  473. package/src/data/read-only-routes.ts +14 -0
  474. package/src/data/sends-table.ts +204 -19
  475. package/src/data/suppressions-table.ts +127 -0
  476. package/src/data/templates-table.ts +295 -63
  477. package/src/data-types/categories-type.ts +4 -5
  478. package/src/data-types/category-type.ts +1 -1
  479. package/src/db/models/index.ts +3 -0
  480. package/src/db/models/sends.model.ts +102 -1
  481. package/src/db/models/settings.model.ts +2 -12
  482. package/src/db/models/suppressions.model.ts +53 -0
  483. package/src/db/models/template-versions.model.ts +23 -0
  484. package/src/db/models/templates.model.ts +17 -0
  485. package/src/db/models/webhook-status.model.ts +25 -0
  486. package/src/db/tables/index.ts +3 -0
  487. package/src/db/tables/send-events.table.ts +6 -2
  488. package/src/db/tables/sends.table.ts +37 -2
  489. package/src/db/tables/suppressions.table.ts +46 -0
  490. package/src/db/tables/template-versions.table.ts +44 -0
  491. package/src/db/tables/templates.table.ts +67 -5
  492. package/src/db/tables/webhook-status.table.ts +44 -0
  493. package/src/engine/changes.ts +198 -0
  494. package/src/engine/conditions.ts +1 -7
  495. package/src/engine/coverage.ts +7 -2
  496. package/src/engine/fallback-html.ts +80 -42
  497. package/src/engine/index.ts +3 -0
  498. package/src/engine/plain-text.ts +86 -0
  499. package/src/engine/resolve.ts +49 -15
  500. package/src/engine/system-variables.ts +124 -0
  501. package/src/engine/tokens.ts +13 -1
  502. package/src/hooks/tenant-export.ts +21 -2
  503. package/src/implementations/dms-mailing/index.ts +1 -1
  504. package/src/index.ts +29 -13
  505. package/src/pages/editor.ts +13 -2
  506. package/src/pages/index.ts +6 -3
  507. package/src/pages/module.ts +31 -2
  508. package/src/pages/overview.ts +187 -128
  509. package/src/pages/sends.ts +271 -112
  510. package/src/pages/settings/form.ts +144 -54
  511. package/src/pages/settings/page.ts +9 -17
  512. package/src/pages/suppressions.ts +157 -0
  513. package/src/pages/templates.ts +140 -43
  514. package/src/permissions.ts +6 -4
  515. package/src/quick-actions.ts +12 -5
  516. package/src/routes/events.ts +110 -33
  517. package/src/routes/index.ts +2 -0
  518. package/src/routes/metrics.ts +348 -98
  519. package/src/routes/period.ts +23 -6
  520. package/src/routes/sends.ts +413 -41
  521. package/src/routes/settings.ts +291 -26
  522. package/src/routes/suppressions.ts +87 -0
  523. package/src/routes/template-versions.ts +116 -0
  524. package/src/routes/templates.ts +334 -48
  525. package/src/services/attention.ts +283 -30
  526. package/src/services/catalog.ts +28 -0
  527. package/src/services/categories.ts +28 -27
  528. package/src/services/checked-send.ts +123 -0
  529. package/src/services/events.ts +174 -20
  530. package/src/services/metrics.ts +137 -28
  531. package/src/services/notify.ts +126 -44
  532. package/src/services/provider-banner.ts +165 -0
  533. package/src/services/render.ts +1 -0
  534. package/src/services/send-stat-items.ts +160 -0
  535. package/src/services/send-stats.ts +122 -0
  536. package/src/services/send.ts +193 -31
  537. package/src/services/settings.ts +52 -8
  538. package/src/services/starters.ts +383 -0
  539. package/src/services/suppressions.ts +116 -0
  540. package/src/services/template-attention.ts +82 -0
  541. package/src/services/template-stats.ts +97 -0
  542. package/src/services/templates.ts +400 -37
  543. package/src/services/webhook-status.ts +202 -0
  544. package/src/types/blocks.ts +6 -0
  545. package/src/types/index.ts +2 -0
  546. package/src/types/sends.ts +64 -2
  547. package/src/types/suppressions.ts +27 -0
  548. package/src/types/webhook.ts +29 -0
  549. package/src/validation/settings.schema.ts +173 -17
  550. package/src/validation/templates.schema.ts +53 -1
  551. package/dist/antelope-modules.d.ts +0 -32
  552. package/dist/antelope-modules.js +0 -36
  553. package/dist/antelope-modules.js.map +0 -1
  554. package/dist/test/antelope.test.d.ts +0 -2
  555. package/dist/test/antelope.test.js +0 -180
  556. package/dist/test/antelope.test.js.map +0 -1
  557. package/dist/test/helpers/http.d.ts +0 -4
  558. package/dist/test/helpers/http.js +0 -28
  559. package/dist/test/helpers/http.js.map +0 -1
  560. package/dist/test/helpers/owner.d.ts +0 -5
  561. package/dist/test/helpers/owner.js +0 -30
  562. package/dist/test/helpers/owner.js.map +0 -1
  563. package/dist/test/integration/events.test.d.ts +0 -1
  564. package/dist/test/integration/events.test.js +0 -143
  565. package/dist/test/integration/events.test.js.map +0 -1
  566. package/dist/test/integration/retention.test.d.ts +0 -1
  567. package/dist/test/integration/retention.test.js +0 -291
  568. package/dist/test/integration/retention.test.js.map +0 -1
  569. package/dist/test/integration/sends.test.d.ts +0 -1
  570. package/dist/test/integration/sends.test.js +0 -210
  571. package/dist/test/integration/sends.test.js.map +0 -1
  572. package/dist/test/integration/settings.test.d.ts +0 -1
  573. package/dist/test/integration/settings.test.js +0 -137
  574. package/dist/test/integration/settings.test.js.map +0 -1
  575. package/dist/test/integration/setup.test.d.ts +0 -1
  576. package/dist/test/integration/setup.test.js +0 -32
  577. package/dist/test/integration/setup.test.js.map +0 -1
  578. package/dist/test/integration/templates.test.d.ts +0 -1
  579. package/dist/test/integration/templates.test.js +0 -233
  580. package/dist/test/integration/templates.test.js.map +0 -1
  581. package/dist/test/integration/tenant-export.test.d.ts +0 -1
  582. package/dist/test/integration/tenant-export.test.js +0 -85
  583. package/dist/test/integration/tenant-export.test.js.map +0 -1
  584. package/dist/test/unit/attention.test.d.ts +0 -1
  585. package/dist/test/unit/attention.test.js +0 -109
  586. package/dist/test/unit/attention.test.js.map +0 -1
  587. package/dist/test/unit/automation.test.d.ts +0 -1
  588. package/dist/test/unit/automation.test.js +0 -104
  589. package/dist/test/unit/automation.test.js.map +0 -1
  590. package/dist/test/unit/categories.test.d.ts +0 -1
  591. package/dist/test/unit/categories.test.js +0 -38
  592. package/dist/test/unit/categories.test.js.map +0 -1
  593. package/dist/test/unit/engine/conditions.test.d.ts +0 -1
  594. package/dist/test/unit/engine/conditions.test.js +0 -23
  595. package/dist/test/unit/engine/conditions.test.js.map +0 -1
  596. package/dist/test/unit/engine/coverage.test.d.ts +0 -1
  597. package/dist/test/unit/engine/coverage.test.js +0 -106
  598. package/dist/test/unit/engine/coverage.test.js.map +0 -1
  599. package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
  600. package/dist/test/unit/engine/fallback-html.test.js +0 -78
  601. package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
  602. package/dist/test/unit/engine/paths.test.d.ts +0 -1
  603. package/dist/test/unit/engine/paths.test.js +0 -33
  604. package/dist/test/unit/engine/paths.test.js.map +0 -1
  605. package/dist/test/unit/engine/resolve.test.d.ts +0 -1
  606. package/dist/test/unit/engine/resolve.test.js +0 -157
  607. package/dist/test/unit/engine/resolve.test.js.map +0 -1
  608. package/dist/test/unit/engine/tokens.test.d.ts +0 -1
  609. package/dist/test/unit/engine/tokens.test.js +0 -26
  610. package/dist/test/unit/engine/tokens.test.js.map +0 -1
  611. package/dist/test/unit/events.test.d.ts +0 -1
  612. package/dist/test/unit/events.test.js +0 -13
  613. package/dist/test/unit/events.test.js.map +0 -1
  614. package/dist/test/unit/metrics.test.d.ts +0 -1
  615. package/dist/test/unit/metrics.test.js +0 -108
  616. package/dist/test/unit/metrics.test.js.map +0 -1
  617. package/dist/test/unit/notify.test.d.ts +0 -1
  618. package/dist/test/unit/notify.test.js +0 -49
  619. package/dist/test/unit/notify.test.js.map +0 -1
  620. package/dist/test/unit/period.test.d.ts +0 -1
  621. package/dist/test/unit/period.test.js +0 -61
  622. package/dist/test/unit/period.test.js.map +0 -1
  623. package/dist/test/unit/permissions.test.d.ts +0 -1
  624. package/dist/test/unit/permissions.test.js +0 -27
  625. package/dist/test/unit/permissions.test.js.map +0 -1
  626. package/dist/test/unit/provider.test.d.ts +0 -1
  627. package/dist/test/unit/provider.test.js +0 -47
  628. package/dist/test/unit/provider.test.js.map +0 -1
  629. package/dist/test/unit/realtime.test.d.ts +0 -1
  630. package/dist/test/unit/realtime.test.js +0 -13
  631. package/dist/test/unit/realtime.test.js.map +0 -1
  632. package/dist/test/unit/render.test.d.ts +0 -1
  633. package/dist/test/unit/render.test.js +0 -23
  634. package/dist/test/unit/render.test.js.map +0 -1
  635. package/dist/test/unit/retention.test.d.ts +0 -1
  636. package/dist/test/unit/retention.test.js +0 -66
  637. package/dist/test/unit/retention.test.js.map +0 -1
  638. package/dist/test/unit/send-batch.test.d.ts +0 -1
  639. package/dist/test/unit/send-batch.test.js +0 -46
  640. package/dist/test/unit/send-batch.test.js.map +0 -1
  641. package/dist/test/unit/send.test.d.ts +0 -1
  642. package/dist/test/unit/send.test.js +0 -120
  643. package/dist/test/unit/send.test.js.map +0 -1
  644. package/dist/test/unit/smoke.test.d.ts +0 -1
  645. package/dist/test/unit/smoke.test.js +0 -10
  646. package/dist/test/unit/smoke.test.js.map +0 -1
  647. package/dist/test/unit/status.test.d.ts +0 -1
  648. package/dist/test/unit/status.test.js +0 -16
  649. package/dist/test/unit/status.test.js.map +0 -1
  650. package/dist/test/unit/templates.test.d.ts +0 -1
  651. package/dist/test/unit/templates.test.js +0 -48
  652. package/dist/test/unit/templates.test.js.map +0 -1
  653. package/dist/test/unit/tenant-export.test.d.ts +0 -1
  654. package/dist/test/unit/tenant-export.test.js +0 -59
  655. package/dist/test/unit/tenant-export.test.js.map +0 -1
  656. package/dist/test/unit/webhook-secret.test.d.ts +0 -1
  657. package/dist/test/unit/webhook-secret.test.js +0 -43
  658. package/dist/test/unit/webhook-secret.test.js.map +0 -1
  659. package/frontend-vue/app/components/AttentionCard.vue +0 -78
  660. package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
  661. package/frontend-vue/app/components/ProviderChip.vue +0 -39
  662. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
  663. package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
  664. package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
  665. package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
  666. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
  667. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
  668. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
  669. package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
  670. package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
  671. package/frontend-vue/tests/smoke.test.ts +0 -8
  672. package/src/antelope-modules.ts +0 -48
  673. package/src/test/antelope.test.ts +0 -190
  674. package/src/test/helpers/http.ts +0 -23
  675. package/src/test/helpers/owner.ts +0 -37
  676. package/src/test/integration/events.test.ts +0 -159
  677. package/src/test/integration/retention.test.ts +0 -363
  678. package/src/test/integration/sends.test.ts +0 -233
  679. package/src/test/integration/settings.test.ts +0 -128
  680. package/src/test/integration/setup.test.ts +0 -35
  681. package/src/test/integration/templates.test.ts +0 -272
  682. package/src/test/integration/tenant-export.test.ts +0 -125
  683. package/src/test/unit/attention.test.ts +0 -129
  684. package/src/test/unit/automation.test.ts +0 -130
  685. package/src/test/unit/categories.test.ts +0 -51
  686. package/src/test/unit/engine/conditions.test.ts +0 -35
  687. package/src/test/unit/engine/coverage.test.ts +0 -118
  688. package/src/test/unit/engine/fallback-html.test.ts +0 -86
  689. package/src/test/unit/engine/paths.test.ts +0 -39
  690. package/src/test/unit/engine/resolve.test.ts +0 -176
  691. package/src/test/unit/engine/tokens.test.ts +0 -26
  692. package/src/test/unit/events.test.ts +0 -19
  693. package/src/test/unit/metrics.test.ts +0 -170
  694. package/src/test/unit/notify.test.ts +0 -71
  695. package/src/test/unit/period.test.ts +0 -91
  696. package/src/test/unit/permissions.test.ts +0 -30
  697. package/src/test/unit/provider.test.ts +0 -57
  698. package/src/test/unit/realtime.test.ts +0 -11
  699. package/src/test/unit/render.test.ts +0 -23
  700. package/src/test/unit/retention.test.ts +0 -91
  701. package/src/test/unit/send-batch.test.ts +0 -72
  702. package/src/test/unit/send.test.ts +0 -161
  703. package/src/test/unit/smoke.test.ts +0 -8
  704. package/src/test/unit/status.test.ts +0 -14
  705. package/src/test/unit/templates.test.ts +0 -52
  706. package/src/test/unit/tenant-export.test.ts +0 -88
  707. package/src/test/unit/webhook-secret.test.ts +0 -51
@@ -1,72 +1,173 @@
1
1
  <script setup lang="ts">
2
+ import { SENDS_PATH } from '../utils/pages'
3
+ import { useElementSize } from '@vueuse/core'
4
+ import {
5
+ useMailingApi,
6
+ type TemplateDetails,
7
+ } from '../composables/useMailingApi'
8
+ import { useTemplateDrawer } from '../composables/useTemplateDrawer'
9
+ import { apiErrorMessage } from '../utils/api-error'
10
+ import { formatCardDate } from '../utils/gallery'
2
11
  import { localeCoverage } from '../utils/locales'
12
+ import { sendSnippet } from '../utils/send-snippet'
13
+ import { variableTypeLabelKey } from '../utils/variable-types'
14
+ import { validateName } from '../utils/slug'
15
+ import {
16
+ validTransitions,
17
+ type LifecycleAction,
18
+ } from '../utils/template-attention'
3
19
  import { editorRoute } from '../utils/editor-route'
4
- import { mergeVariables } from '../utils/variables'
20
+ import {
21
+ isDeclarationChange,
22
+ mergeVariables,
23
+ publishedContract,
24
+ workingContract,
25
+ } from '../utils/variables'
26
+ import { systemVariables } from '../utils/system-variables'
5
27
  import type {
28
+ PreviewResponse,
6
29
  TemplateContentResponse,
30
+ TemplatePerformance,
7
31
  TemplateRow,
32
+ TemplateStatus,
8
33
  VariableDefinition,
9
34
  VariableType,
10
35
  } from '../types/mailing'
11
- import { VARIABLE_TYPES } from '../types/mailing'
36
+
37
+ /** What the DMS hands a row drawer to step through the rows shown. */
38
+ interface DrawerNavigation {
39
+ hasPrev: boolean
40
+ hasNext: boolean
41
+ prev: () => void
42
+ next: () => void
43
+ }
12
44
 
13
45
  interface TemplateDrawerProps {
14
- templateId?: string
15
46
  rowData?: TemplateRow
47
+ templateId?: string
16
48
  onSuccessCallback?: () => void
49
+ navigation?: DrawerNavigation
50
+ /** Called after a change made in place (our own opener refreshes its list). */
51
+ onChanged?: () => void
17
52
  }
18
53
 
19
- type StatusAction = 'publish' | 'unpublish' | 'archive'
54
+ type DrawerTab = 'preview' | 'variables' | 'performance' | 'history'
20
55
 
21
- const STATUS_DONE_KEYS: Record<StatusAction, string> = {
22
- publish: 'published',
23
- unpublish: 'unpublished',
24
- archive: 'archived',
56
+ type ConfirmTone = 'primary' | 'warning'
57
+
58
+ interface LifecycleSpec {
59
+ icon: string
60
+ run: (id: string) => Promise<unknown>
61
+ /** Absent: the move is harmless and runs at once. */
62
+ confirmColor?: ConfirmTone
25
63
  }
26
64
 
65
+ /** The send log reads these: `?template=` narrows it, `?view=` opens a tab. */
66
+ const SENDS_TEMPLATE_KEY = 'template'
67
+ const SENDS_VIEW_KEY = 'view'
68
+ const PROBLEMS_VIEW = 'problems'
27
69
  const PREVIEW_WIDTH = 600
28
- const PREVIEW_SCALE = 0.53
29
- const MISSING_LOCALE_MARK = ' ·'
70
+ const PREVIEW_FALLBACK_SCALE = 0.72
71
+ const OPEN_RATE_DIGITS = 1
72
+ const PERCENT = 100
73
+ const DEFAULT_VARIABLE_TYPE: VariableType = 'string'
74
+
75
+ const STATUS_TONES: Record<TemplateStatus, string> = {
76
+ live: 'success',
77
+ draft: 'neutral',
78
+ archived: 'warning',
79
+ }
80
+ const DRAFT_CHANGES_TONE = 'info'
30
81
 
31
82
  const props = defineProps<TemplateDrawerProps>()
32
- const emit = defineEmits<{ success: [] }>()
83
+ defineEmits<{ success: [changed?: boolean] }>()
33
84
 
34
85
  const api = useMailingApi()
35
86
  const toast = useToast()
36
- const { t } = useI18n()
87
+ const { confirm } = useConfirm()
88
+ const { t, locale: uiLocale } = useI18n()
89
+ const { processApiMessage } = useTranslation()
37
90
  const { uniqueLocales } = useUniqueLocales()
38
- const { open: openTestSendModal } = useTestSendModal()
39
- const { open: openRealSendModal } = useRealSendModal()
91
+ const overlays = useTemplateDrawer()
92
+
93
+ const LIFECYCLE: Record<LifecycleAction, LifecycleSpec> = {
94
+ publish: {
95
+ icon: 'i-ph-rocket-launch',
96
+ run: api.publish,
97
+ confirmColor: 'primary',
98
+ },
99
+ unpublish: {
100
+ icon: 'i-ph-arrow-u-up-left',
101
+ run: api.unpublish,
102
+ confirmColor: 'warning',
103
+ },
104
+ archive: { icon: 'i-ph-archive', run: api.archive, confirmColor: 'warning' },
105
+ restore: { icon: 'i-ph-arrow-counter-clockwise', run: api.restore },
106
+ }
40
107
 
41
108
  const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
42
-
43
109
  const detail = ref<TemplateContentResponse | null>(null)
44
- const loading = ref(false)
110
+ const performance = ref<TemplatePerformance | null>(null)
111
+ const loading = ref(true)
112
+ const tab = ref<DrawerTab>('preview')
45
113
  const locale = ref('')
46
- const isDuplicateOpen = ref(false)
47
- const duplicate = reactive({ slug: '', name: '' })
48
- const newVariable = reactive<VariableDefinition>({
49
- path: '',
50
- type: 'string',
51
- required: false,
52
- })
114
+ const subject = ref('')
115
+ const isRenaming = ref(false)
116
+ const draftName = ref('')
117
+ const previewBox = ref<HTMLElement | null>(null)
118
+ const { width: previewBoxWidth } = useElementSize(previewBox)
119
+
120
+ /** The preview fills the drawer's width, whatever it is. */
121
+ const previewScale = computed(() =>
122
+ previewBoxWidth.value
123
+ ? Math.min(1, previewBoxWidth.value / PREVIEW_WIDTH)
124
+ : PREVIEW_FALLBACK_SCALE,
125
+ )
126
+
127
+ async function load(): Promise<void> {
128
+ if (!templateId.value) return
129
+ loading.value = true
130
+ try {
131
+ const [content, stats] = await Promise.all([
132
+ api.templateContent(templateId.value),
133
+ api.performance(templateId.value).catch(() => null),
134
+ ])
135
+ detail.value = content
136
+ performance.value = stats
137
+ locale.value ||= content.fallbackLocale
138
+ } catch (error) {
139
+ toast.add({
140
+ color: 'error',
141
+ title: processApiMessage(apiErrorMessage(error)),
142
+ })
143
+ } finally {
144
+ loading.value = false
145
+ }
146
+ }
147
+
148
+ onMounted(load)
53
149
 
54
150
  const template = computed(() => detail.value?.template ?? props.rowData ?? null)
151
+ const isLive = computed(() => template.value?.status === 'live')
152
+ const previewVersion = computed(() => (isLive.value ? 'published' : 'draft'))
153
+ const shownContent = computed(() =>
154
+ isLive.value
155
+ ? (detail.value?.publishedContent ?? detail.value?.content)
156
+ : detail.value?.content,
157
+ )
55
158
 
56
159
  const coverage = computed(() =>
57
- detail.value
58
- ? localeCoverage(
59
- Object.keys(detail.value.content.locales),
60
- uniqueLocales.value.map((entry) => entry.code),
61
- )
62
- : [],
160
+ localeCoverage(
161
+ Object.keys(shownContent.value?.locales ?? {}),
162
+ uniqueLocales.value.map((entry) => entry.code),
163
+ ),
63
164
  )
64
165
 
65
166
  const localeItems = computed(() =>
66
167
  coverage.value.map((entry) => ({
67
- label:
68
- entry.code.toUpperCase() + (entry.present ? '' : MISSING_LOCALE_MARK),
168
+ label: entry.code.toUpperCase(),
69
169
  value: entry.code,
170
+ icon: entry.present ? 'i-ph-check-circle' : 'i-ph-warning-circle',
70
171
  })),
71
172
  )
72
173
 
@@ -74,321 +175,731 @@ const isLocaleMissing = computed(() =>
74
175
  coverage.value.some((entry) => entry.code === locale.value && !entry.present),
75
176
  )
76
177
 
178
+ /**
179
+ * The contract the tab shows: the published one on a live template (what
180
+ * callers send against), the draft's otherwise.
181
+ */
182
+ const contract = computed(() => {
183
+ if (!detail.value) return { variables: [], detected: [] }
184
+ return isLive.value
185
+ ? publishedContract(detail.value)
186
+ : workingContract(detail.value)
187
+ })
188
+
77
189
  const variables = computed(() =>
78
- mergeVariables(
79
- detail.value?.detectedVariables ?? [],
80
- detail.value?.variables ?? [],
81
- ),
190
+ mergeVariables(contract.value.detected, contract.value.variables),
191
+ )
192
+
193
+ /** A live template whose draft changes the declarations, not shown yet. */
194
+ const hasDraftDeclarations = computed(
195
+ () =>
196
+ isLive.value && Boolean(detail.value?.changes.some(isDeclarationChange)),
197
+ )
198
+
199
+ /** The footer links' URLs the contract asks of every send. */
200
+ const systemVars = computed(() =>
201
+ shownContent.value
202
+ ? systemVariables(shownContent.value, template.value?.category)
203
+ : [],
204
+ )
205
+
206
+ const tabs = computed(() => [
207
+ { label: t('dms_mailing.templates.drawer.tab_preview'), value: 'preview' },
208
+ {
209
+ label: t('dms_mailing.templates.drawer.variables'),
210
+ value: 'variables',
211
+ badge: {
212
+ label: String(variables.value.length + systemVars.value.length),
213
+ color: 'neutral',
214
+ variant: 'soft',
215
+ },
216
+ },
217
+ {
218
+ label: t('dms_mailing.templates.drawer.tab_performance'),
219
+ value: 'performance',
220
+ },
221
+ { label: t('dms_mailing.history.tab'), value: 'history' },
222
+ ])
223
+
224
+ const hasDraftChanges = computed(
225
+ () => isLive.value && Boolean(template.value?.isDraftPending),
226
+ )
227
+
228
+ const statusLabel = computed(() =>
229
+ hasDraftChanges.value
230
+ ? t('dms_mailing.templates.card.draft_changes')
231
+ : t(`dms_mailing.templates.status.${template.value?.status ?? 'draft'}`),
82
232
  )
83
233
 
84
- // The manual form is for typing a detected path, so it offers the undeclared
85
- // ones first rather than an empty box.
86
- const undeclaredPaths = computed(() =>
87
- variables.value
88
- .filter((entry) => entry.usage === 'detected')
89
- .map((entry) => entry.path),
234
+ const statusTone = computed(() =>
235
+ hasDraftChanges.value
236
+ ? DRAFT_CHANGES_TONE
237
+ : STATUS_TONES[template.value?.status ?? 'draft'],
90
238
  )
91
239
 
92
- watch(undeclaredPaths, (paths) => {
93
- if (!newVariable.path) newVariable.path = paths[0] ?? ''
240
+ const editedLine = computed(() => {
241
+ const row = detail.value?.template
242
+ const at = row?.draftUpdatedAt || template.value?.updatedAt
243
+ const by = row?.draftUpdatedBy || template.value?.updatedBy || ''
244
+ return t('dms_mailing.templates.drawer.edited', {
245
+ date: formatCardDate(at, new Date(), uiLocale.value, (date) =>
246
+ t('dms_mailing.common.on_date', { date }),
247
+ ),
248
+ name: by,
249
+ })
94
250
  })
95
251
 
96
- const variableTypeItems = VARIABLE_TYPES.map((type: VariableType) => ({
97
- label: type,
98
- value: type,
99
- }))
252
+ const versionLabel = computed(() =>
253
+ detail.value?.version
254
+ ? `v${detail.value.version}`
255
+ : t('dms_mailing.templates.drawer.never_published'),
256
+ )
100
257
 
101
- async function load(): Promise<void> {
102
- if (!templateId.value) return
103
- loading.value = true
104
- try {
105
- const response = await api.templateContent(templateId.value)
106
- detail.value = response
107
- locale.value = locale.value || response.fallbackLocale
108
- } catch (error) {
109
- toast.add({ color: 'error', title: String(error) })
110
- } finally {
111
- loading.value = false
258
+ const formatNumber = (value: number) =>
259
+ new Intl.NumberFormat(uiLocale.value, {
260
+ maximumFractionDigits: OPEN_RATE_DIGITS,
261
+ }).format(value)
262
+
263
+ /** A rate in percent, written the reader's way ("20.8%", "20,8 %"). */
264
+ const formatPercent = (value: number) =>
265
+ new Intl.NumberFormat(uiLocale.value, {
266
+ style: 'percent',
267
+ maximumFractionDigits: OPEN_RATE_DIGITS,
268
+ }).format(value / PERCENT)
269
+
270
+ /** The send log narrowed to this template, on one of its tabs if given. */
271
+ function sendLogRoute(view?: string) {
272
+ return {
273
+ path: SENDS_PATH,
274
+ query: {
275
+ [SENDS_TEMPLATE_KEY]: template.value?.slug ?? '',
276
+ ...(view ? { [SENDS_VIEW_KEY]: view } : {}),
277
+ },
112
278
  }
113
279
  }
114
280
 
281
+ function sendLogUrl(view?: string): string {
282
+ const { path, query } = sendLogRoute(view)
283
+ return `${path}?${new URLSearchParams(query).toString()}`
284
+ }
285
+
286
+ const hasSends = computed(() => Boolean(performance.value?.sends))
287
+
288
+ const stats = computed(() => [
289
+ {
290
+ id: 'sends',
291
+ eyebrow: t('dms_mailing.templates.drawer.stats_sends'),
292
+ value: formatNumber(performance.value?.sends ?? 0),
293
+ },
294
+ {
295
+ id: 'open',
296
+ eyebrow: t('dms_mailing.templates.drawer.stats_open'),
297
+ value: performance.value?.sends
298
+ ? formatPercent(performance.value.openRate)
299
+ : '—',
300
+ },
301
+ {
302
+ id: 'problems',
303
+ eyebrow: t('dms_mailing.templates.drawer.stats_problems'),
304
+ value: formatNumber(performance.value?.problems ?? 0),
305
+ detail: performance.value?.problems
306
+ ? t('dms_mailing.templates.drawer.problems_detail')
307
+ : undefined,
308
+ detailTone: 'error' as const,
309
+ to: performance.value?.problems ? sendLogUrl(PROBLEMS_VIEW) : undefined,
310
+ },
311
+ ])
312
+
313
+ const snippet = computed(() =>
314
+ sendSnippet({
315
+ slug: template.value?.slug ?? '',
316
+ locale: locale.value || detail.value?.fallbackLocale || '',
317
+ variables: [
318
+ ...variables.value.filter((variable) => variable.usage !== 'unused'),
319
+ ...systemVars.value,
320
+ ],
321
+ testData: detail.value?.testData ?? {},
322
+ }),
323
+ )
324
+
325
+ function reloadInPlace(): void {
326
+ void load()
327
+ props.onChanged?.()
328
+ }
329
+
115
330
  function settle(): void {
116
331
  if (props.onSuccessCallback) {
117
332
  props.onSuccessCallback()
118
333
  return
119
334
  }
120
- void load()
335
+ reloadInPlace()
121
336
  }
122
337
 
123
- function closeContainer(): void {
124
- if (props.onSuccessCallback) {
125
- props.onSuccessCallback()
126
- return
338
+ function openSendLog(): void {
339
+ void navigateDms(sendLogRoute())
340
+ }
341
+
342
+ const nameError = computed(() => {
343
+ const issue = isRenaming.value ? validateName(draftName.value) : null
344
+ return issue ? t(`dms_mailing.templates.name_errors.${issue}`) : null
345
+ })
346
+
347
+ /** Writes the name or the category, the other details kept as they are. */
348
+ async function saveDetails(changes: Partial<TemplateDetails>): Promise<void> {
349
+ const row = template.value
350
+ if (!row) return
351
+ try {
352
+ await api.saveTemplateDetails(templateId.value, {
353
+ name: row.name,
354
+ slug: row.slug,
355
+ category: row.category || null,
356
+ ...changes,
357
+ })
358
+ reloadInPlace()
359
+ } catch (error) {
360
+ toast.add({
361
+ color: 'error',
362
+ title: processApiMessage(apiErrorMessage(error)),
363
+ })
127
364
  }
128
- emit('success')
129
365
  }
130
366
 
131
- async function openEditor(targetLocale?: string): Promise<void> {
132
- closeContainer()
133
- await nextTick()
134
- await navigateDms(editorRoute(templateId.value, targetLocale ?? locale.value))
367
+ function startRename(): void {
368
+ draftName.value = template.value?.name ?? ''
369
+ isRenaming.value = true
135
370
  }
136
371
 
137
- async function runStatusAction(action: StatusAction): Promise<void> {
138
- const actions = {
139
- publish: api.publish,
140
- unpublish: api.unpublish,
141
- archive: api.archive,
372
+ async function submitRename(): Promise<void> {
373
+ if (nameError.value) return
374
+ const name = draftName.value.trim()
375
+ isRenaming.value = false
376
+ if (name !== template.value?.name) await saveDetails({ name })
377
+ }
378
+
379
+ function onCategoryChange(value: string | null | undefined): void {
380
+ const category = value || null
381
+ if (category !== (template.value?.category || null)) {
382
+ void saveDetails({ category })
142
383
  }
384
+ }
385
+
386
+ function openEditor(targetLocale?: string): void {
387
+ void navigateDms(editorRoute(templateId.value, targetLocale))
388
+ }
389
+
390
+ async function confirmed(action: LifecycleAction): Promise<boolean> {
391
+ const spec = LIFECYCLE[action]
392
+ const run = async () => {
393
+ await spec.run(templateId.value)
394
+ }
395
+ if (!spec.confirmColor) {
396
+ await run()
397
+ return true
398
+ }
399
+ return confirm({
400
+ title: t(`dms_mailing.templates.lifecycle.${action}_title`, {
401
+ name: template.value?.name ?? '',
402
+ }),
403
+ description: t(`dms_mailing.templates.lifecycle.${action}_description`),
404
+ color: spec.confirmColor,
405
+ icon: spec.icon,
406
+ confirmLabel: t(`dms_mailing.templates.lifecycle.${action}_confirm`),
407
+ cancelLabel: t('dms_mailing.common.cancel'),
408
+ onConfirm: run,
409
+ })
410
+ }
411
+
412
+ async function runLifecycle(action: LifecycleAction): Promise<void> {
143
413
  try {
144
- await actions[action](templateId.value)
414
+ if (!(await confirmed(action))) return
145
415
  toast.add({
146
416
  color: 'success',
147
- title: t(`dms_mailing.templates.actions.${STATUS_DONE_KEYS[action]}`),
417
+ title: t(`dms_mailing.templates.lifecycle.${action}_done`),
148
418
  })
149
419
  settle()
150
420
  } catch (error) {
151
- toast.add({ color: 'error', title: String(error) })
421
+ toast.add({
422
+ color: 'error',
423
+ title: processApiMessage(apiErrorMessage(error)),
424
+ })
152
425
  }
153
426
  }
154
427
 
155
- const statusItems = computed(() => [
156
- (['publish', 'unpublish', 'archive'] as StatusAction[]).map((action) => ({
157
- label: t(`dms_mailing.templates.actions.${action}`),
158
- onSelect: () => runStatusAction(action),
159
- })),
160
- ])
428
+ const menuItems = computed(() => {
429
+ const row = template.value
430
+ if (!row) return []
431
+ const moves = validTransitions(row).map((action) => ({
432
+ label: t(`dms_mailing.templates.lifecycle.${action}`),
433
+ icon: LIFECYCLE[action].icon,
434
+ onSelect: () => void runLifecycle(action),
435
+ }))
436
+ const send = isLive.value
437
+ ? [
438
+ {
439
+ label: t('dms_mailing.templates.actions.real_send'),
440
+ icon: 'i-ph-paper-plane-right',
441
+ color: 'error' as const,
442
+ onSelect: () =>
443
+ overlays.openRealSend(templateId.value, {
444
+ rowData: row,
445
+ onDone: reloadInPlace,
446
+ }),
447
+ },
448
+ ]
449
+ : []
450
+ return [moves, send].filter((group) => group.length)
451
+ })
161
452
 
162
- async function submitDuplicate(): Promise<void> {
163
- if (!duplicate.slug || !duplicate.name) return
453
+ /** Declares a path in the draft, which the tab shows on a template not live. */
454
+ async function declareVariable(path: string): Promise<void> {
455
+ if (!detail.value) return
456
+ const next: VariableDefinition[] = [
457
+ ...detail.value.variables,
458
+ { path, type: DEFAULT_VARIABLE_TYPE, required: false },
459
+ ]
164
460
  try {
165
- await api.duplicate(templateId.value, duplicate.slug, duplicate.name)
166
- isDuplicateOpen.value = false
461
+ const response = await api.saveVariables(
462
+ templateId.value,
463
+ next,
464
+ detail.value.revision,
465
+ )
466
+ detail.value.variables = response.variables
467
+ detail.value.revision = response.revision
468
+ } catch (error) {
167
469
  toast.add({
168
- color: 'success',
169
- title: t('dms_mailing.templates.actions.duplicate'),
470
+ color: 'error',
471
+ title: processApiMessage(apiErrorMessage(error)),
170
472
  })
171
- settle()
172
- } catch (error) {
173
- toast.add({ color: 'error', title: String(error) })
473
+ reloadInPlace()
174
474
  }
175
475
  }
176
476
 
177
- async function addVariable(): Promise<void> {
178
- if (!newVariable.path || !detail.value) return
179
- const variables = [...detail.value.variables, { ...newVariable }]
477
+ /**
478
+ * Copies a version into the draft. Customers keep the live version: the
479
+ * editor's review publishes the restored draft.
480
+ */
481
+ async function restoreVersion(version: number): Promise<void> {
180
482
  try {
181
- await api.saveVariables(templateId.value, variables)
182
- detail.value.variables = variables
183
- newVariable.path = ''
184
- newVariable.required = false
483
+ await api.restoreVersion(templateId.value, version, detail.value?.revision)
484
+ toast.add({
485
+ color: 'success',
486
+ title: t('dms_mailing.history.restored', { version }),
487
+ description: t('dms_mailing.history.restored_description'),
488
+ actions: [
489
+ {
490
+ label: t('dms_mailing.templates.actions.open_editor'),
491
+ onClick: () => openEditor(),
492
+ },
493
+ ],
494
+ })
185
495
  } catch (error) {
186
- toast.add({ color: 'error', title: String(error) })
496
+ toast.add({
497
+ color: 'error',
498
+ title: processApiMessage(apiErrorMessage(error)),
499
+ })
187
500
  }
501
+ reloadInPlace()
188
502
  }
189
503
 
190
- onMounted(load)
504
+ function onResolved(preview: PreviewResponse): void {
505
+ subject.value = preview.subject
506
+ }
191
507
  </script>
192
508
 
193
509
  <template>
194
- <!-- The width is pinned because the DMS drawer takes no size: `DrawerOptions`
195
- has none and `UDrawer` sizes itself to its body, so anything that changes the
196
- body's intrinsic width resizes the whole panel — here the missing-locale
197
- notice, which exists for some locales and not others. -->
198
- <div class="flex w-[28rem] max-w-full flex-col gap-5">
199
- <USkeleton v-if="loading && !detail" class="h-64 w-full" />
510
+ <div class="flex w-[30rem] max-w-full flex-col">
511
+ <USkeleton v-if="loading && !template" class="h-64 w-full" />
200
512
 
201
513
  <template v-else-if="template">
202
- <div class="flex flex-col gap-1">
203
- <span class="text-highlighted text-sm font-semibold">
204
- {{ template.name }}
205
- </span>
206
- <span class="text-dimmed font-mono text-[11.5px]">
207
- {{ template.slug }} · {{ template.updatedBy }}
208
- </span>
209
- </div>
210
-
211
- <DmsSegmented
212
- v-if="localeItems.length > 1"
213
- v-model="locale"
214
- :items="localeItems"
215
- size="xs"
216
- :aria-label="t('dms_mailing.sends.cols.locale')"
217
- />
218
-
219
- <div class="border-default relative overflow-hidden rounded-lg border">
220
- <DmsMailingTemplatePreviewFrame
221
- :template-id="templateId"
222
- :locale="locale"
223
- :width="PREVIEW_WIDTH"
224
- :scale="PREVIEW_SCALE"
225
- />
226
-
227
- <!-- Laid over the preview rather than stacked under it: the
228
- notice exists for some locales only, and everything below must
229
- stay where it is when the locale changes. It also sits on the
230
- thing it describes, since the frame is showing the fallback. -->
231
- <div v-if="isLocaleMissing" class="absolute inset-x-0 bottom-0 p-1.5">
232
- <UAlert
233
- color="warning"
234
- variant="subtle"
235
- icon="i-ph-warning"
236
- :title="
237
- t('dms_mailing.templates.drawer.missing_locale', {
238
- locale: locale.toUpperCase(),
239
- fallback: (detail?.fallbackLocale ?? '').toUpperCase(),
240
- })
241
- "
242
- :ui="{
243
- root: 'items-center gap-2 rounded-md p-2 backdrop-blur-sm',
244
- icon: 'size-4 shrink-0',
245
- wrapper: 'min-w-0',
246
- title: 'text-[11.5px] leading-snug font-medium',
247
- }"
248
- >
249
- <template #description>
514
+ <div class="flex flex-col gap-1.5 pb-3">
515
+ <div class="flex items-center gap-1">
516
+ <DmsEyebrow :label="t('dms_mailing.templates.drawer.eyebrow')" />
517
+ <div class="ml-auto flex items-center gap-0.5">
518
+ <template v-if="navigation">
250
519
  <UButton
251
- variant="link"
252
- size="xs"
253
- class="p-0 text-[11.5px]"
254
- :label="
255
- t('dms_mailing.templates.drawer.create_locale', {
256
- locale: locale.toUpperCase(),
257
- })
258
- "
259
- @click="openEditor(locale)"
260
- />
261
- </template>
262
- </UAlert>
263
- </div>
264
- </div>
265
-
266
- <div class="flex items-center gap-2">
267
- <UButton
268
- class="flex-1 justify-center"
269
- icon="i-ph-pencil-simple"
270
- :label="t('dms_mailing.templates.actions.open_editor')"
271
- @click="openEditor()"
272
- />
273
- <UButton
274
- icon="i-ph-paper-plane-tilt"
275
- color="neutral"
276
- variant="ghost"
277
- :title="t('dms_mailing.templates.actions.test_send')"
278
- @click="openTestSendModal(templateId, locale)"
279
- />
280
- <UButton
281
- v-if="template && template.status === 'live'"
282
- icon="i-ph-paper-plane-right"
283
- color="neutral"
284
- variant="ghost"
285
- :title="t('dms_mailing.templates.actions.send')"
286
- @click="template && openRealSendModal(templateId, template, locale)"
287
- />
288
- <UPopover v-model:open="isDuplicateOpen">
289
- <UButton
290
- icon="i-ph-copy"
291
- color="neutral"
292
- variant="ghost"
293
- :title="t('dms_mailing.templates.actions.duplicate')"
294
- />
295
- <template #content>
296
- <div class="flex w-64 flex-col gap-2 p-3">
297
- <UInput
298
- v-model="duplicate.slug"
299
- :placeholder="t('dms_mailing.templates.cols.slug')"
520
+ icon="i-ph-caret-up"
521
+ color="neutral"
522
+ variant="ghost"
300
523
  size="sm"
524
+ square
525
+ :disabled="!navigation.hasPrev"
526
+ :aria-label="t('dms_mailing.templates.drawer.previous')"
527
+ @click="navigation.prev()"
301
528
  />
302
- <UInput
303
- v-model="duplicate.name"
304
- :placeholder="t('dms_mailing.templates.cols.name')"
529
+ <UButton
530
+ icon="i-ph-caret-down"
531
+ color="neutral"
532
+ variant="ghost"
305
533
  size="sm"
534
+ square
535
+ :disabled="!navigation.hasNext"
536
+ :aria-label="t('dms_mailing.templates.drawer.next')"
537
+ @click="navigation.next()"
306
538
  />
539
+ </template>
540
+ <UDropdownMenu
541
+ v-if="menuItems.length"
542
+ :items="menuItems"
543
+ :content="{ align: 'end' }"
544
+ >
307
545
  <UButton
546
+ icon="i-ph-dots-three"
547
+ color="neutral"
548
+ variant="ghost"
308
549
  size="sm"
309
- block
310
- :label="t('dms_mailing.templates.actions.duplicate')"
311
- :disabled="!duplicate.slug || !duplicate.name"
312
- @click="submitDuplicate"
550
+ square
551
+ :aria-label="t('dms_mailing.templates.drawer.more')"
313
552
  />
314
- </div>
315
- </template>
316
- </UPopover>
317
- <UDropdownMenu :items="statusItems" :content="{ align: 'end' }">
318
- <UButton icon="i-ph-dots-three" color="neutral" variant="ghost" />
319
- </UDropdownMenu>
320
- </div>
321
-
322
- <USeparator />
323
-
324
- <div class="flex flex-col gap-2">
325
- <h3
326
- class="text-dimmed font-mono text-[10.5px] uppercase tracking-widest"
327
- >
328
- {{ t('dms_mailing.templates.drawer.variables') }}
329
- </h3>
330
- <div
331
- v-for="variable in variables"
332
- :key="variable.path"
333
- class="flex items-center gap-2"
553
+ </UDropdownMenu>
554
+ </div>
555
+ </div>
556
+ <form
557
+ v-if="isRenaming"
558
+ class="flex items-start gap-1.5"
559
+ @submit.prevent="submitRename"
560
+ @keydown.esc.stop="isRenaming = false"
334
561
  >
335
- <code
336
- class="flex-1 truncate font-mono text-[12px]"
337
- :class="
338
- variable.usage === 'unused'
339
- ? 'text-dimmed line-through'
340
- : 'text-toned'
341
- "
342
- >
343
- {{ variable.path }}
344
- </code>
345
- <UBadge
346
- v-if="variable.usage === 'unused'"
347
- :label="t('dms_mailing.templates.drawer.unused')"
348
- color="warning"
349
- variant="subtle"
562
+ <UFormField class="min-w-0 flex-1" :error="nameError ?? undefined">
563
+ <DmsInputText
564
+ v-model="draftName"
565
+ class="w-full"
566
+ autofocus
567
+ :aria-label="t('dms_mailing.templates.cols.name')"
568
+ />
569
+ </UFormField>
570
+ <UButton
571
+ type="submit"
572
+ icon="i-ph-check"
350
573
  size="sm"
574
+ square
575
+ :aria-label="t('dms_mailing.templates.drawer.rename')"
351
576
  />
352
- <UBadge
353
- v-if="variable.required"
354
- :label="t('dms_mailing.templates.drawer.required')"
577
+ <UButton
578
+ icon="i-ph-x"
355
579
  color="neutral"
356
- variant="subtle"
357
- size="sm"
358
- />
359
- <span class="text-dimmed text-[11px]">{{ variable.type }}</span>
360
- </div>
361
- <p v-if="!variables.length" class="text-muted text-[12.5px]">
362
- {{ t('dms_mailing.templates.drawer.no_variables') }}
363
- </p>
364
- <div class="flex items-center gap-2">
365
- <UInput
366
- v-model="newVariable.path"
367
- :placeholder="t('dms_mailing.templates.drawer.declare_variable')"
580
+ variant="ghost"
368
581
  size="sm"
369
- class="flex-1"
370
- list="detected-variable-paths"
582
+ square
583
+ :aria-label="t('dms_mailing.common.cancel')"
584
+ @click="isRenaming = false"
371
585
  />
372
- <datalist id="detected-variable-paths">
373
- <option v-for="path in undeclaredPaths" :key="path" :value="path" />
374
- </datalist>
375
- <USelect
376
- v-model="newVariable.type"
377
- :items="variableTypeItems"
378
- size="sm"
379
- class="w-28"
586
+ </form>
587
+ <h3
588
+ v-else
589
+ class="text-highlighted flex min-w-0 flex-wrap items-center gap-2 text-[17px] font-[650] tracking-[-0.02em] [overflow-wrap:anywhere]"
590
+ >
591
+ {{ template.name }}
592
+ <DmsStatusPill
593
+ :tone="statusTone"
594
+ :label="statusLabel"
595
+ :dot="isLive ? 'live' : 'static'"
380
596
  />
381
- <USwitch v-model="newVariable.required" size="sm" />
382
597
  <UButton
383
- icon="i-ph-plus"
598
+ icon="i-ph-pencil-simple"
384
599
  color="neutral"
385
600
  variant="ghost"
601
+ size="xs"
602
+ square
603
+ :aria-label="t('dms_mailing.templates.drawer.rename')"
604
+ @click="startRename"
605
+ />
606
+ </h3>
607
+ <p class="text-muted text-[12.5px] [overflow-wrap:anywhere]">
608
+ <span class="font-mono">{{ template.slug }}</span>
609
+ · {{ versionLabel }} · {{ editedLine }}
610
+ </p>
611
+ <div class="flex items-center gap-2 pt-1">
612
+ <span class="text-muted shrink-0 text-[12.5px]">
613
+ {{ t('dms_mailing.templates.drawer.category') }}
614
+ </span>
615
+ <MailingCategoryInput
616
+ class="min-w-0 flex-1"
617
+ :model-value="template.category ?? null"
618
+ @update:model-value="onCategoryChange"
619
+ />
620
+ </div>
621
+ </div>
622
+
623
+ <UTabs
624
+ v-model="tab"
625
+ :items="tabs"
626
+ :content="false"
627
+ variant="link"
628
+ size="sm"
629
+ class="border-default -mx-1 border-b"
630
+ />
631
+
632
+ <div v-show="tab === 'preview'" class="flex flex-col gap-3 py-4">
633
+ <div class="flex items-center justify-between gap-2">
634
+ <DmsSegmented
635
+ v-if="localeItems.length > 1"
636
+ v-model="locale"
637
+ :items="localeItems"
638
+ size="xs"
639
+ :aria-label="t('dms_mailing.test_send.locale')"
640
+ />
641
+ <span
642
+ v-if="subject"
643
+ class="text-muted min-w-0 truncate text-[12.5px]"
644
+ :title="subject"
645
+ >
646
+ {{ t('dms_mailing.templates.drawer.subject', { subject }) }}
647
+ </span>
648
+ </div>
649
+ <div
650
+ class="border-default bg-elevated/40 overflow-hidden rounded-lg border p-3"
651
+ >
652
+ <div ref="previewBox" class="flex justify-center">
653
+ <MailingTemplatePreviewFrame
654
+ :template-id="templateId"
655
+ :locale="locale"
656
+ :version="previewVersion"
657
+ :width="PREVIEW_WIDTH"
658
+ :scale="previewScale"
659
+ :label="template.name"
660
+ @resolved="onResolved"
661
+ />
662
+ </div>
663
+ </div>
664
+ <DmsBanner
665
+ v-if="isLocaleMissing"
666
+ tone="warning"
667
+ size="sm"
668
+ icon="i-ph-translate"
669
+ :title="
670
+ t('dms_mailing.templates.drawer.missing_title', {
671
+ locale: locale.toUpperCase(),
672
+ })
673
+ "
674
+ :description="
675
+ t('dms_mailing.templates.drawer.missing_description', {
676
+ locale: locale.toUpperCase(),
677
+ fallback: (detail?.fallbackLocale ?? '').toUpperCase(),
678
+ })
679
+ "
680
+ >
681
+ <template #actions>
682
+ <UButton
683
+ color="neutral"
684
+ variant="outline"
685
+ size="xs"
686
+ :label="
687
+ t('dms_mailing.templates.drawer.create_short', {
688
+ locale: locale.toUpperCase(),
689
+ })
690
+ "
691
+ @click="openEditor(locale)"
692
+ />
693
+ </template>
694
+ </DmsBanner>
695
+ </div>
696
+
697
+ <div v-show="tab === 'variables'" class="flex flex-col gap-4 py-4">
698
+ <div class="flex flex-col gap-1">
699
+ <DmsEyebrow
700
+ :label="t('dms_mailing.templates.drawer.declared_detected')"
701
+ />
702
+ <p v-if="!variables.length" class="text-muted text-[12.5px]">
703
+ {{ t('dms_mailing.templates.drawer.no_variables') }}
704
+ </p>
705
+ <div
706
+ v-for="variable in variables"
707
+ :key="variable.path"
708
+ class="border-default flex min-h-9 items-center gap-2 border-b last:border-b-0"
709
+ >
710
+ <code
711
+ class="truncate font-mono text-[12px]"
712
+ :class="
713
+ variable.usage === 'unused' ? 'text-dimmed' : 'text-toned'
714
+ "
715
+ >
716
+ {{ variable.path }}
717
+ </code>
718
+ <span
719
+ v-if="variable.required && variable.usage !== 'detected'"
720
+ class="text-error"
721
+ :title="t('dms_mailing.templates.drawer.required')"
722
+ >
723
+ *
724
+ </span>
725
+ <DmsStatusPill
726
+ v-if="variable.usage === 'detected'"
727
+ tone="warning"
728
+ size="sm"
729
+ dot="none"
730
+ :label="t('dms_mailing.templates.drawer.not_declared')"
731
+ />
732
+ <DmsStatusPill
733
+ v-if="variable.usage === 'unused'"
734
+ tone="neutral"
735
+ size="sm"
736
+ dot="none"
737
+ :label="t('dms_mailing.templates.drawer.unused')"
738
+ />
739
+ <UButton
740
+ v-if="variable.usage === 'detected' && !isLive"
741
+ class="ml-auto"
742
+ color="neutral"
743
+ variant="ghost"
744
+ size="xs"
745
+ :label="t('dms_mailing.templates.drawer.declare')"
746
+ @click="declareVariable(variable.path)"
747
+ />
748
+ <span
749
+ v-else-if="variable.usage !== 'detected'"
750
+ class="text-dimmed ml-auto font-mono text-[11px]"
751
+ >
752
+ {{ t(variableTypeLabelKey(variable.type)) }}
753
+ </span>
754
+ </div>
755
+ </div>
756
+ <DmsBanner
757
+ v-if="hasDraftDeclarations"
758
+ tone="info"
759
+ size="sm"
760
+ icon="i-ph-brackets-curly"
761
+ :title="t('dms_mailing.templates.drawer.draft_declarations')"
762
+ :description="
763
+ t('dms_mailing.templates.drawer.draft_declarations_description')
764
+ "
765
+ >
766
+ <template #actions>
767
+ <UButton
768
+ color="neutral"
769
+ variant="outline"
770
+ size="xs"
771
+ :label="t('dms_mailing.templates.actions.open_editor')"
772
+ @click="openEditor()"
773
+ />
774
+ </template>
775
+ </DmsBanner>
776
+ <MailingSystemVariables :variables="systemVars" />
777
+ <div class="flex flex-col gap-1.5">
778
+ <div class="flex items-center justify-between">
779
+ <DmsEyebrow
780
+ :label="t('dms_mailing.templates.drawer.call_from_code')"
781
+ />
782
+ <DmsCopyButton :value="snippet" />
783
+ </div>
784
+ <pre
785
+ class="bg-elevated text-toned overflow-x-auto rounded-md p-3 font-mono text-[11.5px] leading-relaxed"
786
+ >{{ snippet }}</pre>
787
+ </div>
788
+ </div>
789
+
790
+ <div v-show="tab === 'performance'" class="flex flex-col gap-4 py-4">
791
+ <div class="flex flex-col gap-2">
792
+ <div class="flex items-center justify-between">
793
+ <DmsEyebrow
794
+ :label="t('dms_mailing.templates.drawer.last_30_days')"
795
+ />
796
+ <UButton
797
+ variant="link"
798
+ size="xs"
799
+ class="p-0"
800
+ :label="t('dms_mailing.templates.drawer.open_send_log')"
801
+ @click="openSendLog"
802
+ />
803
+ </div>
804
+ <DmsStatGroup
805
+ layout="joined"
806
+ :columns="3"
807
+ :items="stats"
808
+ :loading="loading"
809
+ />
810
+ </div>
811
+ <div class="flex flex-col gap-1">
812
+ <DmsEyebrow :label="t('dms_mailing.templates.drawer.called_from')" />
813
+ <p
814
+ v-if="!hasSends || !performance?.sources.length"
815
+ class="text-muted text-[12.5px]"
816
+ >
817
+ {{ t('dms_mailing.templates.drawer.no_sources') }}
818
+ </p>
819
+ <DmsListRow
820
+ v-for="source in performance?.sources ?? []"
821
+ :key="source.source"
822
+ icon="i-ph-code"
823
+ size="sm"
824
+ mono
825
+ :title="
826
+ source.source || t('dms_mailing.templates.drawer.unknown_source')
827
+ "
828
+ :trailing="
829
+ t(
830
+ 'dms_mailing.templates.drawer.sends_count',
831
+ { count: formatNumber(source.count) },
832
+ source.count,
833
+ )
834
+ "
835
+ />
836
+ </div>
837
+ <div class="flex flex-col gap-1">
838
+ <DmsEyebrow :label="t('dms_mailing.templates.drawer.fallbacks')" />
839
+ <p v-if="!hasSends" class="text-muted text-[12.5px]">
840
+ {{ t('dms_mailing.templates.drawer.no_sources') }}
841
+ </p>
842
+ <p
843
+ v-else-if="!performance?.fallbacks.length"
844
+ class="text-muted text-[12.5px]"
845
+ >
846
+ {{ t('dms_mailing.templates.drawer.no_fallbacks') }}
847
+ </p>
848
+ <DmsListRow
849
+ v-for="fallback in performance?.fallbacks ?? []"
850
+ :key="`${fallback.requested}-${fallback.used}`"
851
+ icon="i-ph-translate"
852
+ tone="warning"
386
853
  size="sm"
387
- :disabled="!newVariable.path"
388
- @click="addVariable"
854
+ :title="`${fallback.requested.toUpperCase()} → ${fallback.used.toUpperCase()}`"
855
+ :trailing="
856
+ t(
857
+ 'dms_mailing.templates.drawer.sends_count',
858
+ { count: formatNumber(fallback.count) },
859
+ fallback.count,
860
+ )
861
+ "
389
862
  />
390
863
  </div>
391
864
  </div>
865
+
866
+ <div v-if="tab === 'history'" class="flex flex-col gap-2 py-4">
867
+ <p class="text-muted text-[12.5px]">
868
+ {{ t('dms_mailing.history.intro') }}
869
+ </p>
870
+ <MailingVersionHistory
871
+ :template-id="templateId"
872
+ :current-version="detail?.version ?? 0"
873
+ :status="template.status"
874
+ :locale="locale"
875
+ :restore="restoreVersion"
876
+ />
877
+ </div>
878
+
879
+ <div
880
+ class="border-default bg-default sticky bottom-0 -mx-1 mt-auto flex items-center gap-2 border-t px-1 py-3"
881
+ >
882
+ <UButton
883
+ icon="i-ph-flask"
884
+ color="neutral"
885
+ variant="ghost"
886
+ :label="t('dms_mailing.templates.card.test')"
887
+ @click="overlays.openTestSend(templateId, { locale })"
888
+ />
889
+ <UButton
890
+ class="ml-auto"
891
+ icon="i-ph-copy"
892
+ color="neutral"
893
+ variant="outline"
894
+ :label="t('dms_mailing.templates.actions.duplicate')"
895
+ @click="overlays.openDuplicate(templateId, { rowData: template })"
896
+ />
897
+ <UButton
898
+ icon="i-ph-pencil-simple"
899
+ :label="t('dms_mailing.templates.actions.open_editor')"
900
+ @click="openEditor(locale)"
901
+ />
902
+ </div>
392
903
  </template>
393
904
  </div>
394
905
  </template>