@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
@@ -0,0 +1,266 @@
1
+ import type {
2
+ Block,
3
+ IfBlock,
4
+ LocaleContent,
5
+ TemplateContent,
6
+ VariableDefinition,
7
+ } from '../types/mailing'
8
+ import { flattenBlocks } from './blocks/tree'
9
+ import { evaluateCondition } from './conditions'
10
+ import { pickLocale } from './locales'
11
+ import { isPresent, readPath } from './paths'
12
+ import { splitTokens } from './tokens'
13
+
14
+ export type EditorCheckState = 'ok' | 'warn' | 'info'
15
+
16
+ /** A check before the i18n pass: its key and the values it interpolates. */
17
+ export interface EditorCheck {
18
+ id: string
19
+ state: EditorCheckState
20
+ key: string
21
+ params: Record<string, string | number>
22
+ }
23
+
24
+ /** What the checks read: declarations, used paths, test data, blocks. */
25
+ export interface CheckInput {
26
+ variables: VariableDefinition[]
27
+ detected: string[]
28
+ data: Record<string, unknown>
29
+ blocks: Block[]
30
+ }
31
+
32
+ const CHECK_KEY_PREFIX = 'dms_mailing.editor.checks'
33
+ const WARNING_STATES: EditorCheckState[] = ['warn']
34
+
35
+ const check = (
36
+ id: string,
37
+ state: EditorCheckState,
38
+ key: string,
39
+ params: Record<string, string | number> = {},
40
+ ): EditorCheck => ({ id, state, key: `${CHECK_KEY_PREFIX}.${key}`, params })
41
+
42
+ /** Paths the content uses that no declaration covers. */
43
+ export function undeclaredPaths(
44
+ detected: string[],
45
+ variables: VariableDefinition[],
46
+ ): string[] {
47
+ const declared = new Set(variables.map((variable) => variable.path))
48
+ return detected.filter((path) => !declared.has(path))
49
+ }
50
+
51
+ /** Required declarations the data set does not provide. */
52
+ export function missingRequired(
53
+ variables: VariableDefinition[],
54
+ data: Record<string, unknown>,
55
+ ): string[] {
56
+ return variables
57
+ .filter((variable) => variable.required)
58
+ .map((variable) => variable.path)
59
+ .filter((path) => !isPresent(readPath(data, path)))
60
+ }
61
+
62
+ /** What the content checks read: the draft and the workspace locales. */
63
+ export interface ContentCheckInput {
64
+ content: TemplateContent
65
+ /** The workspace locales, each of which recipients may ask for. */
66
+ locales: string[]
67
+ fallback: string
68
+ }
69
+
70
+ const FOOTER_TYPE = 'footer'
71
+
72
+ const footerCount = (locale: LocaleContent): number =>
73
+ flattenBlocks(locale.blocks).filter((block) => block.type === FOOTER_TYPE)
74
+ .length
75
+
76
+ /** A rule a locale's body breaks: the check key it reports under. */
77
+ interface BodyRule {
78
+ key: string
79
+ isBroken: (locale: LocaleContent) => boolean
80
+ }
81
+
82
+ const BODY_RULES: BodyRule[] = [
83
+ {
84
+ key: 'no_blocks',
85
+ isBroken: (locale) =>
86
+ !locale.blocks.some((block) => block.type !== FOOTER_TYPE),
87
+ },
88
+ { key: 'no_footer', isBroken: (locale) => footerCount(locale) === 0 },
89
+ { key: 'many_footers', isBroken: (locale) => footerCount(locale) > 1 },
90
+ {
91
+ key: 'footer_not_last',
92
+ isBroken: (locale) =>
93
+ footerCount(locale) > 0 && locale.blocks.at(-1)?.type !== FOOTER_TYPE,
94
+ },
95
+ ]
96
+
97
+ const localeEntries = (content: TemplateContent): [string, LocaleContent][] =>
98
+ Object.entries(content.locales)
99
+
100
+ /** Locales a publish is refused for: a send would go out without a subject. */
101
+ export function publishBlockers(content: TemplateContent): EditorCheck[] {
102
+ return localeEntries(content)
103
+ .filter(([, locale]) => !locale.subject.trim())
104
+ .map(([code]) =>
105
+ check(`subject-${code}`, 'warn', 'empty_subject', {
106
+ locale: code.toUpperCase(),
107
+ }),
108
+ )
109
+ }
110
+
111
+ function bodyChecks(content: TemplateContent): EditorCheck[] {
112
+ return localeEntries(content).flatMap(([code, locale]) =>
113
+ BODY_RULES.filter((rule) => rule.isBroken(locale)).map((rule) =>
114
+ check(`${rule.key}-${code}`, 'warn', rule.key, {
115
+ locale: code.toUpperCase(),
116
+ }),
117
+ ),
118
+ )
119
+ }
120
+
121
+ /** Every text a locale shows: its envelope and each block's string fields. */
122
+ function localeTexts(locale: LocaleContent): string[] {
123
+ const fields = flattenBlocks(locale.blocks).flatMap((block) =>
124
+ Object.values(block as unknown as Record<string, unknown>),
125
+ )
126
+ return [
127
+ locale.subject,
128
+ locale.preheader,
129
+ ...fields.filter((value): value is string => typeof value === 'string'),
130
+ ]
131
+ }
132
+
133
+ function tokensOf(locale: LocaleContent, kind: 'token' | 'invalid'): string[] {
134
+ return localeTexts(locale)
135
+ .flatMap(splitTokens)
136
+ .filter((part) => part.kind === kind)
137
+ .map((part) => part.value)
138
+ }
139
+
140
+ /** `{{…}}` look-alikes the engine leaves as typed: customers would see them. */
141
+ function malformedTokenChecks(content: TemplateContent): EditorCheck[] {
142
+ return localeEntries(content).flatMap(([code, locale]) =>
143
+ [...new Set(tokensOf(locale, 'invalid'))].map((token) =>
144
+ check(`token-${code}-${token}`, 'warn', 'invalid_token', {
145
+ locale: code.toUpperCase(),
146
+ token,
147
+ }),
148
+ ),
149
+ )
150
+ }
151
+
152
+ const isStructured = (value: unknown): boolean =>
153
+ value !== null && typeof value === 'object' && !(value instanceof Date)
154
+
155
+ /** Tokens whose test value is an object or a list: they print as raw data. */
156
+ function structuredTokenChecks(
157
+ content: TemplateContent,
158
+ data: Record<string, unknown>,
159
+ ): EditorCheck[] {
160
+ const paths = localeEntries(content).flatMap(([, locale]) =>
161
+ tokensOf(locale, 'token'),
162
+ )
163
+ return [...new Set(paths)]
164
+ .filter((path) => isStructured(readPath(data, path)))
165
+ .map((path) =>
166
+ check(`structured-${path}`, 'warn', 'token_not_text', { path }),
167
+ )
168
+ }
169
+
170
+ /** What the publish review warns about the content's `{{…}}` tokens. */
171
+ export function tokenChecks(
172
+ content: TemplateContent,
173
+ data: Record<string, unknown>,
174
+ ): EditorCheck[] {
175
+ return [
176
+ ...malformedTokenChecks(content),
177
+ ...structuredTokenChecks(content, data),
178
+ ]
179
+ }
180
+
181
+ function missingLocaleChecks(input: ContentCheckInput): EditorCheck[] {
182
+ const present = Object.keys(input.content.locales)
183
+ if (!present.length) return []
184
+ return input.locales
185
+ .filter((code) => !input.content.locales[code])
186
+ .map((code) =>
187
+ check(`locale-${code}`, 'warn', 'missing_locale', {
188
+ locale: code.toUpperCase(),
189
+ fallback: pickLocale(present, code, input.fallback).toUpperCase(),
190
+ }),
191
+ )
192
+ }
193
+
194
+ /** What the publish review warns about the draft's content itself. */
195
+ export function contentChecks(input: ContentCheckInput): EditorCheck[] {
196
+ return [...bodyChecks(input.content), ...missingLocaleChecks(input)]
197
+ }
198
+
199
+ /** The failed checks the publish review warns about. */
200
+ export function publishChecks(input: CheckInput): EditorCheck[] {
201
+ return [
202
+ ...undeclaredPaths(input.detected, input.variables).map((path) =>
203
+ check(`undeclared-${path}`, 'warn', 'undeclared', { path }),
204
+ ),
205
+ ...missingRequired(input.variables, input.data).map((path) =>
206
+ check(`required-${path}`, 'warn', 'required_missing', { path }),
207
+ ),
208
+ ]
209
+ }
210
+
211
+ function requiredCheck(input: CheckInput): EditorCheck[] {
212
+ const required = input.variables.filter((variable) => variable.required)
213
+ if (!required.length) return []
214
+ const missing = missingRequired(input.variables, input.data)
215
+ if (!missing.length)
216
+ return [
217
+ check('required', 'ok', 'required_covered', { count: required.length }),
218
+ ]
219
+ return missing
220
+ .filter((path) => !input.detected.includes(path))
221
+ .map((path) =>
222
+ check(`required-${path}`, 'warn', 'required_missing', { path }),
223
+ )
224
+ }
225
+
226
+ function coverageChecks(input: CheckInput): EditorCheck[] {
227
+ const undeclared = new Set(undeclaredPaths(input.detected, input.variables))
228
+ return input.detected
229
+ .filter((path) => !isPresent(readPath(input.data, path)))
230
+ .map((path) =>
231
+ check(
232
+ `missing-${path}`,
233
+ 'warn',
234
+ undeclared.has(path) ? 'missing_undeclared' : 'missing',
235
+ { path },
236
+ ),
237
+ )
238
+ }
239
+
240
+ function conditionChecks(input: CheckInput): EditorCheck[] {
241
+ return flattenBlocks(input.blocks)
242
+ .filter((block): block is IfBlock => block.type === 'if')
243
+ .map((block) => {
244
+ const result = evaluateCondition(block.condition, input.data)
245
+ return check(
246
+ `condition-${block.id}`,
247
+ 'info',
248
+ result ? 'condition_true' : 'condition_false',
249
+ { path: block.condition.path },
250
+ )
251
+ })
252
+ }
253
+
254
+ /** The Test data tab's checks: coverage, then how each condition evaluates. */
255
+ export function testDataChecks(input: CheckInput): EditorCheck[] {
256
+ return [
257
+ ...requiredCheck(input),
258
+ ...coverageChecks(input),
259
+ ...conditionChecks(input),
260
+ ]
261
+ }
262
+
263
+ /** How many checks need a look: the Test data tab's badge. */
264
+ export function openIssueCount(checks: EditorCheck[]): number {
265
+ return checks.filter((entry) => WARNING_STATES.includes(entry.state)).length
266
+ }
@@ -1,7 +1,14 @@
1
- export const EDITOR_PATH = '/modules/mailing/editor'
1
+ import { TEMPLATES_PATH } from './pages'
2
+
3
+ const PATH_SEPARATOR = '/'
4
+
5
+ /**
6
+ * The DMS state key the breadcrumb reads the record label from (the same one
7
+ * the core's `usePageRecordLabel` writes).
8
+ */
9
+ export const RECORD_LABEL_STATE_KEY = 'dms-page-record-label'
2
10
 
