@antelopejs/dms-mailing 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/constants.d.ts +1 -0
  2. package/dist/constants.js +2 -1
  3. package/dist/constants.js.map +1 -1
  4. package/dist/data/templates-table.d.ts +4 -1
  5. package/dist/data/templates-table.js +55 -13
  6. package/dist/data/templates-table.js.map +1 -1
  7. package/dist/data-types/categories-type.d.ts +5 -5
  8. package/dist/db/models/templates.model.d.ts +10 -1
  9. package/dist/db/models/templates.model.js +27 -1
  10. package/dist/db/models/templates.model.js.map +1 -1
  11. package/dist/db/tables/templates.table.d.ts +7 -1
  12. package/dist/db/tables/templates.table.js +3 -1
  13. package/dist/db/tables/templates.table.js.map +1 -1
  14. package/dist/engine/formats.d.ts +13 -0
  15. package/dist/engine/formats.js +85 -0
  16. package/dist/engine/formats.js.map +1 -0
  17. package/dist/engine/index.d.ts +1 -0
  18. package/dist/engine/index.js +1 -0
  19. package/dist/engine/index.js.map +1 -1
  20. package/dist/engine/resolve.d.ts +2 -1
  21. package/dist/engine/resolve.js +14 -7
  22. package/dist/engine/resolve.js.map +1 -1
  23. package/dist/engine/tokens.d.ts +2 -1
  24. package/dist/engine/tokens.js +3 -5
  25. package/dist/engine/tokens.js.map +1 -1
  26. package/dist/frontend-module.d.ts +2 -0
  27. package/dist/frontend-module.js +24 -0
  28. package/dist/frontend-module.js.map +1 -0
  29. package/dist/hooks/tenant-export.js +4 -1
  30. package/dist/hooks/tenant-export.js.map +1 -1
  31. package/dist/implementations/dms-mailing/index.d.ts +1 -0
  32. package/dist/implementations/dms-mailing/index.js +3 -1
  33. package/dist/implementations/dms-mailing/index.js.map +1 -1
  34. package/dist/index.js +2 -19
  35. package/dist/index.js.map +1 -1
  36. package/dist/pages/overview.d.ts +1 -1
  37. package/dist/pages/overview.js +11 -13
  38. package/dist/pages/overview.js.map +1 -1
  39. package/dist/pages/sends.d.ts +1 -1
  40. package/dist/pages/sends.js +16 -8
  41. package/dist/pages/sends.js.map +1 -1
  42. package/dist/pages/templates.js +3 -8
  43. package/dist/pages/templates.js.map +1 -1
  44. package/dist/pages/toolbar.d.ts +12 -0
  45. package/dist/pages/toolbar.js +41 -0
  46. package/dist/pages/toolbar.js.map +1 -0
  47. package/dist/realtime.d.ts +2 -2
  48. package/dist/realtime.js +8 -8
  49. package/dist/realtime.js.map +1 -1
  50. package/dist/routes/metrics.d.ts +9 -2
  51. package/dist/routes/metrics.js +90 -35
  52. package/dist/routes/metrics.js.map +1 -1
  53. package/dist/routes/period.d.ts +1 -0
  54. package/dist/routes/period.js +19 -0
  55. package/dist/routes/period.js.map +1 -1
  56. package/dist/routes/sends.js +6 -3
  57. package/dist/routes/sends.js.map +1 -1
  58. package/dist/routes/settings.js +6 -7
  59. package/dist/routes/settings.js.map +1 -1
  60. package/dist/routes/templates.d.ts +8 -4
  61. package/dist/routes/templates.js +40 -15
  62. package/dist/routes/templates.js.map +1 -1
  63. package/dist/services/categories.d.ts +2 -1
  64. package/dist/services/categories.js +9 -0
  65. package/dist/services/categories.js.map +1 -1
  66. package/dist/services/declarations.js +2 -2
  67. package/dist/services/declarations.js.map +1 -1
  68. package/dist/services/events.js +1 -1
  69. package/dist/services/events.js.map +1 -1
  70. package/dist/services/localized-text.d.ts +7 -0
  71. package/dist/services/localized-text.js +28 -0
  72. package/dist/services/localized-text.js.map +1 -0
  73. package/dist/services/send-stat-items.js +34 -12
  74. package/dist/services/send-stat-items.js.map +1 -1
  75. package/dist/services/send-stats.d.ts +7 -0
  76. package/dist/services/send-stats.js +17 -5
  77. package/dist/services/send-stats.js.map +1 -1
  78. package/dist/services/send.d.ts +2 -0
  79. package/dist/services/send.js +11 -12
  80. package/dist/services/send.js.map +1 -1
  81. package/dist/services/sender-declaration.d.ts +2 -0
  82. package/dist/services/sender-declaration.js +18 -0
  83. package/dist/services/sender-declaration.js.map +1 -0
  84. package/dist/services/starters.d.ts +2 -2
  85. package/dist/services/starters.js +1 -10
  86. package/dist/services/starters.js.map +1 -1
  87. package/dist/services/templates.d.ts +11 -5
  88. package/dist/services/templates.js +24 -6
  89. package/dist/services/templates.js.map +1 -1
  90. package/dist/services/webhook-status.d.ts +2 -1
  91. package/dist/services/webhook-status.js +6 -5
  92. package/dist/services/webhook-status.js.map +1 -1
  93. package/dist/types/settings.d.ts +6 -0
  94. package/dist/types/settings.js +25 -5
  95. package/dist/types/settings.js.map +1 -1
  96. package/dist/validation/declarations.schema.d.ts +25 -7
  97. package/dist/validation/declarations.schema.js +16 -3
  98. package/dist/validation/declarations.schema.js.map +1 -1
  99. package/dist/validation/settings.schema.d.ts +25 -25
  100. package/dist/validation/settings.schema.js +30 -6
  101. package/dist/validation/settings.schema.js.map +1 -1
  102. package/dist/validation/templates.schema.d.ts +13 -0
  103. package/dist/validation/templates.schema.js +6 -1
  104. package/dist/validation/templates.schema.js.map +1 -1
  105. package/frontend-vue/app/components/CategoriesInput.vue +15 -13
  106. package/frontend-vue/app/components/FunnelCard.vue +46 -3
  107. package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
  108. package/frontend-vue/app/components/OverviewGate.vue +0 -8
  109. package/frontend-vue/app/components/ProviderPill.vue +50 -0
  110. package/frontend-vue/app/components/SendDrawer.vue +206 -183
  111. package/frontend-vue/app/components/StarterGallery.vue +1 -1
  112. package/frontend-vue/app/components/TemplateCard.vue +4 -1
  113. package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
  114. package/frontend-vue/app/components/TemplateGallery.vue +57 -17
  115. package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
  116. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
  117. package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
  118. package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
  119. package/frontend-vue/app/composables/useMailingApi.ts +3 -3
  120. package/frontend-vue/app/composables/useMailingReads.ts +2 -2
  121. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
  122. package/frontend-vue/app/types/mailing.ts +18 -0
  123. package/frontend-vue/app/utils/categories.ts +58 -49
  124. package/frontend-vue/app/utils/funnel.ts +25 -0
  125. package/frontend-vue/app/utils/period.ts +0 -7
  126. package/frontend-vue/app/utils/starters.ts +3 -1
  127. package/frontend-vue/app/utils/tokens.ts +6 -1
  128. package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
  129. package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
  130. package/frontend-vue/package.json +1 -1
  131. package/frontend-vue/tests/categories-input.test.ts +4 -10
  132. package/frontend-vue/tests/categories.test.ts +55 -17
  133. package/frontend-vue/tests/conditions.test.ts +1 -4
  134. package/frontend-vue/tests/funnel-card.test.ts +27 -0
  135. package/frontend-vue/tests/funnel.test.ts +25 -0
  136. package/frontend-vue/tests/localized-defaults.test.ts +0 -29
  137. package/frontend-vue/tests/new-template-modal.test.ts +1 -1
  138. package/frontend-vue/tests/overview-gate.test.ts +1 -24
  139. package/frontend-vue/tests/page-reads.test.ts +9 -25
  140. package/frontend-vue/tests/period.test.ts +0 -12
  141. package/frontend-vue/tests/replay-toast.test.ts +2 -1
  142. package/frontend-vue/tests/send-drawer.test.ts +123 -0
  143. package/frontend-vue/tests/template-drawer.test.ts +1 -1
  144. package/frontend-vue/tests/template-gallery.test.ts +90 -3
  145. package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
  146. package/package.json +6 -5
  147. package/skills/dms-mailing/SKILL.md +17 -4
  148. package/src/constants.ts +2 -0
  149. package/src/data/sends-table.ts +1 -1
  150. package/src/data/templates-table.ts +123 -19
  151. package/src/db/models/templates.model.ts +61 -3
  152. package/src/db/tables/templates.table.ts +10 -1
  153. package/src/engine/formats.ts +124 -0
  154. package/src/engine/index.ts +1 -0
  155. package/src/engine/resolve.ts +18 -7
  156. package/src/engine/tokens.ts +7 -5
  157. package/src/frontend-module.ts +36 -0
  158. package/src/hooks/tenant-export.ts +8 -1
  159. package/src/implementations/dms-mailing/index.ts +2 -0
  160. package/src/index.ts +2 -23
  161. package/src/pages/overview.ts +20 -16
  162. package/src/pages/sends.ts +27 -11
  163. package/src/pages/templates.ts +4 -8
  164. package/src/pages/toolbar.ts +77 -0
  165. package/src/realtime.ts +17 -8
  166. package/src/routes/metrics.ts +158 -45
  167. package/src/routes/period.ts +32 -0
  168. package/src/routes/sends.ts +13 -4
  169. package/src/routes/settings.ts +11 -8
  170. package/src/routes/templates.ts +72 -26
  171. package/src/services/categories.ts +14 -1
  172. package/src/services/declarations.ts +4 -3
  173. package/src/services/events.ts +1 -1
  174. package/src/services/localized-text.ts +53 -0
  175. package/src/services/send-stat-items.ts +60 -13
  176. package/src/services/send-stats.ts +37 -5
  177. package/src/services/send.ts +19 -20
  178. package/src/services/sender-declaration.ts +27 -0
  179. package/src/services/starters.ts +3 -15
  180. package/src/services/templates.ts +50 -19
  181. package/src/services/webhook-status.ts +3 -3
  182. package/src/types/settings.ts +35 -5
  183. package/src/validation/declarations.schema.ts +29 -3
  184. package/src/validation/settings.schema.ts +56 -14
  185. package/src/validation/templates.schema.ts +10 -0
  186. package/frontend-vue/app/components/PageHeader.vue +0 -94
  187. package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
