@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,156 @@
1
+ import type { VariableDefinition, VariableType } from '../types/mailing'
2
+ import { deepClone } from './clone'
3
+ import { isPresent, readPath } from './paths'
4
+
5
+ /**
6
+ * How a value of a real send is entered: typed, ticked, or a list or an object
7
+ * taken as is from the test data (neither has a one-line form).
8
+ */
9
+ export type SendValueKind = 'text' | 'boolean' | 'list' | 'object'
10
+
11
+ /** One variable the real-send dialog asks a value for. */
12
+ export interface SendField {
13
+ path: string
14
+ required: boolean
15
+ kind: SendValueKind
16
+ }
17
+
18
+ /** The dialog's values, by variable path. */
19
+ export type SendValues = Record<string, unknown>
20
+
21
+ const PATH_SEPARATOR = '.'
22
+ const ARRAY_SUFFIX = '[]'
23
+ const RESERVED_SEGMENTS = ['__proto__', 'constructor', 'prototype']
24
+
25
+ const KIND_BY_TYPE: Partial<Record<VariableType, SendValueKind>> = {
26
+ boolean: 'boolean',
27
+ array: 'list',
28
+ }
29
+
30
+ function kindOfValue(value: unknown): SendValueKind {
31
+ if (typeof value === 'boolean') return 'boolean'
32
+ if (Array.isArray(value)) return 'list'
33
+ return value !== null && typeof value === 'object' ? 'object' : 'text'
34
+ }
35
+
36
+ function normalize(path: string): string {
37
+ const trimmed = path.trim()
38
+ return trimmed.endsWith(ARRAY_SUFFIX)
39
+ ? trimmed.slice(0, -ARRAY_SUFFIX.length)
40
+ : trimmed
41
+ }
42
+
43
+ /** Whether `path` holds `other` (`order` holds `order.number`). */
44
+ function isAncestor(path: string, other: string): boolean {
45
+ return other.startsWith(`${path}${PATH_SEPARATOR}`)
46
+ }
47
+
48
+ /**
49
+ * The variables a real send needs values for: the declared ones first, then
50
+ * the ones the content uses without declaring them, never required. An
51
+ * undeclared path whose children are asked for (`{{order}}` next to
52
+ * `{{order.number}}`) gets no field of its own: its children build it.
53
+ */
54
+ export function sendFields(
55
+ declared: VariableDefinition[],
56
+ detected: string[],
57
+ testData: Record<string, unknown>,
58
+ ): SendField[] {
59
+ const fields = declared.map((variable) => ({
60
+ path: normalize(variable.path),
61
+ required: variable.required,
62
+ kind:
63
+ KIND_BY_TYPE[variable.type] ??
64
+ kindOfValue(readPath(testData, variable.path)),
65
+ }))
66
+ const known = new Set(fields.map((field) => field.path))
67
+ const paths = [...new Set(detected.map(normalize))].filter(
68
+ (path) => path && !known.has(path),
69
+ )
70
+ const every = [...known, ...paths]
71
+ const undeclared = paths
72
+ .filter((path) => !every.some((other) => isAncestor(path, other)))
73
+ .map((path) => ({
74
+ path,
75
+ required: false,
76
+ kind: kindOfValue(readPath(testData, path)),
77
+ }))
78
+ return [...fields, ...undeclared]
79
+ }
80
+
81
+ const INITIAL_VALUES: Record<SendValueKind, (value: unknown) => unknown> = {
82
+ text: (value) => (isPresent(value) ? String(value) : ''),
83
+ boolean: (value) => Boolean(value),
84
+ list: (value) => (isPresent(value) ? value : undefined),
85
+ object: (value) => (isPresent(value) ? value : undefined),
86
+ }
87
+
88
+ /** The values the dialog opens with: the template's test data. */
89
+ export function initialSendValues(
90
+ fields: SendField[],
91
+ testData: Record<string, unknown>,
92
+ ): SendValues {
93
+ return Object.fromEntries(
94
+ fields.map((field) => [
95
+ field.path,
96
+ INITIAL_VALUES[field.kind](readPath(testData, field.path)),
97
+ ]),
98
+ )
99
+ }
100
+
101
+ const HAS_VALUE: Record<SendValueKind, (value: unknown) => boolean> = {
102
+ text: (value) => typeof value === 'string' && value.trim() !== '',
103
+ boolean: () => true,
104
+ list: isPresent,
105
+ object: isPresent,
106
+ }
107
+
108
+ /** The required variables still without a value: the send waits for them. */
109
+ export function missingRequiredPaths(
110
+ fields: SendField[],
111
+ values: SendValues,
112
+ ): string[] {
113
+ return fields
114
+ .filter((field) => field.required)
115
+ .filter((field) => !HAS_VALUE[field.kind](values[field.path]))
116
+ .map((field) => field.path)
117
+ }
118
+
119
+ function setPath(
120
+ target: Record<string, unknown>,
121
+ path: string,
122
+ value: unknown,
123
+ ): void {
124
+ const segments = path.split(PATH_SEPARATOR)
125
+ if (segments.some((segment) => RESERVED_SEGMENTS.includes(segment))) return
126
+ const last = segments.pop() as string
127
+ const parent = segments.reduce<Record<string, unknown>>((node, segment) => {
128
+ const child = node[segment]
129
+ if (child === null || typeof child !== 'object') node[segment] = {}
130
+ return node[segment] as Record<string, unknown>
131
+ }, target)
132
+ parent[last] = value
133
+ }
134
+
135
+ function depth(field: SendField): number {
136
+ return field.path.split(PATH_SEPARATOR).length
137
+ }
138
+
139
+ /**
140
+ * The `data` a real send carries: every value given, nested by its path. The
141
+ * shallow paths go first, copied, so a value typed for `order.number` always
142
+ * wins over the `order` object taken from the test data.
143
+ */
144
+ export function buildSendData(
145
+ fields: SendField[],
146
+ values: SendValues,
147
+ ): Record<string, unknown> {
148
+ const data: Record<string, unknown> = {}
149
+ fields
150
+ .filter((field) => HAS_VALUE[field.kind](values[field.path]))
151
+ .sort((left, right) => depth(left) - depth(right))
152
+ .forEach((field) =>
153
+ setPath(data, field.path, deepClone(values[field.path])),
154
+ )
155
+ return data
156
+ }
@@ -0,0 +1,113 @@
1
+ export type EditorShortcut =
2
+ | 'save'
3
+ | 'publish'
4
+ | 'test'
5
+ | 'search'
6
+ | 'delete'
7
+ | 'duplicate'
8
+ | 'escape'
9
+ | 'undo'
10
+ | 'redo'
11
+
12
+ /** The parts of a `KeyboardEvent` a shortcut is matched on. */
13
+ export interface KeyStroke {
14
+ key: string
15
+ metaKey: boolean
16
+ ctrlKey: boolean
17
+ shiftKey: boolean
18
+ altKey: boolean
19
+ }
20
+
21
+ interface ShortcutRule {
22
+ shortcut: EditorShortcut
23
+ keys: string[]
24
+ hasModifier: boolean
25
+ hasShift?: boolean
26
+ isAllowedWhileTyping: boolean
27
+ }
28
+
29
+ const rule = (
30
+ shortcut: EditorShortcut,
31
+ keys: string[],
32
+ hasModifier: boolean,
33
+ isAllowedWhileTyping: boolean,
34
+ hasShift?: boolean,
35
+ ): ShortcutRule => ({
36
+ shortcut,
37
+ keys,
38
+ hasModifier,
39
+ isAllowedWhileTyping,
40
+ hasShift,
41
+ })
42
+
43
+ const RULES: ShortcutRule[] = [
44
+ rule('save', ['s'], true, true),
45
+ rule('publish', ['enter'], true, true),
46
+ rule('duplicate', ['d'], true, false),
47
+ rule('redo', ['z'], true, false, true),
48
+ rule('redo', ['y'], true, false),
49
+ rule('undo', ['z'], true, false, false),
50
+ rule('test', ['t'], false, false),
51
+ rule('search', ['/'], false, false),
52
+ rule('delete', ['backspace', 'delete'], false, false),
53
+ rule('escape', ['escape'], false, false),
54
+ ]
55
+
56
+ const NON_TEXT_INPUT_TYPES = [
57
+ 'button',
58
+ 'checkbox',
59
+ 'radio',
60
+ 'range',
61
+ 'reset',
62
+ 'submit',
63
+ 'color',
64
+ 'file',
65
+ ]
66
+ const TEXT_TAGS = ['TEXTAREA', 'SELECT']
67
+ const INPUT_TAG = 'INPUT'
68
+ const EDITABLE_SELECTOR = '[contenteditable=""], [contenteditable="true"]'
69
+
70
+ interface TargetLike {
71
+ tagName?: string
72
+ type?: string
73
+ isContentEditable?: boolean
74
+ closest?: (selector: string) => unknown
75
+ }
76
+
77
+ /**
78
+ * Whether the keystroke lands in something the user types in: single-key
79
+ * shortcuts (T, /, ⌫) must leave those alone.
80
+ */
81
+ export function isTypingTarget(target: EventTarget | null): boolean {
82
+ const element = target as TargetLike | null
83
+ if (!element?.tagName) return false
84
+ if (TEXT_TAGS.includes(element.tagName)) return true
85
+ if (element.tagName === INPUT_TAG)
86
+ return !NON_TEXT_INPUT_TYPES.includes(element.type ?? '')
87
+ if (element.isContentEditable) return true
88
+ return Boolean(element.closest?.(EDITABLE_SELECTOR))
89
+ }
90
+
91
+ function matches(stroke: KeyStroke, candidate: ShortcutRule): boolean {
92
+ const hasModifier = stroke.metaKey || stroke.ctrlKey
93
+ if (stroke.altKey || hasModifier !== candidate.hasModifier) return false
94
+ if (
95
+ candidate.hasShift !== undefined &&
96
+ candidate.hasShift !== stroke.shiftKey
97
+ )
98
+ return false
99
+ return candidate.keys.includes(stroke.key.toLowerCase())
100
+ }
101
+
102
+ /** The editor shortcut a keystroke triggers, or `null`. */
103
+ export function matchShortcut(
104
+ stroke: KeyStroke,
105
+ isTyping: boolean,
106
+ ): EditorShortcut | null {
107
+ const found = RULES.find(
108
+ (candidate) =>
109
+ matches(stroke, candidate) &&
110
+ (!isTyping || candidate.isAllowedWhileTyping),
111
+ )
112
+ return found?.shortcut ?? null
113
+ }
@@ -0,0 +1,82 @@
1
+ /** Mirrors the backend `SLUG_PATTERN`: what `SendTemplate(slug)` accepts. */
2
+ export const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/
3
+
4
+ export const MAX_SLUG_LENGTH = 64
5
+
6
+ export const MAX_NAME_LENGTH = 120
7
+
8
+ const SLUG_SEPARATOR = '-'
9
+
10
+ const FIRST_FREE_SUFFIX = 2
11
+
12
+ export type SlugIssue = 'required' | 'format' | 'too_long' | 'taken'
13
+
14
+ export type NameIssue = 'required' | 'too_long'
15
+
16
+ /**
17
+ * The slug a name suggests: ASCII, lowercase, words joined by dashes, cut to
18
+ * the maximum length without a trailing dash.
19
+ */
20
+ export function slugify(name: string): string {
21
+ return name
22
+ .normalize('NFD')
23
+ .replace(/\p{Diacritic}/gu, '')
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, SLUG_SEPARATOR)
26
+ .replace(/^-+|-+$/g, '')
27
+ .slice(0, MAX_SLUG_LENGTH)
28
+ .replace(/-+$/, '')
29
+ }
30
+
31
+ /**
32
+ * What is wrong with a slug, or `null` when it can be created: present, in
33
+ * the backend format, short enough and not used by another template.
34
+ */
35
+ export function validateSlug(
36
+ slug: string,
37
+ takenSlugs: ReadonlySet<string>,
38
+ ): SlugIssue | null {
39
+ if (!slug) return 'required'
40
+ if (slug.length > MAX_SLUG_LENGTH) return 'too_long'
41
+ if (!SLUG_PATTERN.test(slug)) return 'format'
42
+ return takenSlugs.has(slug) ? 'taken' : null
43
+ }
44
+
45
+ /** What is wrong with a template name, or `null` when it is fine. */
46
+ export function validateName(name: string): NameIssue | null {
47
+ const trimmed = name.trim()
48
+ if (!trimmed) return 'required'
49
+ return trimmed.length > MAX_NAME_LENGTH ? 'too_long' : null
50
+ }
51
+
52
+ /** `base` with a `-N` suffix, the base cut so the whole fits the slug limit. */
53
+ function suffixedSlug(base: string, suffix: number): string {
54
+ const ending = `${SLUG_SEPARATOR}${suffix}`
55
+ const kept = base.slice(0, MAX_SLUG_LENGTH - ending.length).replace(/-+$/, '')
56
+ return `${kept}${ending}`
57
+ }
58
+
59
+ /**
60
+ * The first free slug from `base`: itself, else `base-2`, `base-3`… cut to
61
+ * the slug limit, so a duplicate of "welcome (copy)" never starts out refused.
62
+ */
63
+ export function freeSlug(
64
+ base: string,
65
+ takenSlugs: ReadonlySet<string>,
66
+ ): string {
67
+ if (!takenSlugs.has(base)) return base
68
+ let suffix = FIRST_FREE_SUFFIX
69
+ while (takenSlugs.has(suffixedSlug(base, suffix))) suffix += 1
70
+ return suffixedSlug(base, suffix)
71
+ }
72
+
73
+ /**
74
+ * A name wrapped by `wrap` ("{name} (copy)"), the inner name cut so the whole
75
+ * stays within the name limit.
76
+ */
77
+ export function fitName(name: string, wrap: (inner: string) => string): string {
78
+ const wrapped = wrap(name)
79
+ if (wrapped.length <= MAX_NAME_LENGTH) return wrapped
80
+ const room = Math.max(0, MAX_NAME_LENGTH - wrap('').length)
81
+ return wrap(name.slice(0, room).trimEnd())
82
+ }
@@ -0,0 +1,23 @@
1
+ import type { StarterSummary } from '../types/mailing'
2
+ import type { OptionalTranslate } from './categories'
3
+
4
+ const STARTER_NAME_KEY_PREFIX = 'dms_mailing.starters.names'
5
+
6
+ /** The i18n key naming a shipped starter ("Order confirmation"). */
7
+ export function starterNameKey(id: string): string {
8
+ return `${STARTER_NAME_KEY_PREFIX}.${id}`
9
+ }
10
+
11
+ /**
12
+ * The starters named in the interface's language; one the catalog does not
13
+ * know keeps the name the backend gives it.
14
+ */
15
+ export function localizeStarters(
16
+ starters: StarterSummary[],
17
+ translate: OptionalTranslate,
18
+ ): StarterSummary[] {
19
+ return starters.map((starter) => ({
20
+ ...starter,
21
+ name: translate(starterNameKey(starter.id)) ?? starter.name,
22
+ }))
23
+ }
@@ -0,0 +1,112 @@
1
+ import type { SuppressionReason, SuppressionSummary } from '../types/mailing'
2
+ import { SUPPRESSIONS_PATH } from './pages'
3
+
4
+ /** The status of a send nothing was sent for: its address is suppressed. */
5
+ export const SUPPRESSED_STATUS = 'suppressed'
6
+
7
+ const EMAIL_QUERY_KEY = 'email'
8
+ const KEY_PREFIX = 'dms_mailing.suppressions.'
9
+
10
+ export type { SuppressionReason, SuppressionSummary }
11
+
12
+ /** A banner of the send drawer about the recipient's suppression. */
13
+ export interface SuppressionNotice {
14
+ tone: 'warning' | 'info' | 'success'
15
+ icon: string
16
+ /** i18n keys, interpolated with `params`. */
17
+ titleKey: string
18
+ descriptionKey: string
19
+ params: Record<string, string>
20
+ /** Whether the banner links to the address on the suppression list. */
21
+ hasListLink: boolean
22
+ }
23
+
24
+ /** What the drawer reads of a send to tell its suppression. */
25
+ export interface SuppressionSend {
26
+ status: string
27
+ recipientEmail: string
28
+ }
29
+
30
+ /** How the drawer words a suppression's date and reason. */
31
+ export interface SuppressionWording {
32
+ date: (iso: string) => string
33
+ reason: (reason: SuppressionReason) => string
34
+ }
35
+
36
+ export interface SuppressionRoute {
37
+ path: string
38
+ query: Record<string, string>
39
+ }
40
+
41
+ export function isSuppressedStatus(status: string | undefined): boolean {
42
+ return status === SUPPRESSED_STATUS
43
+ }
44
+
45
+ /** The i18n key naming a suppression reason ("Hard bounce"). */
46
+ export function reasonKey(reason: SuppressionReason): string {
47
+ return `${KEY_PREFIX}reasons.${reason}`
48
+ }
49
+
50
+ /** The prefix of the error a suppressed send keeps, one key per reason. */
51
+ const SUPPRESSED_ERROR_PREFIX = '$dms_mailing.errors.suppressed.'
52
+
53
+ /**
54
+ * The reason a suppressed send's error names ("manual" out of
55
+ * `$dms_mailing.errors.suppressed.manual`); `null` for any other error.
56
+ */
57
+ export function suppressedReasonOf(
58
+ error: string | undefined,
59
+ ): SuppressionReason | null {
60
+ if (!error?.startsWith(SUPPRESSED_ERROR_PREFIX)) return null
61
+ return error.slice(SUPPRESSED_ERROR_PREFIX.length) as SuppressionReason
62
+ }
63
+
64
+ /**
65
+ * The suppression list, narrowed to one address. The list filters on the
66
+ * stored address exactly, and addresses are stored trimmed and lowercased.
67
+ */
68
+ export function suppressionRoute(email: string): SuppressionRoute {
69
+ return {
70
+ path: SUPPRESSIONS_PATH,
71
+ query: { [EMAIL_QUERY_KEY]: email.trim().toLowerCase() },
72
+ }
73
+ }
74
+
75
+ const notice = (
76
+ key: string,
77
+ tone: SuppressionNotice['tone'],
78
+ hasListLink: boolean,
79
+ params: Record<string, string>,
80
+ ): SuppressionNotice => ({
81
+ tone,
82
+ icon: 'i-ph-prohibit',
83
+ titleKey: `${KEY_PREFIX}notice.${key}.title`,
84
+ descriptionKey: `${KEY_PREFIX}notice.${key}.description`,
85
+ params,
86
+ hasListLink,
87
+ })
88
+
89
+ /**
90
+ * What the drawer says about the recipient's suppression: a send that was
91
+ * not sent (and whether the address is still suppressed), or an address that
92
+ * receives nothing any more since this send. `null` when there is nothing to
93
+ * say.
94
+ */
95
+ export function suppressionNotice(
96
+ send: SuppressionSend,
97
+ suppression: SuppressionSummary | null | undefined,
98
+ wording: SuppressionWording,
99
+ ): SuppressionNotice | null {
100
+ const params = suppression
101
+ ? {
102
+ date: wording.date(suppression.createdAt),
103
+ reason: wording.reason(suppression.reason),
104
+ email: suppression.email,
105
+ }
106
+ : { date: '', reason: '', email: send.recipientEmail }
107
+ if (isSuppressedStatus(send.status))
108
+ return suppression
109
+ ? notice('not_sent', 'warning', true, params)
110
+ : notice('reactivated', 'success', false, params)
111
+ return suppression ? notice('on_list', 'info', true, params) : null
112
+ }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ Block,
3
+ FooterBlock,
4
+ LocaleContent,
5
+ TemplateContent,
6
+ VariableDefinition,
7
+ } from '../types/mailing'
8
+ import type { EditorCheck } from './editor-checks'
9
+ import {
10
+ PREFERENCES_URL_PATH,
11
+ SYSTEM_PATHS,
12
+ UNSUBSCRIBE_URL_PATH,
13
+ pathsInBlocks,
14
+ textPaths,
15
+ } from './paths'
16
+
17
+ /** The category whose e-mails are campaigns; the settings never remove it. */
18
+ export const MARKETING_CATEGORY = 'marketing'
19
+
20
+ type FooterLabelKey = 'unsubscribeLabel' | 'preferencesLabel'
21
+
22
+ /**
23
+ * Each footer link: the label that shows it and the URL the caller passes.
24
+ * The footer links are the system variables of a template's contract, as in
25
+ * the server's `src/engine/system-variables.ts`, which the layer cannot
26
+ * import: a link shows when its label is filled.
27
+ */
28
+ export const FOOTER_LINK_PATHS: Record<FooterLabelKey, string> = {
29
+ unsubscribeLabel: UNSUBSCRIBE_URL_PATH,
30
+ preferencesLabel: PREFERENCES_URL_PATH,
31
+ }
32
+
33
+ const LABEL_KEYS = Object.keys(FOOTER_LINK_PATHS) as FooterLabelKey[]
34
+ const SYSTEM_VARIABLE_TYPE = 'url'
35
+ const CHECK_KEY_PREFIX = 'dms_mailing.editor.checks'
36
+
37
+ /** A system variable: declared by the content itself, never by hand. */
38
+ export interface SystemVariable extends VariableDefinition {
39
+ system: true
40
+ }
41
+
42
+ export function isMarketingCategory(category?: string | null): boolean {
43
+ return category === MARKETING_CATEGORY
44
+ }
45
+
46
+ /**
47
+ * Whether a new footer of this category carries the unsubscribe and
48
+ * preferences links: a transactional e-mail is not subscribed to, so only an
49
+ * uncategorised or marketing one does.
50
+ */
51
+ export function hasFooterLinksByDefault(category?: string | null): boolean {
52
+ return !category || isMarketingCategory(category)
53
+ }
54
+
55
+ const isFooter = (block: Block): block is FooterBlock => block.type === 'footer'
56
+
57
+ /** The URL paths a footer shows a link for: the ones with a label. */
58
+ export function footerLinkPaths(block: FooterBlock): string[] {
59
+ return LABEL_KEYS.filter((key) => block[key].trim()).map(
60
+ (key) => FOOTER_LINK_PATHS[key],
61
+ )
62
+ }
63
+
64
+ function footersIn(blocks: Block[]): FooterBlock[] {
65
+ return blocks.flatMap((block) => {
66
+ if (isFooter(block)) return [block]
67
+ if (block.type !== 'if') return []
68
+ return footersIn([...block.children, ...(block.elseChildren ?? [])])
69
+ })
70
+ }
71
+
72
+ function localeSystemPaths(locale: LocaleContent): string[] {
73
+ const linked = footersIn(locale.blocks).flatMap(footerLinkPaths)
74
+ const referenced = [
75
+ ...textPaths(locale.subject),
76
+ ...textPaths(locale.preheader),
77
+ ...pathsInBlocks(locale.blocks),
78
+ ]
79
+ return [...linked, ...referenced]
80
+ }
81
+
82
+ /** The system paths any locale of the content uses, in a stable order. */
83
+ export function contentSystemPaths(
84
+ content: Pick<TemplateContent, 'locales'>,
85
+ ): string[] {
86
+ const used = new Set(
87
+ Object.values(content.locales ?? {}).flatMap(localeSystemPaths),
88
+ )
89
+ return SYSTEM_PATHS.filter((path) => used.has(path))
90
+ }
91
+
92
+ /**
93
+ * The URLs a template's caller passes for the links its content shows, as
94
+ * declarations: type url, required when the template is marketing.
95
+ */
96
+ export function systemVariables(
97
+ content: Pick<TemplateContent, 'locales'>,
98
+ category?: string | null,
99
+ ): SystemVariable[] {
100
+ const required = isMarketingCategory(category)
101
+ return contentSystemPaths(content).map((path) => ({
102
+ path,
103
+ type: SYSTEM_VARIABLE_TYPE,
104
+ required,
105
+ system: true,
106
+ }))
107
+ }
108
+
109
+ const hasUnsubscribeLink = (locale: LocaleContent): boolean =>
110
+ localeSystemPaths(locale).includes(UNSUBSCRIBE_URL_PATH)
111
+
112
+ const check = (
113
+ id: string,
114
+ key: string,
115
+ params: Record<string, string | number>,
116
+ ): EditorCheck => ({
117
+ id,
118
+ state: 'warn',
119
+ key: `${CHECK_KEY_PREFIX}.${key}`,
120
+ params,
121
+ })
122
+
123
+ function unlinkedMarketingLocales(
124
+ content: Pick<TemplateContent, 'locales'>,
125
+ category?: string | null,
126
+ ): EditorCheck[] {
127
+ if (!isMarketingCategory(category)) return []
128
+ return Object.entries(content.locales ?? {})
129
+ .filter(([, locale]) => !hasUnsubscribeLink(locale))
130
+ .map(([code]) =>
131
+ check(`unsubscribe-${code}`, 'marketing_no_unsubscribe', {
132
+ locale: code.toUpperCase(),
133
+ }),
134
+ )
135
+ }
136
+
137
+ /**
138
+ * What the publish review warns about the footer links: a marketing locale
139
+ * without an unsubscribe link, and a required system URL the test data does
140
+ * not carry — a send without it is refused or goes out without the link.
141
+ */
142
+ export function systemVariableChecks(
143
+ content: Pick<TemplateContent, 'locales'>,
144
+ category: string | null | undefined,
145
+ data: Record<string, unknown>,
146
+ ): EditorCheck[] {
147
+ const missing = systemVariables(content, category)
148
+ .filter((variable) => variable.required)
149
+ .filter((variable) => !hasSystemUrl(data, variable.path))
150
+ .map((variable) =>
151
+ check(`system-${variable.path}`, 'system_url_missing', {
152
+ path: variable.path,
153
+ }),
154
+ )
155
+ return [...unlinkedMarketingLocales(content, category), ...missing]
156
+ }
157
+
158
+ /** Whether the data a canvas previews carries a usable URL for `path`. */
159
+ export function hasSystemUrl(
160
+ data: Record<string, unknown> | null | undefined,
161
+ path: string,
162
+ ): boolean {
163
+ const value = data?.[path]
164
+ return typeof value === 'string' && value.trim() !== ''
165
+ }