@antelopejs/dms-mailing 0.2.2

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 (550) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +162 -0
  3. package/dist/antelope-modules.d.ts +32 -0
  4. package/dist/antelope-modules.js +36 -0
  5. package/dist/antelope-modules.js.map +1 -0
  6. package/dist/automation/email-event-trigger.d.ts +16 -0
  7. package/dist/automation/email-event-trigger.js +70 -0
  8. package/dist/automation/email-event-trigger.js.map +1 -0
  9. package/dist/automation/index.d.ts +4 -0
  10. package/dist/automation/index.js +39 -0
  11. package/dist/automation/index.js.map +1 -0
  12. package/dist/automation/send-template-action.d.ts +13 -0
  13. package/dist/automation/send-template-action.js +71 -0
  14. package/dist/automation/send-template-action.js.map +1 -0
  15. package/dist/config.d.ts +5 -0
  16. package/dist/config.js +13 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/constants.d.ts +28 -0
  19. package/dist/constants.js +32 -0
  20. package/dist/constants.js.map +1 -0
  21. package/dist/crons/index.d.ts +1 -0
  22. package/dist/crons/index.js +18 -0
  23. package/dist/crons/index.js.map +1 -0
  24. package/dist/crons/retention.d.ts +18 -0
  25. package/dist/crons/retention.js +107 -0
  26. package/dist/crons/retention.js.map +1 -0
  27. package/dist/data/body.d.ts +1 -0
  28. package/dist/data/body.js +18 -0
  29. package/dist/data/body.js.map +1 -0
  30. package/dist/data/index.d.ts +3 -0
  31. package/dist/data/index.js +20 -0
  32. package/dist/data/index.js.map +1 -0
  33. package/dist/data/sends-table.d.ts +28 -0
  34. package/dist/data/sends-table.js +161 -0
  35. package/dist/data/sends-table.js.map +1 -0
  36. package/dist/data/templates-table.d.ts +31 -0
  37. package/dist/data/templates-table.js +173 -0
  38. package/dist/data/templates-table.js.map +1 -0
  39. package/dist/data-types/categories-type.d.ts +35 -0
  40. package/dist/data-types/categories-type.js +37 -0
  41. package/dist/data-types/categories-type.js.map +1 -0
  42. package/dist/data-types/category-type.d.ts +8 -0
  43. package/dist/data-types/category-type.js +43 -0
  44. package/dist/data-types/category-type.js.map +1 -0
  45. package/dist/data-types/index.d.ts +2 -0
  46. package/dist/data-types/index.js +19 -0
  47. package/dist/data-types/index.js.map +1 -0
  48. package/dist/db/index.d.ts +2 -0
  49. package/dist/db/index.js +19 -0
  50. package/dist/db/index.js.map +1 -0
  51. package/dist/db/models/index.d.ts +4 -0
  52. package/dist/db/models/index.js +21 -0
  53. package/dist/db/models/index.js.map +1 -0
  54. package/dist/db/models/send-events.model.d.ts +27 -0
  55. package/dist/db/models/send-events.model.js +35 -0
  56. package/dist/db/models/send-events.model.js.map +1 -0
  57. package/dist/db/models/sends.model.d.ts +32 -0
  58. package/dist/db/models/sends.model.js +115 -0
  59. package/dist/db/models/sends.model.js.map +1 -0
  60. package/dist/db/models/settings.model.d.ts +23 -0
  61. package/dist/db/models/settings.model.js +13 -0
  62. package/dist/db/models/settings.model.js.map +1 -0
  63. package/dist/db/models/templates.model.d.ts +23 -0
  64. package/dist/db/models/templates.model.js +13 -0
  65. package/dist/db/models/templates.model.js.map +1 -0
  66. package/dist/db/tables/index.d.ts +4 -0
  67. package/dist/db/tables/index.js +21 -0
  68. package/dist/db/tables/index.js.map +1 -0
  69. package/dist/db/tables/send-events.table.d.ts +10 -0
  70. package/dist/db/tables/send-events.table.js +44 -0
  71. package/dist/db/tables/send-events.table.js.map +1 -0
  72. package/dist/db/tables/sends.table.d.ts +26 -0
  73. package/dist/db/tables/sends.table.js +113 -0
  74. package/dist/db/tables/sends.table.js.map +1 -0
  75. package/dist/db/tables/settings.table.d.ts +15 -0
  76. package/dist/db/tables/settings.table.js +68 -0
  77. package/dist/db/tables/settings.table.js.map +1 -0
  78. package/dist/db/tables/templates.table.d.ts +18 -0
  79. package/dist/db/tables/templates.table.js +81 -0
  80. package/dist/db/tables/templates.table.js.map +1 -0
  81. package/dist/engine/conditions.d.ts +2 -0
  82. package/dist/engine/conditions.js +20 -0
  83. package/dist/engine/conditions.js.map +1 -0
  84. package/dist/engine/coverage.d.ts +4 -0
  85. package/dist/engine/coverage.js +45 -0
  86. package/dist/engine/coverage.js.map +1 -0
  87. package/dist/engine/fallback-html.d.ts +4 -0
  88. package/dist/engine/fallback-html.js +84 -0
  89. package/dist/engine/fallback-html.js.map +1 -0
  90. package/dist/engine/index.d.ts +7 -0
  91. package/dist/engine/index.js +24 -0
  92. package/dist/engine/index.js.map +1 -0
  93. package/dist/engine/paths.d.ts +3 -0
  94. package/dist/engine/paths.js +32 -0
  95. package/dist/engine/paths.js.map +1 -0
  96. package/dist/engine/resolve.d.ts +4 -0
  97. package/dist/engine/resolve.js +139 -0
  98. package/dist/engine/resolve.js.map +1 -0
  99. package/dist/engine/tokens.d.ts +7 -0
  100. package/dist/engine/tokens.js +29 -0
  101. package/dist/engine/tokens.js.map +1 -0
  102. package/dist/engine/walk.d.ts +3 -0
  103. package/dist/engine/walk.js +14 -0
  104. package/dist/engine/walk.js.map +1 -0
  105. package/dist/hooks/index.d.ts +1 -0
  106. package/dist/hooks/index.js +18 -0
  107. package/dist/hooks/index.js.map +1 -0
  108. package/dist/hooks/tenant-export.d.ts +14 -0
  109. package/dist/hooks/tenant-export.js +84 -0
  110. package/dist/hooks/tenant-export.js.map +1 -0
  111. package/dist/implementations/dms-mailing/index.d.ts +2 -0
  112. package/dist/implementations/dms-mailing/index.js +8 -0
  113. package/dist/implementations/dms-mailing/index.js.map +1 -0
  114. package/dist/index.d.ts +13 -0
  115. package/dist/index.js +95 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/pages/editor.d.ts +9 -0
  118. package/dist/pages/editor.js +30 -0
  119. package/dist/pages/editor.js.map +1 -0
  120. package/dist/pages/index.d.ts +7 -0
  121. package/dist/pages/index.js +24 -0
  122. package/dist/pages/index.js.map +1 -0
  123. package/dist/pages/module.d.ts +1 -0
  124. package/dist/pages/module.js +12 -0
  125. package/dist/pages/module.js.map +1 -0
  126. package/dist/pages/overview.d.ts +14 -0
  127. package/dist/pages/overview.js +92 -0
  128. package/dist/pages/overview.js.map +1 -0
  129. package/dist/pages/sends.d.ts +13 -0
  130. package/dist/pages/sends.js +125 -0
  131. package/dist/pages/sends.js.map +1 -0
  132. package/dist/pages/settings/form.d.ts +1 -0
  133. package/dist/pages/settings/form.js +68 -0
  134. package/dist/pages/settings/form.js.map +1 -0
  135. package/dist/pages/settings/index.d.ts +2 -0
  136. package/dist/pages/settings/index.js +19 -0
  137. package/dist/pages/settings/index.js.map +1 -0
  138. package/dist/pages/settings/page.d.ts +10 -0
  139. package/dist/pages/settings/page.js +35 -0
  140. package/dist/pages/settings/page.js.map +1 -0
  141. package/dist/pages/templates.d.ts +10 -0
  142. package/dist/pages/templates.js +140 -0
  143. package/dist/pages/templates.js.map +1 -0
  144. package/dist/permissions.d.ts +1 -0
  145. package/dist/permissions.js +37 -0
  146. package/dist/permissions.js.map +1 -0
  147. package/dist/quick-actions.d.ts +2 -0
  148. package/dist/quick-actions.js +18 -0
  149. package/dist/quick-actions.js.map +1 -0
  150. package/dist/realtime.d.ts +4 -0
  151. package/dist/realtime.js +44 -0
  152. package/dist/realtime.js.map +1 -0
  153. package/dist/routes/events.d.ts +9 -0
  154. package/dist/routes/events.js +63 -0
  155. package/dist/routes/events.js.map +1 -0
  156. package/dist/routes/index.d.ts +7 -0
  157. package/dist/routes/index.js +24 -0
  158. package/dist/routes/index.js.map +1 -0
  159. package/dist/routes/metrics.d.ts +54 -0
  160. package/dist/routes/metrics.js +235 -0
  161. package/dist/routes/metrics.js.map +1 -0
  162. package/dist/routes/period.d.ts +5 -0
  163. package/dist/routes/period.js +36 -0
  164. package/dist/routes/period.js.map +1 -0
  165. package/dist/routes/provider.d.ts +13 -0
  166. package/dist/routes/provider.js +41 -0
  167. package/dist/routes/provider.js.map +1 -0
  168. package/dist/routes/sends.d.ts +31 -0
  169. package/dist/routes/sends.js +178 -0
  170. package/dist/routes/sends.js.map +1 -0
  171. package/dist/routes/settings.d.ts +14 -0
  172. package/dist/routes/settings.js +72 -0
  173. package/dist/routes/settings.js.map +1 -0
  174. package/dist/routes/templates.d.ts +61 -0
  175. package/dist/routes/templates.js +230 -0
  176. package/dist/routes/templates.js.map +1 -0
  177. package/dist/services/attention.d.ts +23 -0
  178. package/dist/services/attention.js +104 -0
  179. package/dist/services/attention.js.map +1 -0
  180. package/dist/services/categories.d.ts +34 -0
  181. package/dist/services/categories.js +34 -0
  182. package/dist/services/categories.js.map +1 -0
  183. package/dist/services/events.d.ts +10 -0
  184. package/dist/services/events.js +89 -0
  185. package/dist/services/events.js.map +1 -0
  186. package/dist/services/metrics.d.ts +34 -0
  187. package/dist/services/metrics.js +103 -0
  188. package/dist/services/metrics.js.map +1 -0
  189. package/dist/services/notify.d.ts +20 -0
  190. package/dist/services/notify.js +63 -0
  191. package/dist/services/notify.js.map +1 -0
  192. package/dist/services/permissions.d.ts +2 -0
  193. package/dist/services/permissions.js +15 -0
  194. package/dist/services/permissions.js.map +1 -0
  195. package/dist/services/provider.d.ts +6 -0
  196. package/dist/services/provider.js +25 -0
  197. package/dist/services/provider.js.map +1 -0
  198. package/dist/services/render.d.ts +6 -0
  199. package/dist/services/render.js +21 -0
  200. package/dist/services/render.js.map +1 -0
  201. package/dist/services/send.d.ts +49 -0
  202. package/dist/services/send.js +259 -0
  203. package/dist/services/send.js.map +1 -0
  204. package/dist/services/settings.d.ts +4 -0
  205. package/dist/services/settings.js +61 -0
  206. package/dist/services/settings.js.map +1 -0
  207. package/dist/services/status.d.ts +3 -0
  208. package/dist/services/status.js +16 -0
  209. package/dist/services/status.js.map +1 -0
  210. package/dist/services/templates.d.ts +17 -0
  211. package/dist/services/templates.js +92 -0
  212. package/dist/services/templates.js.map +1 -0
  213. package/dist/test/antelope.test.d.ts +2 -0
  214. package/dist/test/antelope.test.js +179 -0
  215. package/dist/test/antelope.test.js.map +1 -0
  216. package/dist/test/helpers/http.d.ts +4 -0
  217. package/dist/test/helpers/http.js +28 -0
  218. package/dist/test/helpers/http.js.map +1 -0
  219. package/dist/test/helpers/owner.d.ts +5 -0
  220. package/dist/test/helpers/owner.js +30 -0
  221. package/dist/test/helpers/owner.js.map +1 -0
  222. package/dist/test/integration/events.test.d.ts +1 -0
  223. package/dist/test/integration/events.test.js +132 -0
  224. package/dist/test/integration/events.test.js.map +1 -0
  225. package/dist/test/integration/retention.test.d.ts +1 -0
  226. package/dist/test/integration/retention.test.js +291 -0
  227. package/dist/test/integration/retention.test.js.map +1 -0
  228. package/dist/test/integration/sends.test.d.ts +1 -0
  229. package/dist/test/integration/sends.test.js +157 -0
  230. package/dist/test/integration/sends.test.js.map +1 -0
  231. package/dist/test/integration/settings.test.d.ts +1 -0
  232. package/dist/test/integration/settings.test.js +45 -0
  233. package/dist/test/integration/settings.test.js.map +1 -0
  234. package/dist/test/integration/setup.test.d.ts +1 -0
  235. package/dist/test/integration/setup.test.js +32 -0
  236. package/dist/test/integration/setup.test.js.map +1 -0
  237. package/dist/test/integration/templates.test.d.ts +1 -0
  238. package/dist/test/integration/templates.test.js +167 -0
  239. package/dist/test/integration/templates.test.js.map +1 -0
  240. package/dist/test/integration/tenant-export.test.d.ts +1 -0
  241. package/dist/test/integration/tenant-export.test.js +85 -0
  242. package/dist/test/integration/tenant-export.test.js.map +1 -0
  243. package/dist/test/unit/attention.test.d.ts +1 -0
  244. package/dist/test/unit/attention.test.js +109 -0
  245. package/dist/test/unit/attention.test.js.map +1 -0
  246. package/dist/test/unit/automation.test.d.ts +1 -0
  247. package/dist/test/unit/automation.test.js +104 -0
  248. package/dist/test/unit/automation.test.js.map +1 -0
  249. package/dist/test/unit/categories.test.d.ts +1 -0
  250. package/dist/test/unit/categories.test.js +38 -0
  251. package/dist/test/unit/categories.test.js.map +1 -0
  252. package/dist/test/unit/engine/conditions.test.d.ts +1 -0
  253. package/dist/test/unit/engine/conditions.test.js +23 -0
  254. package/dist/test/unit/engine/conditions.test.js.map +1 -0
  255. package/dist/test/unit/engine/coverage.test.d.ts +1 -0
  256. package/dist/test/unit/engine/coverage.test.js +106 -0
  257. package/dist/test/unit/engine/coverage.test.js.map +1 -0
  258. package/dist/test/unit/engine/fallback-html.test.d.ts +1 -0
  259. package/dist/test/unit/engine/fallback-html.test.js +78 -0
  260. package/dist/test/unit/engine/fallback-html.test.js.map +1 -0
  261. package/dist/test/unit/engine/paths.test.d.ts +1 -0
  262. package/dist/test/unit/engine/paths.test.js +33 -0
  263. package/dist/test/unit/engine/paths.test.js.map +1 -0
  264. package/dist/test/unit/engine/resolve.test.d.ts +1 -0
  265. package/dist/test/unit/engine/resolve.test.js +157 -0
  266. package/dist/test/unit/engine/resolve.test.js.map +1 -0
  267. package/dist/test/unit/engine/tokens.test.d.ts +1 -0
  268. package/dist/test/unit/engine/tokens.test.js +26 -0
  269. package/dist/test/unit/engine/tokens.test.js.map +1 -0
  270. package/dist/test/unit/events.test.d.ts +1 -0
  271. package/dist/test/unit/events.test.js +13 -0
  272. package/dist/test/unit/events.test.js.map +1 -0
  273. package/dist/test/unit/metrics.test.d.ts +1 -0
  274. package/dist/test/unit/metrics.test.js +108 -0
  275. package/dist/test/unit/metrics.test.js.map +1 -0
  276. package/dist/test/unit/notify.test.d.ts +1 -0
  277. package/dist/test/unit/notify.test.js +49 -0
  278. package/dist/test/unit/notify.test.js.map +1 -0
  279. package/dist/test/unit/period.test.d.ts +1 -0
  280. package/dist/test/unit/period.test.js +44 -0
  281. package/dist/test/unit/period.test.js.map +1 -0
  282. package/dist/test/unit/provider.test.d.ts +1 -0
  283. package/dist/test/unit/provider.test.js +47 -0
  284. package/dist/test/unit/provider.test.js.map +1 -0
  285. package/dist/test/unit/realtime.test.d.ts +1 -0
  286. package/dist/test/unit/realtime.test.js +13 -0
  287. package/dist/test/unit/realtime.test.js.map +1 -0
  288. package/dist/test/unit/render.test.d.ts +1 -0
  289. package/dist/test/unit/render.test.js +23 -0
  290. package/dist/test/unit/render.test.js.map +1 -0
  291. package/dist/test/unit/retention.test.d.ts +1 -0
  292. package/dist/test/unit/retention.test.js +66 -0
  293. package/dist/test/unit/retention.test.js.map +1 -0
  294. package/dist/test/unit/send-batch.test.d.ts +1 -0
  295. package/dist/test/unit/send-batch.test.js +46 -0
  296. package/dist/test/unit/send-batch.test.js.map +1 -0
  297. package/dist/test/unit/send.test.d.ts +1 -0
  298. package/dist/test/unit/send.test.js +120 -0
  299. package/dist/test/unit/send.test.js.map +1 -0
  300. package/dist/test/unit/smoke.test.d.ts +1 -0
  301. package/dist/test/unit/smoke.test.js +10 -0
  302. package/dist/test/unit/smoke.test.js.map +1 -0
  303. package/dist/test/unit/status.test.d.ts +1 -0
  304. package/dist/test/unit/status.test.js +16 -0
  305. package/dist/test/unit/status.test.js.map +1 -0
  306. package/dist/test/unit/templates.test.d.ts +1 -0
  307. package/dist/test/unit/templates.test.js +48 -0
  308. package/dist/test/unit/templates.test.js.map +1 -0
  309. package/dist/test/unit/tenant-export.test.d.ts +1 -0
  310. package/dist/test/unit/tenant-export.test.js +59 -0
  311. package/dist/test/unit/tenant-export.test.js.map +1 -0
  312. package/dist/test/unit/webhook-secret.test.d.ts +1 -0
  313. package/dist/test/unit/webhook-secret.test.js +42 -0
  314. package/dist/test/unit/webhook-secret.test.js.map +1 -0
  315. package/dist/types/blocks.d.ts +2 -0
  316. package/dist/types/blocks.js +3 -0
  317. package/dist/types/blocks.js.map +1 -0
  318. package/dist/types/index.d.ts +4 -0
  319. package/dist/types/index.js +21 -0
  320. package/dist/types/index.js.map +1 -0
  321. package/dist/types/resolved.d.ts +68 -0
  322. package/dist/types/resolved.js +3 -0
  323. package/dist/types/resolved.js.map +1 -0
  324. package/dist/types/sends.d.ts +14 -0
  325. package/dist/types/sends.js +19 -0
  326. package/dist/types/sends.js.map +1 -0
  327. package/dist/types/settings.d.ts +16 -0
  328. package/dist/types/settings.js +11 -0
  329. package/dist/types/settings.js.map +1 -0
  330. package/dist/validation/settings.schema.d.ts +63 -0
  331. package/dist/validation/settings.schema.js +29 -0
  332. package/dist/validation/settings.schema.js.map +1 -0
  333. package/dist/validation/templates.schema.d.ts +248 -0
  334. package/dist/validation/templates.schema.js +126 -0
  335. package/dist/validation/templates.schema.js.map +1 -0
  336. package/frontend-vue/.editorconfig +12 -0
  337. package/frontend-vue/.prettierrc +11 -0
  338. package/frontend-vue/app/components/AttentionCard.vue +78 -0
  339. package/frontend-vue/app/components/CategoriesInput.vue +126 -0
  340. package/frontend-vue/app/components/CategoryInput.vue +67 -0
  341. package/frontend-vue/app/components/Editor.vue +63 -0
  342. package/frontend-vue/app/components/FunnelCard.vue +66 -0
  343. package/frontend-vue/app/components/HtmlPreviewModal.vue +20 -0
  344. package/frontend-vue/app/components/NewTemplateModal.vue +158 -0
  345. package/frontend-vue/app/components/ProviderChip.vue +39 -0
  346. package/frontend-vue/app/components/SendDrawer.vue +298 -0
  347. package/frontend-vue/app/components/TemplateCard.vue +127 -0
  348. package/frontend-vue/app/components/TemplateDrawer.vue +394 -0
  349. package/frontend-vue/app/components/TemplateGallery.vue +112 -0
  350. package/frontend-vue/app/components/TemplatePreviewFrame.vue +103 -0
  351. package/frontend-vue/app/components/TestSendModal.vue +162 -0
  352. package/frontend-vue/app/components/TokenText.vue +26 -0
  353. package/frontend-vue/app/components/blocks/Button.vue +19 -0
  354. package/frontend-vue/app/components/blocks/Code.vue +17 -0
  355. package/frontend-vue/app/components/blocks/Divider.vue +3 -0
  356. package/frontend-vue/app/components/blocks/Footer.vue +22 -0
  357. package/frontend-vue/app/components/blocks/Heading.vue +18 -0
  358. package/frontend-vue/app/components/blocks/Hero.vue +32 -0
  359. package/frontend-vue/app/components/blocks/List.vue +33 -0
  360. package/frontend-vue/app/components/blocks/Paragraph.vue +18 -0
  361. package/frontend-vue/app/components/blocks/Total.vue +18 -0
  362. package/frontend-vue/app/components/editor/EditorBlock.vue +121 -0
  363. package/frontend-vue/app/components/editor/EditorBlockList.vue +44 -0
  364. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +186 -0
  365. package/frontend-vue/app/components/editor/EditorCanvas.vue +91 -0
  366. package/frontend-vue/app/components/editor/EditorConditionFields.vue +88 -0
  367. package/frontend-vue/app/components/editor/EditorDataPanel.vue +128 -0
  368. package/frontend-vue/app/components/editor/EditorDraftBar.vue +35 -0
  369. package/frontend-vue/app/components/editor/EditorIfBlock.vue +120 -0
  370. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +62 -0
  371. package/frontend-vue/app/components/editor/EditorPalette.vue +62 -0
  372. package/frontend-vue/app/components/editor/EditorRail.vue +58 -0
  373. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +37 -0
  374. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +87 -0
  375. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +66 -0
  376. package/frontend-vue/app/components/editor/EditorToolbar.vue +155 -0
  377. package/frontend-vue/app/components/editor/EditorVariables.vue +163 -0
  378. package/frontend-vue/app/components/editor/EditorWorkspace.vue +189 -0
  379. package/frontend-vue/app/composables/useEditorContext.ts +44 -0
  380. package/frontend-vue/app/composables/useMailingApi.ts +197 -0
  381. package/frontend-vue/app/composables/useMailingPeriodData.ts +39 -0
  382. package/frontend-vue/app/composables/useTemplateDrawer.ts +90 -0
  383. package/frontend-vue/app/composables/useTemplateEditor.ts +228 -0
  384. package/frontend-vue/app/emails/EmailMailingTemplate.vue +24 -0
  385. package/frontend-vue/app/emails/components/MailingBlocks.vue +117 -0
  386. package/frontend-vue/app/plugins/template-gallery-display.client.ts +14 -0
  387. package/frontend-vue/app/types/component.ts +29 -0
  388. package/frontend-vue/app/types/mailing.ts +365 -0
  389. package/frontend-vue/app/utils/blocks/components.ts +14 -0
  390. package/frontend-vue/app/utils/blocks/dnd.ts +15 -0
  391. package/frontend-vue/app/utils/blocks/factory.ts +100 -0
  392. package/frontend-vue/app/utils/blocks/fields.ts +58 -0
  393. package/frontend-vue/app/utils/blocks/index.ts +5 -0
  394. package/frontend-vue/app/utils/blocks/tree.ts +74 -0
  395. package/frontend-vue/app/utils/categories.ts +54 -0
  396. package/frontend-vue/app/utils/clone.ts +3 -0
  397. package/frontend-vue/app/utils/conditions.ts +50 -0
  398. package/frontend-vue/app/utils/editor-route.ts +18 -0
  399. package/frontend-vue/app/utils/gallery.ts +41 -0
  400. package/frontend-vue/app/utils/locales.ts +41 -0
  401. package/frontend-vue/app/utils/paths.ts +84 -0
  402. package/frontend-vue/app/utils/period.ts +23 -0
  403. package/frontend-vue/app/utils/testData.ts +10 -0
  404. package/frontend-vue/app/utils/timeline.ts +68 -0
  405. package/frontend-vue/app/utils/tokens.ts +24 -0
  406. package/frontend-vue/app/utils/variables.ts +48 -0
  407. package/frontend-vue/dms.email.ts +9 -0
  408. package/frontend-vue/dms.frontend.ts +29 -0
  409. package/frontend-vue/eslint.config.mjs +13 -0
  410. package/frontend-vue/i18n/locales/mailing-en-GB.json +397 -0
  411. package/frontend-vue/i18n/locales/mailing-fr-FR.json +397 -0
  412. package/frontend-vue/package.json +32 -0
  413. package/frontend-vue/pnpm-lock.yaml +6446 -0
  414. package/frontend-vue/pnpm-workspace.yaml +2 -0
  415. package/frontend-vue/tests/block-components.test.ts +14 -0
  416. package/frontend-vue/tests/block-fields.test.ts +23 -0
  417. package/frontend-vue/tests/blocks-factory.test.ts +28 -0
  418. package/frontend-vue/tests/blocks-tree.test.ts +49 -0
  419. package/frontend-vue/tests/categories.test.ts +64 -0
  420. package/frontend-vue/tests/conditions.test.ts +36 -0
  421. package/frontend-vue/tests/dnd.test.ts +11 -0
  422. package/frontend-vue/tests/editor-block-list.test.ts +54 -0
  423. package/frontend-vue/tests/editor-route.test.ts +23 -0
  424. package/frontend-vue/tests/email-render-cases.json +77 -0
  425. package/frontend-vue/tests/gallery.test.ts +39 -0
  426. package/frontend-vue/tests/i18n-editor.test.ts +21 -0
  427. package/frontend-vue/tests/i18n.test.ts +83 -0
  428. package/frontend-vue/tests/locales.test.ts +38 -0
  429. package/frontend-vue/tests/mailing-api.test.ts +10 -0
  430. package/frontend-vue/tests/paths.test.ts +128 -0
  431. package/frontend-vue/tests/period.test.ts +13 -0
  432. package/frontend-vue/tests/smoke.test.ts +8 -0
  433. package/frontend-vue/tests/template-editor.test.ts +65 -0
  434. package/frontend-vue/tests/test-data.test.ts +15 -0
  435. package/frontend-vue/tests/timeline.test.ts +29 -0
  436. package/frontend-vue/tests/tokens.test.ts +12 -0
  437. package/frontend-vue/tests/variables.test.ts +46 -0
  438. package/frontend-vue/vitest.config.ts +10 -0
  439. package/package.json +94 -0
  440. package/src/antelope-modules.ts +48 -0
  441. package/src/automation/email-event-trigger.ts +112 -0
  442. package/src/automation/index.ts +33 -0
  443. package/src/automation/send-template-action.ts +100 -0
  444. package/src/config.ts +15 -0
  445. package/src/constants.ts +42 -0
  446. package/src/crons/index.ts +1 -0
  447. package/src/crons/retention.ts +126 -0
  448. package/src/data/body.ts +14 -0
  449. package/src/data/index.ts +3 -0
  450. package/src/data/sends-table.ts +154 -0
  451. package/src/data/templates-table.ts +202 -0
  452. package/src/data-types/categories-type.ts +29 -0
  453. package/src/data-types/category-type.ts +44 -0
  454. package/src/data-types/index.ts +2 -0
  455. package/src/db/index.ts +2 -0
  456. package/src/db/models/index.ts +4 -0
  457. package/src/db/models/send-events.model.ts +46 -0
  458. package/src/db/models/sends.model.ts +147 -0
  459. package/src/db/models/settings.model.ts +12 -0
  460. package/src/db/models/templates.model.ts +12 -0
  461. package/src/db/tables/index.ts +4 -0
  462. package/src/db/tables/send-events.table.ts +31 -0
  463. package/src/db/tables/sends.table.ts +85 -0
  464. package/src/db/tables/settings.table.ts +49 -0
  465. package/src/db/tables/templates.table.ts +70 -0
  466. package/src/engine/conditions.ts +30 -0
  467. package/src/engine/coverage.ts +67 -0
  468. package/src/engine/fallback-html.ts +116 -0
  469. package/src/engine/index.ts +7 -0
  470. package/src/engine/paths.ts +33 -0
  471. package/src/engine/resolve.ts +178 -0
  472. package/src/engine/tokens.ts +35 -0
  473. package/src/engine/walk.ts +18 -0
  474. package/src/hooks/index.ts +1 -0
  475. package/src/hooks/tenant-export.ts +127 -0
  476. package/src/implementations/dms-mailing/index.ts +2 -0
  477. package/src/index.ts +62 -0
  478. package/src/pages/editor.ts +23 -0
  479. package/src/pages/index.ts +11 -0
  480. package/src/pages/module.ts +9 -0
  481. package/src/pages/overview.ts +175 -0
  482. package/src/pages/sends.ts +174 -0
  483. package/src/pages/settings/form.ts +67 -0
  484. package/src/pages/settings/index.ts +2 -0
  485. package/src/pages/settings/page.ts +34 -0
  486. package/src/pages/templates.ts +141 -0
  487. package/src/permissions.ts +44 -0
  488. package/src/quick-actions.ts +20 -0
  489. package/src/realtime.ts +53 -0
  490. package/src/routes/events.ts +57 -0
  491. package/src/routes/index.ts +7 -0
  492. package/src/routes/metrics.ts +243 -0
  493. package/src/routes/period.ts +46 -0
  494. package/src/routes/provider.ts +31 -0
  495. package/src/routes/sends.ts +215 -0
  496. package/src/routes/settings.ts +66 -0
  497. package/src/routes/templates.ts +256 -0
  498. package/src/services/attention.ts +151 -0
  499. package/src/services/categories.ts +43 -0
  500. package/src/services/events.ts +121 -0
  501. package/src/services/metrics.ts +175 -0
  502. package/src/services/notify.ts +90 -0
  503. package/src/services/permissions.ts +42 -0
  504. package/src/services/provider.ts +46 -0
  505. package/src/services/render.ts +33 -0
  506. package/src/services/send.ts +409 -0
  507. package/src/services/settings.ts +74 -0
  508. package/src/services/status.ts +16 -0
  509. package/src/services/templates.ts +157 -0
  510. package/src/test/antelope.test.ts +188 -0
  511. package/src/test/helpers/http.ts +23 -0
  512. package/src/test/helpers/owner.ts +37 -0
  513. package/src/test/integration/events.test.ts +136 -0
  514. package/src/test/integration/retention.test.ts +363 -0
  515. package/src/test/integration/sends.test.ts +200 -0
  516. package/src/test/integration/settings.test.ts +50 -0
  517. package/src/test/integration/setup.test.ts +35 -0
  518. package/src/test/integration/templates.test.ts +229 -0
  519. package/src/test/integration/tenant-export.test.ts +125 -0
  520. package/src/test/unit/attention.test.ts +129 -0
  521. package/src/test/unit/automation.test.ts +130 -0
  522. package/src/test/unit/categories.test.ts +51 -0
  523. package/src/test/unit/engine/conditions.test.ts +35 -0
  524. package/src/test/unit/engine/coverage.test.ts +118 -0
  525. package/src/test/unit/engine/fallback-html.test.ts +86 -0
  526. package/src/test/unit/engine/paths.test.ts +39 -0
  527. package/src/test/unit/engine/resolve.test.ts +176 -0
  528. package/src/test/unit/engine/tokens.test.ts +26 -0
  529. package/src/test/unit/events.test.ts +19 -0
  530. package/src/test/unit/metrics.test.ts +170 -0
  531. package/src/test/unit/notify.test.ts +71 -0
  532. package/src/test/unit/period.test.ts +64 -0
  533. package/src/test/unit/provider.test.ts +57 -0
  534. package/src/test/unit/realtime.test.ts +11 -0
  535. package/src/test/unit/render.test.ts +23 -0
  536. package/src/test/unit/retention.test.ts +91 -0
  537. package/src/test/unit/send-batch.test.ts +72 -0
  538. package/src/test/unit/send.test.ts +161 -0
  539. package/src/test/unit/smoke.test.ts +8 -0
  540. package/src/test/unit/status.test.ts +14 -0
  541. package/src/test/unit/templates.test.ts +52 -0
  542. package/src/test/unit/tenant-export.test.ts +88 -0
  543. package/src/test/unit/webhook-secret.test.ts +50 -0
  544. package/src/types/blocks.ts +31 -0
  545. package/src/types/index.ts +4 -0
  546. package/src/types/resolved.ts +89 -0
  547. package/src/types/sends.ts +44 -0
  548. package/src/types/settings.ts +24 -0
  549. package/src/validation/settings.schema.ts +33 -0
  550. package/src/validation/templates.schema.ts +145 -0