@@ -4,6 +4,7 @@ import {
4
4
  declareTemplateCategory,
5
5
  } from "../../services/declarations";
6
6
  import { importSends } from "../../services/send-import";
7
+ import { declareSender } from "../../services/sender-declaration";
7
8
 
8
9
  export { recordEmailEvent as RecordEmailEvent } from "../../services/events";
9
10
  export { sendUnlessSuppressed as SendTemplate } from "../../services/checked-send";
@@ -16,4 +17,5 @@ export const DeclareTemplate = afterDatabaseReady(declareTemplate);
16
17
  export const DeclareTemplateCategory = afterDatabaseReady(
17
18
  declareTemplateCategory,
18
19
  );
20
+ export const DeclareSender = afterDatabaseReady(declareSender);
19
21
  export const ImportSends = afterDatabaseReady(importSends);
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import "./permissions";
2
2
  import "./db";
3
- import path from "node:path";
4
3
  import { ImplementInterface } from "@antelopejs/interface-core";
5
4
  import { AddFrontendModule } from "@antelopejs/interface-dms/page";
6
5
  import {
@@ -8,7 +7,6 @@ import {
8
7
  unregisterAutomationNodes,
9
8
  } from "./automation";
10
9
  import { type DmsMailingConfig, setConfig } from "./config";
11
- import { API_BASE_PATH, FRONTEND_MODULE_NAME } from "./constants";
12
10
  import {
13
11
  ATTENTION_JOB,
14
12
  RETENTION_JOB,
@@ -22,7 +20,7 @@ import {
22
20
  unregisterTenantExport,
23
21
  } from "./hooks";
24
22
  import { registerRealtimeTopics } from "./realtime";
25
- import { WEBHOOK_SECRET_HEADER } from "./services/webhook-status";
23
+ import { frontendModule } from "./frontend-module";
26
24
 
27
25
  export * from "./data";
28
26
  export * from "./data-types";
@@ -33,18 +31,6 @@ export type { DmsMailingConfig } from "./config";
33
31
 
34
32
  const CRON_JOBS = [RETENTION_JOB, ATTENTION_JOB];
35
33
 
36
- /**
37
- * The provider webhook (`/events/:provider/:tenantId`), served through the
38
- * dashboard's public URL: the front proxy lets it through without an
39
- * `Origin`, never sends it a session and relays the secret header, which the
40
- * route checks itself.
41
- */
42
- const PROVIDER_WEBHOOK_ENDPOINT = {
43
- path: `${API_BASE_PATH}/events/*/*`,
44
- methods: ["POST"],
45
- headers: [WEBHOOK_SECRET_HEADER],
46
- };
47
-
48
34
  let scheduledJobs: ScheduledJob[] = [];
49
35
 
50
36
  export async function construct(config: DmsMailingConfig = {}): Promise<void> {
@@ -57,14 +43,7 @@ export async function construct(config: DmsMailingConfig = {}): Promise<void> {
57
43
  registerAutomationNodes();
58
44
  registerTenantExport();
59
45
  registerRealtimeTopics();
60
- await AddFrontendModule({
61
- name: FRONTEND_MODULE_NAME,
62
- sourcePath: path.join(__dirname, "../frontend-vue"),
63
- renderer: { name: "vue", version: "3" },
64
- options: { dmsMailing: {} },
65
- priority: 0,
66
- publicEndpoints: [PROVIDER_WEBHOOK_ENDPOINT],
67
- });
46
+ await AddFrontendModule(frontendModule());
68
47
  }
69
48
 
70
49
  export function destroy(): void {
@@ -23,6 +23,7 @@ import {
23
23
  } from "../constants";
24
24
  import { BUSINESS_AUDIENCE, SEND_AUDIENCE_QUERY_KEY } from "../types";
25
25
  import { mailingNavCategory } from "./module";
26
+ import { MAILING_LIVE, mailingToolbar } from "./toolbar";
26
27
 
27
28
  const OVERVIEW_ORDER = 0;
28
29
  const GRID_GAP = "1rem";
@@ -30,6 +31,7 @@ const CHART_HEIGHT = "260px";
30
31
  const WIDE_SPAN = 2;
31
32
  const ATTENTION_SKELETON_ROWS = 3;
32
33
 
34
+ /** A KPI card of the Overview, refreshed on each send while the page is live. */
33
35
  const kpi = (
34
36
  id: string,
35
37
  icon: string,
@@ -40,6 +42,7 @@ const kpi = (
40
42
  variant: "stat",
41
43
  fetchUrl: `${API_BASE_PATH}/metrics/kpi/${id}?${SEND_AUDIENCE_QUERY_KEY}=${BUSINESS_AUDIENCE}`,
42
44
  periodScope: OVERVIEW_PERIOD_SCOPE,
45
+ realtimeTopic: MAILING_SENDS_TOPIC,
43
46
  valueFormat: "compact",
44
47
  showSparkline: true,
45
48
  ...extra,
@@ -104,9 +107,11 @@ const activity = Grid({ gap: GRID_GAP }).child(
104
107
  * `name` carried by each series goes to ApexCharts verbatim.
105
108
  */
106
109
  primaryLabel: "$dms_mailing.metrics.sends",
107
- comparisonLabel: "$dms_mailing.metrics.previous",
110
+ /** Neutral: the toolbar compares with the previous period or year. */
111
+ comparisonLabel: "$dms_mailing.metrics.compared",
108
112
  fetchUrl: `${API_BASE_PATH}/metrics/volume`,
109
113
  periodScope: OVERVIEW_PERIOD_SCOPE,
114
+ realtimeTopic: MAILING_SENDS_TOPIC,
110
115
  chart: ChartArea({
111
116
  height: CHART_HEIGHT,
112
117
  xaxisType: "datetime",
@@ -157,11 +162,16 @@ const breakdown = Grid({ gap: GRID_GAP }).child(
157
162
  title: "$dms_mailing.metrics.ranking",
158
163
  fetchUrl: `${API_BASE_PATH}/metrics/top-templates?metric=open-rate`,
159
164
  periodScope: OVERVIEW_PERIOD_SCOPE,
165
+ realtimeTopic: MAILING_SENDS_TOPIC,
160
166
  valueFormat: "percent",
161
167
  showRank: true,
162
168
  highlightTopN: 2,
163
169
  showBar: true,
164
- showDelta: false,
170
+ /**
171
+ * Each row's change against the window the toolbar compares with;
172
+ * "No comparison" leaves the rows without one.
173
+ */
174
+ showDelta: true,
165
175
  skeletonCount: 5,
166
176
  emptyLabel: "$dms_mailing.metrics.ranking_empty",
167
177
  }),
@@ -173,9 +183,10 @@ const breakdown = Grid({ gap: GRID_GAP }).child(
173
183
  description: "$dms_mailing.metrics.domain_sends",
174
184
  fetchUrl: `${API_BASE_PATH}/metrics/domains`,
175
185
  periodScope: OVERVIEW_PERIOD_SCOPE,
186
+ realtimeTopic: MAILING_SENDS_TOPIC,
176
187
  valueFormat: "percent",
177
188
  showRank: false,
178
- showDelta: false,
189
+ showDelta: true,
179
190
  skeletonCount: 5,
180
191
  emptyLabel: "$dms_mailing.metrics.ranking_empty",
181
192
  }),
@@ -192,6 +203,7 @@ export class OverviewPageController extends PageController(
192
203
  module: MODULE_ID,
193
204
  category: mailingNavCategory,
194
205
  order: OVERVIEW_ORDER,
206
+ live: MAILING_LIVE,
195
207
  },
196
208
  DefaultLayout({
197
209
  fullWidth: true,
@@ -206,19 +218,11 @@ export class OverviewPageController extends PageController(
206
218
  ],
207
219
  }),
208
220
  ) {
209
- static header = CustomComponent("MailingPageHeader")
210
- .options({
211
- periodScope: OVERVIEW_PERIOD_SCOPE,
212
- presets: ["last-7-days", "last-30-days", "last-90-days"],
213
- defaultPreset: "last-30-days",
214
- defaultComparison: "previous-period",
215
- showProvider: true,
216
- })
217
- .meta({
218
- name: "$dms_mailing.permissions.page_header.name",
219
- description: "$dms_mailing.permissions.page_header.description",
220
- icon: "i-ph-plugs-connected",
221
- });
221
+ static header = mailingToolbar({
222
+ scope: OVERVIEW_PERIOD_SCOPE,
223
+ presets: ["last-7-days", "last-30-days", "last-90-days"],
224
+ defaultPreset: "last-30-days",
225
+ });
222
226
 
223
227
  static dashboard = CustomComponent("MailingOverviewGate")
224
228
  .meta({
@@ -23,6 +23,7 @@ import { SendModel } from "../db";
23
23
  import { SendsTableAPI } from "../data/sends-table";
24
24
  import { RESOLVED_STAGE, type SendStage } from "../types";
25
25
  import { mailingNavCategory } from "./module";
26
+ import { MAILING_LIVE, NEW_SENDS_LABEL, mailingToolbar } from "./toolbar";
26
27
 
27
28
  const SENDS_ORDER = 3;
28
29
  const DAY_MS = 86_400_000;
@@ -187,18 +188,15 @@ export class SendsPageController extends PageController(
187
188
  module: MODULE_ID,
188
189
  category: mailingNavCategory,
189
190
  order: SENDS_ORDER,
191
+ live: MAILING_LIVE,
190
192
  },
191
193
  DefaultLayout({ fullWidth: true }),
192
194
  ) {
193
- static header = CustomComponent("MailingPageHeader")
194
- .options({
195
- periodScope: SENDS_PERIOD_SCOPE,
196
- presets: ["last-24h", "last-7-days", "last-30-days"],
197
- defaultPreset: "last-24h",
198
- defaultComparison: "previous-period",
199
- showProvider: true,
200
- })
201
- .meta(permissionMeta("page_header", "i-ph-plugs-connected"));
195
+ static header = mailingToolbar({
196
+ scope: SENDS_PERIOD_SCOPE,
197
+ presets: ["last-24h", "last-7-days", "last-30-days"],
198
+ defaultPreset: "last-24h",
199
+ });
202
200
 
203
201
  static health = Banner({
204
202
  fetchUrl: `${API_BASE_PATH}/sends/banner`,
@@ -226,6 +224,16 @@ export class SendsPageController extends PageController(
226
224
  searchPlaceholder: "$dms_mailing.sends.search",
227
225
  pageSize: 50,
228
226
  rowClick: "single",
227
+ /**
228
+ * New sends wait in a "N new sends" strip while the user reads a send,
229
+ * scrolls down the log or searches it, so the rows do not move under them.
230
+ */
231
+ liveHold: {
232
+ whileRowOpen: true,
233
+ whileScrolled: true,
234
+ whileSearching: true,
235
+ missedLabel: NEW_SENDS_LABEL,
236
+ },
229
237
  views: {
230
238
  layout: "tabs",
231
239
  items: [
@@ -294,16 +302,24 @@ export class SendsPageController extends PageController(
294
302
  /**
295
303
  * A send is an append-only record: it cannot be edited, so the
296
304
  * built-in `edit`/`details` row click has nothing to open. This
297
- * action takes the row click and opens the send in the side panel.
305
+ * action takes the row click and opens the send in a drawer on the
306
+ * right, at the DMS's default width (the user's resize is kept).
298
307
  */
299
308
  isDefault: true,
300
309
  target: {
301
- type: "sidePanel",
310
+ type: "drawer",
311
+ direction: "right",
302
312
  component: CustomComponent("MailingSendDrawer").meta(
303
313
  permissionMeta("send_drawer", "i-ph-info"),
304
314
  ),
305
315
  title: "$dms_mailing.sends.actions.details",
316
+ // Names the open send: its recipient and the template it used.
317
+ rowTitle: "$dms_mailing.sends.drawer.heading",
306
318
  positionLabel: "$dms_mailing.sends.drawer.position",
319
+ // A side panel keeps the open send in the URL and words a missing
320
+ // one by default; a drawer does both only when asked.
321
+ deepLink: true,
322
+ notFound: {},
307
323
  },
308
324
  },
309
325
  {
@@ -20,6 +20,7 @@ import {
20
20
  assertEditable,
21
21
  TemplatesTableAPI,
22
22
  } from "../data/templates-table";
23
+ import { localeOfRequest } from "../services/localized-text";
23
24
  import { mailingNavCategory } from "./module";
24
25
 
25
26
  const TEMPLATES_ORDER = 1;
@@ -95,13 +96,6 @@ export class TemplatesPageController extends PageController(
95
96
  filter: { accessorKey: "needsAttention", value: "true", mode: "is" },
96
97
  },
97
98
  ],
98
- quickFilters: [
99
- {
100
- field: "category",
101
- label: "$dms_mailing.templates.filters.category",
102
- icon: "i-ph-tag",
103
- },
104
- ],
105
99
  footer: {
106
100
  countLabel: "$dms_mailing.templates.footer.count",
107
101
  hint: "$dms_mailing.templates.footer.hint",
@@ -115,7 +109,7 @@ export class TemplatesPageController extends PageController(
115
109
  },
116
110
  async edit(ctx, { id, body, current }) {
117
111
  await assertEditable(getRequestTenantId(ctx), body, current);
118
- return afterEdit(this.model, id, body.name);
112
+ return afterEdit(this.model, id, body.name, localeOfRequest(ctx));
119
113
  },
120
114
  },
121
115
  rowActions: {
@@ -152,6 +146,8 @@ export class TemplatesPageController extends PageController(
152
146
  deepLink: true,
153
147
  target: {
154
148
  type: "drawer",
149
+ // On the right, as the gallery opens it (useTemplateDrawer).
150
+ direction: "right",
155
151
  component: templateDrawer,
156
152
  title: "$dms_mailing.templates.actions.details",
157
153
  },
@@ -0,0 +1,77 @@
1
+ import {
2
+ type PageToolbarProps,
3
+ type PeriodComparison,
4
+ type PeriodPreset,
5
+ pageToolbar,
6
+ } from "@antelopejs/interface-dms/base";
7
+ import type { ComponentBuilder } from "@antelopejs/interface-dms/component";
8
+ import { CustomComponent } from "@antelopejs/interface-dms/base/custom";
9
+ import type { PageLiveOptions } from "@antelopejs/interface-dms/page";
10
+ import { MODULE_ID } from "../constants";
11
+
12
+ /** Shortest interval, in seconds, between two refetches of a live block. */
13
+ const LIVE_MAX_RATE_SECONDS = 5;
14
+
15
+ /**
16
+ * Direct / Pause of the pages fed by the sends topic. One `rememberKey` for
17
+ * the module: pausing the Sends log pauses the Overview too.
18
+ */
19
+ export const MAILING_LIVE: PageLiveOptions = {
20
+ maxRate: LIVE_MAX_RATE_SECONDS,
21
+ rememberKey: MODULE_ID,
22
+ };
23
+
24
+ /** What the paused toggle and a held table call the sends they missed. */
25
+ export const NEW_SENDS_LABEL = "$dms_mailing.live.new_sends";
26
+
27
+ /**
28
+ * The comparisons every mailing toolbar offers, in its "vs …" menu: the
29
+ * previous period by default, the same window a year back, or none, which
30
+ * leaves the cards without a delta.
31
+ */
32
+ export const MAILING_COMPARISONS: PeriodComparison[] = [
33
+ "previous-period",
34
+ "previous-year",
35
+ "none",
36
+ ];
37
+
38
+ /** The period a page's toolbar offers. */
39
+ export interface MailingToolbarPeriod {
40
+ scope: string;
41
+ presets: PeriodPreset[];
42
+ defaultPreset: PeriodPreset;
43
+ }
44
+
45
+ /**
46
+ * The DMS page toolbar of a mailing page: the provider pill on the left, then
47
+ * "updated", Direct / Pause and the period with its comparison menu ("vs …").
48
+ *
49
+ * The page keeps it under its `header` static, the id the former header
50
+ * block had, so a role granting it keeps granting the bar.
51
+ */
52
+ export function mailingToolbar(
53
+ period: MailingToolbarPeriod,
54
+ ): ComponentBuilder<PageToolbarProps> {
55
+ return pageToolbar({
56
+ leading: {
57
+ provider: CustomComponent("MailingProviderPill").meta({
58
+ name: "$dms_mailing.permissions.provider_pill.name",
59
+ description: "$dms_mailing.permissions.provider_pill.description",
60
+ icon: "i-ph-plugs-connected",
61
+ }),
62
+ },
63
+ live: { missedLabel: NEW_SENDS_LABEL },
64
+ period: {
65
+ id: period.scope,
66
+ presets: period.presets,
67
+ defaultPreset: period.defaultPreset,
68
+ // More than one comparison: pageToolbar() draws them as a "vs …" menu
69
+ // on the first one, the previous period.
70
+ comparisons: MAILING_COMPARISONS,
71
+ },
72
+ }).meta({
73
+ name: "$dms_mailing.permissions.page_header.name",
74
+ description: "$dms_mailing.permissions.page_header.description",
75
+ icon: "i-ph-sliders-horizontal",
76
+ });
77
+ }
package/src/realtime.ts CHANGED
@@ -31,11 +31,20 @@ export function registerRealtimeTopics(): void {
31
31
  }
32
32
  }
33
33
 
34
- function publishSendChange(type: string, sendId: string): void {
35
- const payload = { ids: [sendId] };
34
+ /**
35
+ * A send of `tenantId` was created or changed: the sends table and the
36
+ * Overview / Sends figures of that tenant read again (AntelopeJS/dms#353
37
+ * scopes the message to it; another workspace hears nothing).
38
+ */
39
+ function publishSendChange(
40
+ type: string,
41
+ tenantId: string,
42
+ sendId: string,
43
+ ): void {
44
+ const options = { payload: { ids: [sendId] }, tenantId };
36
45
  void Promise.all([
37
- PublishMessage(SENDS_ROW_TOPIC, type, { payload }),
38
- PublishMessage(MAILING_SENDS_TOPIC, type, { payload }),
46
+ PublishMessage(SENDS_ROW_TOPIC, type, options),
47
+ PublishMessage(MAILING_SENDS_TOPIC, type, options),
39
48
  ]).catch((error: unknown) => {
40
49
  Logging.Error(
41
50
  `[dms-mailing] Failed to publish "${type}" for send "${sendId}":`,
@@ -44,10 +53,10 @@ function publishSendChange(type: string, sendId: string): void {
44
53
  });
45
54
  }
46
55
 
47
- export function publishSendCreated(sendId: string): void {
48
- publishSendChange(SEND_CREATED_TYPE, sendId);
56
+ export function publishSendCreated(tenantId: string, sendId: string): void {
57
+ publishSendChange(SEND_CREATED_TYPE, tenantId, sendId);
49
58
  }
50
59
 
51
- export function publishSendUpdated(sendId: string): void {
52
- publishSendChange(SEND_UPDATED_TYPE, sendId);
60
+ export function publishSendUpdated(tenantId: string, sendId: string): void {
61
+ publishSendChange(SEND_UPDATED_TYPE, tenantId, sendId);
53
62
  }