3
11
  export interface EditorRouteQuery extends Record<string, string | undefined> {
4
- id: string
5
12
  locale?: string
6
13
  }
7
14
 
@@ -10,9 +17,41 @@ export interface EditorRoute {
10
17
  query: EditorRouteQuery
11
18
  }
12
19
 
20
+ /** Where the editor of `templateId` lives, optionally on one locale. */
13
21
  export function editorRoute(templateId: string, locale?: string): EditorRoute {
14
22
  return {
15
- path: EDITOR_PATH,
16
- query: locale ? { id: templateId, locale } : { id: templateId },
23
+ path: `${TEMPLATES_PATH}${PATH_SEPARATOR}${encodeURIComponent(templateId)}`,
24
+ query: locale ? { locale } : {},
17
25
  }
18
26
  }
27
+
28
+ export interface EditorRouteParams {
29
+ id?: string | number
30
+ }
31
+
32
+ /**
33
+ * The template id of the editor page: the `:id` route param the DMS hands the
34
+ * component, else the last segment of the current path.
35
+ */
36
+ export function templateIdFrom(
37
+ params: EditorRouteParams | undefined,
38
+ path: string,
39
+ ): string {
40
+ if (params?.id !== undefined && params.id !== '') return String(params.id)
41
+ const last = path.split(PATH_SEPARATOR).filter(Boolean).at(-1) ?? ''
42
+ return decodeURIComponent(last)
43
+ }
44
+
45
+ /** The record label the breadcrumb ends with: the template's name. */
46
+ export interface RecordLabel {
47
+ path: string
48
+ label: string
49
+ }
50
+
51
+ /** Builds the breadcrumb label for the page at `path`; no name clears it. */
52
+ export function recordLabelFor(
53
+ path: string,
54
+ name: string | undefined,
55
+ ): RecordLabel | null {
56
+ return name ? { path, label: name } : null
57
+ }
@@ -0,0 +1,74 @@
1
+ import type { FunnelKind, FunnelStep } from '../types/mailing'
2
+
3
+ /** The step that leaks out of the funnel rather than going down it. */
4
+ export const FUNNEL_LEAK_STEP = 'unsubscribed'
5
+
6
+ export const FUNNEL_KINDS: FunnelKind[] = ['all', 'transactional', 'marketing']
7
+
8
+ /**
9
+ * A step as `GET /metrics/funnel` answers it: `rate` is `null` on an empty
10
+ * base, `isMeasured` false when nothing reports the events it counts.
11
+ */
12
+ export interface FunnelStepReading extends Omit<FunnelStep, 'rate'> {
13
+ rate: number | null
14
+ isMeasured?: boolean
15
+ }
16
+
17
+ export interface FunnelDrop {
18
+ from: FunnelStepReading
19
+ to: FunnelStepReading
20
+ /** Share of the sends lost between the two steps, in points. */
21
+ points: number
22
+ }
23
+
24
+ /** What the card's footer says about the funnel. */
25
+ export type FunnelTakeaway = 'empty' | 'untracked' | 'no_loss' | 'drop'
26
+
27
+ const RATE_DECIMALS = 1
28
+ const PERCENT = 100
29
+ const ABSENT_RATE = '—'
30
+
31
+ /** `false` only when the backend says nothing reports the step's events. */
32
+ export const isMeasured = (step: FunnelStepReading): boolean =>
33
+ step.isMeasured !== false
34
+
35
+ /**
36
+ * The biggest fall between two consecutive steps of the funnel (the leak and
37
+ * the unmeasured steps excluded), measured on the sends lost. `null` when
38
+ * nothing was sent or no step loses anything.
39
+ */
40
+ export function biggestDrop(steps: FunnelStepReading[]): FunnelDrop | null {
41
+ const path = steps.filter(
42
+ (step) => step.id !== FUNNEL_LEAK_STEP && isMeasured(step),
43
+ )
44
+ const total = path[0]?.count ?? 0
45
+ if (!total) return null
46
+ const drops = path.slice(1).map((step, index) => {
47
+ const from = path[index] as FunnelStepReading
48
+ return {
49
+ from,
50
+ to: step,
51
+ points: ((from.count - step.count) / total) * PERCENT,
52
+ }
53
+ })
54
+ return drops.reduce<FunnelDrop | null>(
55
+ (best, drop) => (drop.points > (best?.points ?? 0) ? drop : best),
56
+ null,
57
+ )
58
+ }
59
+
60
+ /** Which sentence the footer writes: there may be no drop for three reasons. */
61
+ export function funnelTakeaway(steps: FunnelStepReading[]): FunnelTakeaway {
62
+ if (!steps[0]?.count) return 'empty'
63
+ if (biggestDrop(steps)) return 'drop'
64
+ return steps.some((step) => !isMeasured(step)) ? 'untracked' : 'no_loss'
65
+ }
66
+
67
+ /** A funnel rate as the card prints it: one decimal, none when whole. */
68
+ export function formatRate(rate: number | null, locale: string): string {
69
+ if (rate === null) return ABSENT_RATE
70
+ return new Intl.NumberFormat(locale, {
71
+ style: 'percent',
72
+ maximumFractionDigits: RATE_DECIMALS,
73
+ }).format(rate / PERCENT)
74
+ }
@@ -6,7 +6,7 @@ export interface CategoryGroup<T> {
6
6
  }