@@ -0,0 +1,394 @@
1
+ <script setup lang="ts">
2
+ import { localeCoverage } from '../utils/locales'
3
+ import { editorRoute } from '../utils/editor-route'
4
+ import { mergeVariables } from '../utils/variables'
5
+ import type {
6
+ TemplateContentResponse,
7
+ TemplateRow,
8
+ VariableDefinition,
9
+ VariableType,
10
+ } from '../types/mailing'
11
+ import { VARIABLE_TYPES } from '../types/mailing'
12
+
13
+ interface TemplateDrawerProps {
14
+ templateId?: string
15
+ rowData?: TemplateRow
16
+ onSuccessCallback?: () => void
17
+ }
18
+
19
+ type StatusAction = 'publish' | 'unpublish' | 'archive'
20
+
21
+ const STATUS_DONE_KEYS: Record<StatusAction, string> = {
22
+ publish: 'published',
23
+ unpublish: 'unpublished',
24
+ archive: 'archived',
25
+ }
26
+
27
+ const PREVIEW_WIDTH = 600
28
+ const PREVIEW_SCALE = 0.53
29
+ const MISSING_LOCALE_MARK = ' ·'
30
+
31
+ const props = defineProps<TemplateDrawerProps>()
32
+ const emit = defineEmits<{ success: [] }>()
33
+
34
+ const api = useMailingApi()
35
+ const toast = useToast()
36
+ const { t } = useI18n()
37
+ const { uniqueLocales } = useUniqueLocales()
38
+ const { open: openTestSendModal } = useTestSendModal()
39
+ const { open: openRealSendModal } = useRealSendModal()
40
+
41
+ const templateId = computed(() => props.rowData?._id ?? props.templateId ?? '')
42
+
43
+ const detail = ref<TemplateContentResponse | null>(null)
44
+ const loading = ref(false)
45
+ 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
+ })
53
+
54
+ const template = computed(() => detail.value?.template ?? props.rowData ?? null)
55
+
56
+ const coverage = computed(() =>
57
+ detail.value
58
+ ? localeCoverage(
59
+ Object.keys(detail.value.content.locales),
60
+ uniqueLocales.value.map((entry) => entry.code),
61
+ )
62
+ : [],
63
+ )
64
+
65
+ const localeItems = computed(() =>
66
+ coverage.value.map((entry) => ({
67
+ label:
68
+ entry.code.toUpperCase() + (entry.present ? '' : MISSING_LOCALE_MARK),
69
+ value: entry.code,
70
+ })),
71
+ )
72
+
73
+ const isLocaleMissing = computed(() =>
74
+ coverage.value.some((entry) => entry.code === locale.value && !entry.present),
75
+ )
76
+
77
+ const variables = computed(() =>
78
+ mergeVariables(
79
+ detail.value?.detectedVariables ?? [],
80
+ detail.value?.variables ?? [],
81
+ ),
82
+ )
83
+
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),
90
+ )
91
+
92
+ watch(undeclaredPaths, (paths) => {
93
+ if (!newVariable.path) newVariable.path = paths[0] ?? ''
94
+ })
95
+
96
+ const variableTypeItems = VARIABLE_TYPES.map((type: VariableType) => ({
97
+ label: type,
98
+ value: type,
99
+ }))
100
+
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
112
+ }
113
+ }
114
+
115
+ function settle(): void {
116
+ if (props.onSuccessCallback) {
117
+ props.onSuccessCallback()
118
+ return
119
+ }
120
+ void load()
121
+ }
122
+
123
+ function closeContainer(): void {
124
+ if (props.onSuccessCallback) {
125
+ props.onSuccessCallback()
126
+ return
127
+ }
128
+ emit('success')
129
+ }
130
+
131
+ async function openEditor(targetLocale?: string): Promise<void> {
132
+ closeContainer()
133
+ await nextTick()
134
+ await navigateDms(editorRoute(templateId.value, targetLocale ?? locale.value))
135
+ }
136
+
137
+ async function runStatusAction(action: StatusAction): Promise<void> {
138
+ const actions = {
139
+ publish: api.publish,
140
+ unpublish: api.unpublish,
141
+ archive: api.archive,
142
+ }
143
+ try {
144
+ await actions[action](templateId.value)
145
+ toast.add({
146
+ color: 'success',
147
+ title: t(`dms_mailing.templates.actions.${STATUS_DONE_KEYS[action]}`),
148
+ })
149
+ settle()
150
+ } catch (error) {
151
+ toast.add({ color: 'error', title: String(error) })
152
+ }
153
+ }
154
+
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
+ ])
161
+
162
+ async function submitDuplicate(): Promise<void> {
163
+ if (!duplicate.slug || !duplicate.name) return
164
+ try {
165
+ await api.duplicate(templateId.value, duplicate.slug, duplicate.name)
166
+ isDuplicateOpen.value = false
167
+ toast.add({
168
+ color: 'success',
169
+ title: t('dms_mailing.templates.actions.duplicate'),
170
+ })
171
+ settle()
172
+ } catch (error) {
173
+ toast.add({ color: 'error', title: String(error) })
174
+ }
175
+ }
176
+
177
+ async function addVariable(): Promise<void> {
178
+ if (!newVariable.path || !detail.value) return
179
+ const variables = [...detail.value.variables, { ...newVariable }]
180
+ try {
181
+ await api.saveVariables(templateId.value, variables)
182
+ detail.value.variables = variables
183
+ newVariable.path = ''
184
+ newVariable.required = false
185
+ } catch (error) {
186
+ toast.add({ color: 'error', title: String(error) })
187
+ }
188
+ }
189
+
190
+ onMounted(load)
191
+ </script>
192
+
193
+ <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" />
200
+
201
+ <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>
250
+ <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')"
300
+ size="sm"
301
+ />
302
+ <UInput
303
+ v-model="duplicate.name"
304
+ :placeholder="t('dms_mailing.templates.cols.name')"
305
+ size="sm"
306
+ />
307
+ <UButton
308
+ size="sm"
309
+ block
310
+ :label="t('dms_mailing.templates.actions.duplicate')"
311
+ :disabled="!duplicate.slug || !duplicate.name"
312
+ @click="submitDuplicate"
313
+ />
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"
334
+ >
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"
350
+ size="sm"
351
+ />
352
+ <UBadge
353
+ v-if="variable.required"
354
+ :label="t('dms_mailing.templates.drawer.required')"
355
+ 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')"
368
+ size="sm"
369
+ class="flex-1"
370
+ list="detected-variable-paths"
371
+ />
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"
380
+ />
381
+ <USwitch v-model="newVariable.required" size="sm" />
382
+ <UButton
383
+ icon="i-ph-plus"
384
+ color="neutral"
385
+ variant="ghost"
386
+ size="sm"
387
+ :disabled="!newVariable.path"
388
+ @click="addVariable"
389
+ />
390
+ </div>
391
+ </div>
392
+ </template>
393
+ </div>
394
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { editorRoute } from '../utils/editor-route'
3
+ import { groupByCategory } from '../utils/gallery'
4
+ import type { GalleryDisplayContext } from '../types/component'
5
+ import type { TemplateCategory, TemplateRow } from '../types/mailing'
6
+
7
+ interface TemplateGalleryProps {
8
+ context: GalleryDisplayContext<TemplateRow>
9
+ }
10
+
11
+ const FIRST_PAGE = 1
12
+
13
+ const props = defineProps<TemplateGalleryProps>()
14
+
15
+ const api = useMailingApi()
16
+ const { t } = useI18n()
17
+ const { open: openTemplateDrawer } = useTemplateDrawer()
18
+ const { open: openTestSendModal } = useTestSendModal()
19
+
20
+ // Fetched, not read from the page options: the tenant edits them in Settings,
21
+ // and a page declaration is serialised once, at registration.
22
+ const categories = ref<TemplateCategory[]>([])
23
+
24
+ onMounted(async () => {
25
+ try {
26
+ const response = await api.listCategories()
27
+ categories.value = response.categories
28
+ } catch {
29
+ categories.value = []
30
+ }
31
+ })
32
+
33
+ const groups = computed(() =>
34
+ groupByCategory(
35
+ props.context.items,
36
+ categories.value,
37
+ t('dms_mailing.templates.uncategorised'),
38
+ ),
39
+ )
40
+
41
+ const page = computed({
42
+ get: () => props.context.pagination.pageIndex + FIRST_PAGE,
43
+ set: (value: number) => props.context.pagination.setPage(value - FIRST_PAGE),
44
+ })
45
+
46
+ function openEditor(template: TemplateRow, locale?: string): void {
47
+ void navigateDms(editorRoute(template._id, locale))
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div class="flex flex-col gap-6 p-4">
53
+ <USkeleton v-if="context.loading && !groups.length" class="h-64 w-full" />
54
+
55
+ <div
56
+ v-else-if="!groups.length"
57
+ class="flex flex-col items-center gap-3 py-16 text-center"
58
+ >
59
+ <UIcon name="i-ph-envelope-simple" class="text-dimmed size-8" />
60
+ <h3 class="text-highlighted text-sm font-semibold">
61
+ {{ t('dms_mailing.templates.empty.title') }}
62
+ </h3>
63
+ <p class="text-muted text-[13px]">
64
+ {{ t('dms_mailing.templates.empty.hint') }}
65
+ </p>
66
+ <UButton
67
+ color="neutral"
68
+ variant="ghost"
69
+ size="sm"
70
+ :label="t('dms_mailing.templates.empty.reset')"
71
+ @click="context.refresh()"
72
+ />
73
+ </div>
74
+
75
+ <div v-else class="flex flex-col gap-6">
76
+ <div v-for="group in groups" :key="group.category.id">
77
+ <div class="mb-3 flex items-center gap-2">
78
+ <UIcon :name="group.category.icon" class="text-dimmed size-4" />
79
+ <h2 class="text-highlighted text-[13px] font-semibold">
80
+ {{ group.category.label }}
81
+ </h2>
82
+ <span class="text-dimmed font-mono text-[11.5px]">
83
+ {{ group.items.length }}
84
+ </span>
85
+ <span class="bg-default ml-2 h-px flex-1" />
86
+ </div>
87
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(272px,1fr))] gap-4">
88
+ <DmsMailingTemplateCard
89
+ v-for="template in group.items"
90
+ :key="template._id"
91
+ :template="template"
92
+ @open="openEditor(template)"
93
+ @open-locale="(code: string) => openEditor(template, code)"
94
+ @details="openTemplateDrawer(template._id)"
95
+ @test-send="openTestSendModal(template._id)"
96
+ />
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <div
102
+ v-if="context.pagination.total > context.pagination.pageSize"
103
+ class="flex justify-end"
104
+ >
105
+ <UPagination
106
+ v-model:page="page"
107
+ :items-per-page="context.pagination.pageSize"
108
+ :total="context.pagination.total"
109
+ />
110
+ </div>
111
+ </div>
112
+ </template>
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ import { useDebounceFn } from '@vueuse/core'
3
+ import type { PreviewResponse, TemplateContent } from '../types/mailing'
4
+
5
+ interface TemplatePreviewFrameProps {
6
+ templateId: string
7
+ /** Absent renders the tenant fallback locale, chosen server-side. */
8
+ locale?: string
9
+ content?: TemplateContent
10
+ data?: Record<string, unknown>
11
+ width: number
12
+ scale?: number
13
+ refreshKey?: number
14
+ }
15
+
16
+ const DEBOUNCE_MS = 300
17
+ const FALLBACK_HEIGHT = 480
18
+
19
+ const props = withDefaults(defineProps<TemplatePreviewFrameProps>(), {
20
+ locale: undefined,
21
+ content: undefined,
22
+ data: undefined,
23
+ scale: 1,
24
+ refreshKey: 0,
25
+ })
26
+
27
+ const emit = defineEmits<{ resolved: [PreviewResponse] }>()
28
+
29
+ const api = useMailingApi()
30
+
31
+ const html = ref('')
32
+ const loading = ref(false)
33
+ const height = ref(FALLBACK_HEIGHT)
34
+ const frame = ref<HTMLIFrameElement | null>(null)
35
+
36
+ async function load(): Promise<void> {
37
+ if (!props.templateId) return
38
+ loading.value = true
39
+ try {
40
+ const preview = await api.preview(props.templateId, {
41
+ locale: props.locale,
42
+ content: props.content,
43
+ data: props.data,
44
+ })
45
+ html.value = preview.html
46
+ emit('resolved', preview)
47
+ } catch {
48
+ html.value = ''
49
+ } finally {
50
+ loading.value = false
51
+ }
52
+ }
53
+
54
+ const reload = useDebounceFn(load, DEBOUNCE_MS)
55
+
56
+ // srcdoc keeps the frame same-origin, so allow-same-origin is what lets the
57
+ // height be measured; scripts stay blocked.
58
+ function measure(): void {
59
+ const body = frame.value?.contentDocument?.body
60
+ height.value = body?.scrollHeight || FALLBACK_HEIGHT
61
+ }
62
+
63
+ watch(
64
+ () => [
65
+ props.templateId,
66
+ props.locale,
67
+ props.content,
68
+ props.data,
69
+ props.refreshKey,
70
+ ],
71
+ () => reload(),
72
+ { immediate: true, deep: true },
73
+ )
74
+
75
+ const boxStyle = computed(() => ({
76
+ width: `${props.width * props.scale}px`,
77
+ height: `${height.value * props.scale}px`,
78
+ }))
79
+
80
+ const frameStyle = computed(() => ({
81
+ width: `${props.width}px`,
82
+ height: `${height.value}px`,
83
+ transform: `scale(${props.scale})`,
84
+ transformOrigin: 'top left',
85
+ }))
86
+ </script>
87
+
88
+ <template>
89
+ <div class="relative overflow-hidden" :style="boxStyle">
90
+ <USkeleton v-if="loading && !html" class="absolute inset-0 size-full" />
91
+ <iframe
92
+ v-show="html"
93
+ ref="frame"
94
+ :srcdoc="html"
95
+ sandbox="allow-same-origin"
96
+ loading="lazy"
97
+ title="preview"
98
+ class="block border-0 bg-white"
99
+ :style="frameStyle"
100
+ @load="measure"
101
+ />
102
+ </div>
103
+ </template>