7
7
 
8
8
  export interface CategorizedRow {
9
- category?: string
9
+ category?: string | null
10
10
  }
11
11
 
12
12
  export const UNCATEGORISED_GROUP_ID = 'uncategorised'
@@ -39,3 +39,181 @@ export function groupByCategory<T extends CategorizedRow>(
39
39
  }
40
40
  return [...known, uncategorised].filter((group) => group.items.length > 0)
41
41
  }
42
+
43
+ /** The slice of the DMS `TableViewDisplayPagination` the gallery reads. */
44
+ export interface GalleryPagination {
45
+ pageIndex: number
46
+ pageSize: number
47
+ total: number
48
+ setPage: (index: number) => void
49
+ mode?: string
50
+ hasMore?: boolean
51
+ loadMore?: () => void
52
+ }
53
+
54
+ /** The slice of the DMS `TableViewDisplayActions` the gallery reads. */
55
+ export interface GalleryActions<T> {
56
+ open: (item: T) => void
57
+ }
58
+
59
+ /** The slice of the TanStack table the gallery reaches for its reset. */
60
+ export interface GalleryTable {
61
+ setGlobalFilter: (value: string) => void
62
+ }
63
+
64
+ /**
65
+ * The slice of the DMS `TableViewDisplayContext` the gallery display reads,
66
+ * declared structurally: the layer does not import from the dms-ui layer.
67
+ */
68
+ export interface GalleryContext<T> {
69
+ items: T[]
70
+ loading: boolean
71
+ pagination: GalleryPagination
72
+ actions?: GalleryActions<T>
73
+ /** Active filters, search and sort of the table, without paging. */
74
+ query?: Record<string, unknown>
75
+ refresh: () => Promise<void> | void
76
+ table?: GalleryTable
77
+ }
78
+
79
+ const SEARCH_KEY = 'search'
80
+ const FILTER_PREFIX = 'filter_'
81
+ /** The "Needs attention" tab filters on this column. */
82
+ const ATTENTION_FILTER_KEY = `${FILTER_PREFIX}needsAttention`
83
+ /** The tabs filter on these columns: a tab is not a user filter. */
84
+ const TAB_FILTER_KEYS = new Set([
85
+ `${FILTER_PREFIX}status`,
86
+ ATTENTION_FILTER_KEY,
87
+ ])
88
+
89
+ /** The search typed in the table, trimmed; empty without one. */
90
+ export function searchTermOf(
91
+ query: Record<string, unknown> | undefined,
92
+ ): string {
93
+ const value = query?.[SEARCH_KEY]
94
+ return typeof value === 'string' ? value.trim() : ''
95
+ }
96
+
97
+ /** The filters the user set (quick filters, filter rows), not the tab. */
98
+ export function userFilterKeys(
99
+ query: Record<string, unknown> | undefined,
100
+ ): string[] {
101
+ return Object.keys(query ?? {}).filter(
102
+ (key) => key.startsWith(FILTER_PREFIX) && !TAB_FILTER_KEYS.has(key),
103
+ )
104
+ }
105
+
106
+ /** Whether the table shows its "Needs attention" tab. */
107
+ export function isAttentionView(
108
+ query: Record<string, unknown> | undefined,
109
+ ): boolean {
110
+ return ATTENTION_FILTER_KEY in (query ?? {})
111
+ }
112
+
113
+ /** Whether a search or a filter narrows what the gallery lists. */
114
+ export function isNarrowed(
115
+ query: Record<string, unknown> | undefined,
116
+ ): boolean {
117
+ return Boolean(searchTermOf(query)) || userFilterKeys(query).length > 0
118
+ }
119
+
120
+ /** Whether `row` holds `search`, whatever its case, in one of `fields`. */
121
+ export function matchesSearch(
122
+ row: object,
123
+ search: string,
124
+ fields: string[],
125
+ ): boolean {
126
+ const term = search.trim().toLowerCase()
127
+ const cellOf = (field: string) =>
128
+ String((row as Record<string, unknown>)[field] ?? '').toLowerCase()
129
+ return !term || fields.some((field) => cellOf(field).includes(term))
130
+ }
131
+
132
+ const SORT_KEY = 'sortKey'
133
+
134
+ /** The table's sort column; absent without one. */
135
+ export function sortKeyOf(
136
+ query: Record<string, unknown> | undefined,
137
+ ): string | undefined {
138
+ const key = query?.[SORT_KEY]
139
+ return typeof key === 'string' && key ? key : undefined
140
+ }
141
+
142
+ const MS_PER_HOUR = 3_600_000
143
+ const MS_PER_MINUTE = 60_000
144
+ const HOURS_PER_DAY = 24
145
+
146
+ /** Words a sentence puts around an absolute date ("le 27 sept." in French). */
147
+ export type OnDate = (date: string) => string
148
+
149
+ const bareDate: OnDate = (date) => date
150
+
151
+ /**
152
+ * A date as a card reads it: "2 hr. ago" / "5 min. ago" within a day, "Sep 27"
153
+ * after. `onDate` dresses the absolute one only, so a sentence written around
154
+ * it ("Edited {date}") reads right in both cases. Empty for an unreadable date.
155
+ */
156
+ export function formatCardDate(
157
+ date: string | null | undefined,
158
+ now: Date,
159
+ locale: string,
160
+ onDate: OnDate = bareDate,
161
+ ): string {
162
+ const time = date ? new Date(date).getTime() : Number.NaN
163
+ if (Number.isNaN(time)) return ''
164
+ const elapsed = Math.max(0, now.getTime() - time)
165
+ const relative = new Intl.RelativeTimeFormat(locale, {
166
+ numeric: 'auto',
167
+ style: 'short',
168
+ })
169
+ if (elapsed < MS_PER_HOUR) {
170
+ return relative.format(-Math.round(elapsed / MS_PER_MINUTE), 'minute')
171
+ }
172
+ if (elapsed < HOURS_PER_DAY * MS_PER_HOUR) {
173
+ return relative.format(-Math.round(elapsed / MS_PER_HOUR), 'hour')
174
+ }
175
+ return onDate(
176
+ new Intl.DateTimeFormat(locale, {
177
+ month: 'short',
178
+ day: '2-digit',
179
+ }).format(new Date(time)),
180
+ )
181
+ }
182
+
183
+ /**
184
+ * How long ago `date` was, the way "live since…" reads it: a duration within a
185
+ * day ("1 hr", "5 min"; "1 h" in French), the date after ("Sep 27"). `null`
186
+ * for an unreadable date.
187
+ */
188
+ export interface CardSince {
189
+ kind: 'duration' | 'date'
190
+ text: string
191
+ }
192
+
193
+ export function formatCardSince(
194
+ date: string | null | undefined,
195
+ now: Date,
196
+ locale: string,
197
+ ): CardSince | null {
198
+ const time = date ? new Date(date).getTime() : Number.NaN
199
+ if (Number.isNaN(time)) return null
200
+ const elapsed = Math.max(0, now.getTime() - time)
201
+ if (elapsed >= HOURS_PER_DAY * MS_PER_HOUR)
202
+ return { kind: 'date', text: formatCardDate(date, now, locale) }
203
+ const isHours = elapsed >= MS_PER_HOUR
204
+ const amount = Math.max(
205
+ 1,
206
+ Math.round(elapsed / (isHours ? MS_PER_HOUR : MS_PER_MINUTE)),
207
+ )
208
+ const text = new Intl.NumberFormat(locale, {
209
+ style: 'unit',
210
+ unit: isHours ? 'hour' : 'minute',
211
+ unitDisplay: 'short',
212
+ }).format(amount)
213
+ return { kind: 'duration', text }
214
+ }
215
+
216
+ /** The first word of a person's name ("Hugo Bernard" → "Hugo"). */
217
+ export function firstName(fullName: string | null | undefined): string {
218
+ return (fullName ?? '').trim().split(/\s+/)[0] ?? ''
219
+ }
@@ -0,0 +1,66 @@
1
+ /** Undo / redo over serialized content snapshots. */
2
+ export interface EditorHistory {
3
+ past: string[]
4
+ future: string[]
5
+ present: string
6
+ lastChangeAt: number
7
+ }
8
+
9
+ /** Snapshots kept behind the present one. */
10
+ export const HISTORY_LIMIT = 50
11
+
12
+ /** Changes closer than this merge into one step (a word typed, a drag). */
13
+ export const HISTORY_COALESCE_MS = 700
14
+
15
+ export function createHistory(snapshot: string): EditorHistory {
16
+ return { past: [], future: [], present: snapshot, lastChangeAt: 0 }
17
+ }
18
+
19
+ /** No change merges into a step closed with this timestamp. */
20
+ const CLOSED_STEP = Number.NEGATIVE_INFINITY
21
+
22
+ /**
23
+ * Records the content after a change. A change within the coalescing window
24
+ * of the previous one extends its step instead of opening a new one; the
25
+ * oldest steps fall off past the limit. A discrete change (a deletion, a
26
+ * move) always is a step of its own, so one undo reverts exactly it.
27
+ */
28
+ export function recordChange(
29
+ history: EditorHistory,
30
+ snapshot: string,
31
+ now: number,
32
+ isDiscrete = false,
33
+ ): void {
34
+ if (snapshot === history.present) return
35
+ const isNewStep =
36
+ isDiscrete ||
37
+ now - history.lastChangeAt >= HISTORY_COALESCE_MS ||
38
+ history.past.length === 0
39
+ if (isNewStep) {
40
+ history.past.push(history.present)
41
+ if (history.past.length > HISTORY_LIMIT) history.past.shift()
42
+ }
43
+ history.present = snapshot
44
+ history.future = []
45
+ history.lastChangeAt = isDiscrete ? CLOSED_STEP : now
46
+ }
47
+
48
+ /** Steps back; the snapshot to restore, or `null` at the oldest one. */
49
+ export function undoChange(history: EditorHistory): string | null {
50
+ const previous = history.past.pop()
51
+ if (previous === undefined) return null
52
+ history.future.push(history.present)
53
+ history.present = previous
54
+ history.lastChangeAt = 0
55
+ return previous
56
+ }
57
+
58
+ /** Steps forward again; `null` when nothing was undone. */
59
+ export function redoChange(history: EditorHistory): string | null {
60
+ const next = history.future.pop()
61
+ if (next === undefined) return null
62
+ history.past.push(history.present)
63
+ history.present = next
64
+ history.lastChangeAt = 0
65
+ return next
66
+ }