@antelopejs/dms-mailing 0.6.1 → 0.7.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.
- package/README.md +8 -7
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +2 -3
- package/dist/constants.js.map +1 -1
- package/dist/data/templates-table.d.ts +4 -1
- package/dist/data/templates-table.js +55 -20
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +5 -5
- package/dist/db/models/index.d.ts +0 -1
- package/dist/db/models/index.js +0 -1
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/send-events.model.js +2 -3
- package/dist/db/models/send-events.model.js.map +1 -1
- package/dist/db/models/sends.model.js +2 -3
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +10 -1
- package/dist/db/models/templates.model.js +29 -10
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +0 -1
- package/dist/db/tables/index.js +0 -1
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/settings.table.d.ts +0 -1
- package/dist/db/tables/settings.table.js +0 -4
- package/dist/db/tables/settings.table.js.map +1 -1
- package/dist/db/tables/templates.table.d.ts +7 -4
- package/dist/db/tables/templates.table.js +4 -14
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/formats.d.ts +13 -0
- package/dist/engine/formats.js +85 -0
- package/dist/engine/formats.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +14 -7
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +3 -5
- package/dist/engine/tokens.js.map +1 -1
- package/dist/frontend-module.d.ts +2 -0
- package/dist/frontend-module.js +24 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/tenant-export.js +4 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +3 -3
- package/dist/implementations/dms-mailing/index.js +8 -7
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +2 -20
- package/dist/index.js.map +1 -1
- package/dist/pages/overview.d.ts +1 -1
- package/dist/pages/overview.js +11 -13
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +1 -1
- package/dist/pages/sends.js +16 -8
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +8 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/templates.js +3 -8
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.d.ts +12 -0
- package/dist/pages/toolbar.js +41 -0
- package/dist/pages/toolbar.js.map +1 -0
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js +8 -8
- package/dist/realtime.js.map +1 -1
- package/dist/routes/metrics.d.ts +9 -2
- package/dist/routes/metrics.js +90 -35
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +1 -0
- package/dist/routes/period.js +19 -0
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +6 -3
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -7
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.d.ts +8 -4
- package/dist/routes/templates.js +41 -20
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/categories.d.ts +2 -1
- package/dist/services/categories.js +9 -0
- package/dist/services/categories.js.map +1 -1
- package/dist/services/declarations.js +42 -184
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +1 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +7 -0
- package/dist/services/localized-text.js +28 -0
- package/dist/services/localized-text.js.map +1 -0
- package/dist/services/send-import.js +0 -2
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send-stat-items.js +34 -12
- package/dist/services/send-stat-items.js.map +1 -1
- package/dist/services/send-stats.d.ts +7 -0
- package/dist/services/send-stats.js +17 -5
- package/dist/services/send-stats.js.map +1 -1
- package/dist/services/send.d.ts +2 -0
- package/dist/services/send.js +11 -12
- package/dist/services/send.js.map +1 -1
- package/dist/services/sender-declaration.d.ts +2 -0
- package/dist/services/sender-declaration.js +18 -0
- package/dist/services/sender-declaration.js.map +1 -0
- package/dist/services/settings.d.ts +1 -1
- package/dist/services/settings.js +1 -5
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +2 -2
- package/dist/services/starters.js +1 -10
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +13 -7
- package/dist/services/templates.js +72 -33
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -1
- package/dist/services/webhook-status.js +6 -5
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/settings.d.ts +8 -0
- package/dist/types/settings.js +33 -6
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +25 -7
- package/dist/validation/declarations.schema.js +16 -3
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/settings.schema.d.ts +41 -41
- package/dist/validation/settings.schema.js +30 -6
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +13 -0
- package/dist/validation/templates.schema.js +6 -1
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +15 -13
- package/frontend-vue/app/components/FunnelCard.vue +46 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
- package/frontend-vue/app/components/OverviewGate.vue +0 -8
- package/frontend-vue/app/components/ProviderPill.vue +50 -0
- package/frontend-vue/app/components/SendDrawer.vue +206 -183
- package/frontend-vue/app/components/StarterGallery.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +4 -1
- package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
- package/frontend-vue/app/components/TemplateGallery.vue +57 -17
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
- package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
- package/frontend-vue/app/composables/useMailingApi.ts +3 -3
- package/frontend-vue/app/composables/useMailingReads.ts +2 -2
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
- package/frontend-vue/app/types/mailing.ts +18 -0
- package/frontend-vue/app/utils/categories.ts +58 -49
- package/frontend-vue/app/utils/funnel.ts +25 -0
- package/frontend-vue/app/utils/period.ts +0 -7
- package/frontend-vue/app/utils/starters.ts +3 -1
- package/frontend-vue/app/utils/tokens.ts +6 -1
- package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +4 -10
- package/frontend-vue/tests/categories.test.ts +55 -17
- package/frontend-vue/tests/conditions.test.ts +1 -4
- package/frontend-vue/tests/funnel-card.test.ts +27 -0
- package/frontend-vue/tests/funnel.test.ts +25 -0
- package/frontend-vue/tests/localized-defaults.test.ts +0 -29
- package/frontend-vue/tests/new-template-modal.test.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -24
- package/frontend-vue/tests/page-reads.test.ts +9 -25
- package/frontend-vue/tests/period.test.ts +0 -12
- package/frontend-vue/tests/replay-toast.test.ts +2 -1
- package/frontend-vue/tests/send-drawer.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +1 -1
- package/frontend-vue/tests/template-gallery.test.ts +90 -3
- package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
- package/package.json +8 -7
- package/skills/dms-mailing/SKILL.md +25 -18
- package/src/constants.ts +2 -2
- package/src/data/sends-table.ts +1 -1
- package/src/data/templates-table.ts +125 -33
- package/src/db/models/index.ts +0 -1
- package/src/db/models/send-events.model.ts +2 -2
- package/src/db/models/sends.model.ts +7 -8
- package/src/db/models/templates.model.ts +64 -16
- package/src/db/tables/index.ts +0 -1
- package/src/db/tables/settings.table.ts +0 -8
- package/src/db/tables/templates.table.ts +12 -17
- package/src/engine/formats.ts +124 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/resolve.ts +18 -7
- package/src/engine/tokens.ts +7 -5
- package/src/frontend-module.ts +36 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/tenant-export.ts +8 -1
- package/src/implementations/dms-mailing/index.ts +5 -16
- package/src/index.ts +3 -29
- package/src/pages/overview.ts +20 -16
- package/src/pages/sends.ts +27 -11
- package/src/pages/settings/form.ts +16 -0
- package/src/pages/templates.ts +4 -8
- package/src/pages/toolbar.ts +77 -0
- package/src/realtime.ts +17 -8
- package/src/routes/metrics.ts +158 -45
- package/src/routes/period.ts +32 -0
- package/src/routes/sends.ts +13 -4
- package/src/routes/settings.ts +11 -8
- package/src/routes/templates.ts +81 -37
- package/src/services/categories.ts +14 -1
- package/src/services/declarations.ts +64 -317
- package/src/services/events.ts +1 -1
- package/src/services/localized-text.ts +53 -0
- package/src/services/send-import.ts +2 -2
- package/src/services/send-stat-items.ts +60 -13
- package/src/services/send-stats.ts +37 -5
- package/src/services/send.ts +19 -20
- package/src/services/sender-declaration.ts +27 -0
- package/src/services/settings.ts +3 -10
- package/src/services/starters.ts +3 -15
- package/src/services/templates.ts +109 -63
- package/src/services/webhook-status.ts +3 -3
- package/src/types/settings.ts +55 -5
- package/src/validation/declarations.schema.ts +29 -3
- package/src/validation/settings.schema.ts +56 -14
- package/src/validation/templates.schema.ts +10 -0
- package/dist/db/models/template-slugs.model.d.ts +0 -27
- package/dist/db/models/template-slugs.model.js +0 -43
- package/dist/db/models/template-slugs.model.js.map +0 -1
- package/dist/db/tables/template-slugs.table.d.ts +0 -7
- package/dist/db/tables/template-slugs.table.js +0 -35
- package/dist/db/tables/template-slugs.table.js.map +0 -1
- package/dist/hooks/database-ready.d.ts +0 -7
- package/dist/hooks/database-ready.js +0 -26
- package/dist/hooks/database-ready.js.map +0 -1
- package/dist/services/fingerprint.d.ts +0 -1
- package/dist/services/fingerprint.js +0 -20
- package/dist/services/fingerprint.js.map +0 -1
- package/frontend-vue/app/components/PageHeader.vue +0 -94
- package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
- package/src/db/models/template-slugs.model.ts +0 -61
- package/src/db/tables/template-slugs.table.ts +0 -29
- package/src/hooks/database-ready.ts +0 -47
- package/src/services/fingerprint.ts +0 -22
package/src/engine/resolve.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "../types";
|
|
16
16
|
import { evaluateCondition } from "./conditions";
|
|
17
17
|
import { safeUrl } from "./fallback-html";
|
|
18
|
+
import { formatValue, type ValueFormat } from "./formats";
|
|
18
19
|
import { getPath, isPresent } from "./paths";
|
|
19
20
|
import { footerLinkPaths } from "./system-variables";
|
|
20
21
|
import { type InterpolationMode, interpolate } from "./tokens";
|
|
@@ -26,6 +27,7 @@ interface ResolveState {
|
|
|
26
27
|
mode: InterpolationMode;
|
|
27
28
|
missing: Set<string>;
|
|
28
29
|
hidden: string[];
|
|
30
|
+
format?: ValueFormat;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** A blocked image source: the hero is left out rather than drawn broken. */
|
|
@@ -34,7 +36,7 @@ const BLOCKED_IMAGE = "";
|
|
|
34
36
|
type BlockResolver = (block: Block, state: ResolveState) => ResolvedBlock[];
|
|
35
37
|
|
|
36
38
|
function text(state: ResolveState, value: string): string {
|
|
37
|
-
const result = interpolate(value, state.data, state.mode);
|
|
39
|
+
const result = interpolate(value, state.data, state.mode, state.format);
|
|
38
40
|
for (const path of result.missing) state.missing.add(path);
|
|
39
41
|
return result.text;
|
|
40
42
|
}
|
|
@@ -61,9 +63,10 @@ function systemUrl(
|
|
|
61
63
|
return isPresent(value) ? safeUrl(String(value)) : "";
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
/** A list cell: a money value or a date in it is written for the locale. */
|
|
67
|
+
function cell(item: unknown, path: string, state: ResolveState): string {
|
|
65
68
|
const value = getPath(item, path);
|
|
66
|
-
return isPresent(value) ?
|
|
69
|
+
return isPresent(value) ? formatValue(value, "", state.format) : "";
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function listRows(block: ListBlock, state: ResolveState): ResolvedListRow[] {
|
|
@@ -73,8 +76,8 @@ function listRows(block: ListBlock, state: ResolveState): ResolvedListRow[] {
|
|
|
73
76
|
return [];
|
|
74
77
|
}
|
|
75
78
|
return source.map((item) => ({
|
|
76
|
-
label: cell(item, block.labelPath),
|
|
77
|
-
value: cell(item, block.valuePath),
|
|
79
|
+
label: cell(item, block.labelPath, state),
|
|
80
|
+
value: cell(item, block.valuePath, state),
|
|
78
81
|
}));
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -191,14 +194,22 @@ const isUserPath = (path: string): boolean =>
|
|
|
191
194
|
|
|
192
195
|
/**
|
|
193
196
|
* Resolves `content` against `data`. A real send passes `send` so a missing
|
|
194
|
-
* value leaves a blank; previews keep the token visible.
|
|
197
|
+
* value leaves a blank; previews keep the token visible. `format` writes the
|
|
198
|
+
* typed values (money, dates) for the e-mail's locale: see `valueFormatOf`.
|
|
195
199
|
*/
|
|
196
200
|
export function resolveEmail(
|
|
197
201
|
content: LocaleContent,
|
|
198
202
|
data: Data,
|
|
199
203
|
mode: InterpolationMode = "preview",
|
|
204
|
+
format?: ValueFormat,
|
|
200
205
|
): ResolvedEmail {
|
|
201
|
-
const state: ResolveState = {
|
|
206
|
+
const state: ResolveState = {
|
|
207
|
+
data,
|
|
208
|
+
mode,
|
|
209
|
+
missing: new Set(),
|
|
210
|
+
hidden: [],
|
|
211
|
+
format,
|
|
212
|
+
};
|
|
202
213
|
const subject = text(state, content.subject);
|
|
203
214
|
const preheader = text(state, content.preheader);
|
|
204
215
|
const blocks = resolveBlocks(content.blocks, state);
|
package/src/engine/tokens.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatValue, type ValueFormat } from "./formats";
|
|
1
2
|
import { getPath, isPresent } from "./paths";
|
|
2
3
|
|
|
3
4
|
export const TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9_.[\]]+)\s*\}\}/g;
|
|
@@ -25,14 +26,15 @@ export function extractTokens(text: string): string[] {
|
|
|
25
26
|
return [...found];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Replaces each token of `text` with its value in `data`, typed values
|
|
31
|
+
* written for `format`'s locale (see {@link formatValue}).
|
|
32
|
+
*/
|
|
32
33
|
export function interpolate(
|
|
33
34
|
text: string,
|
|
34
35
|
data: Record<string, unknown>,
|
|
35
36
|
mode: InterpolationMode = "preview",
|
|
37
|
+
format?: ValueFormat,
|
|
36
38
|
): InterpolationResult {
|
|
37
39
|
const missing = new Set<string>();
|
|
38
40
|
const output = text.replace(TOKEN_PATTERN, (token, path: string) => {
|
|
@@ -41,7 +43,7 @@ export function interpolate(
|
|
|
41
43
|
missing.add(path);
|
|
42
44
|
return MISSING_TOKEN[mode](token);
|
|
43
45
|
}
|
|
44
|
-
return formatValue(value);
|
|
46
|
+
return formatValue(value, path, format);
|
|
45
47
|
});
|
|
46
48
|
return { text: output, missing: [...missing] };
|
|
47
49
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { AddFrontendModuleOptions } from "@antelopejs/interface-dms/page";
|
|
3
|
+
import {
|
|
4
|
+
API_BASE_PATH,
|
|
5
|
+
FRONTEND_MODULE_NAME,
|
|
6
|
+
WEBHOOK_SECRET_HEADER,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The provider webhook (`/events/:provider/:tenantId`), served through the
|
|
11
|
+
* dashboard's public URL: the front proxy lets it through without an
|
|
12
|
+
* `Origin`, never sends it a session and relays the secret header, which the
|
|
13
|
+
* route checks itself.
|
|
14
|
+
*/
|
|
15
|
+
const PROVIDER_WEBHOOK_ENDPOINT = {
|
|
16
|
+
path: `${API_BASE_PATH}/events/*/*`,
|
|
17
|
+
methods: ["POST"],
|
|
18
|
+
headers: [WEBHOOK_SECRET_HEADER],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This module's frontend, from its configuration alone: the backend registers
|
|
23
|
+
* it with `AddFrontendModule`, and `@antelopejs/dms-frontend` calls this same
|
|
24
|
+
* function (declared as `dmsFrontend` in package.json) to build and start the
|
|
25
|
+
* frontend without a backend. It reads nothing but its arguments.
|
|
26
|
+
*/
|
|
27
|
+
export function frontendModule(): AddFrontendModuleOptions {
|
|
28
|
+
return {
|
|
29
|
+
name: FRONTEND_MODULE_NAME,
|
|
30
|
+
sourcePath: path.join(__dirname, "../frontend-vue"),
|
|
31
|
+
renderer: { name: "vue", version: "3" },
|
|
32
|
+
options: { dmsMailing: {} },
|
|
33
|
+
priority: 0,
|
|
34
|
+
publicEndpoints: [PROVIDER_WEBHOOK_ENDPOINT],
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@antelopejs/interface-dms/hooks";
|
|
11
11
|
import { MODULE_ID } from "../constants";
|
|
12
12
|
import {
|
|
13
|
+
type MailingTemplate,
|
|
13
14
|
SettingsModel,
|
|
14
15
|
SuppressionModel,
|
|
15
16
|
TemplateModel,
|
|
@@ -98,6 +99,12 @@ async function loadPage<T extends object>(
|
|
|
98
99
|
.filter((row): row is T => row !== undefined);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/** A template as exported: its name in every language. */
|
|
103
|
+
const templateCodec: RowCodec<MailingTemplate> = {
|
|
104
|
+
fromDatabase: (row) =>
|
|
105
|
+
TemplateModel.fromDatabase(row)?.localize("*", ["name"]),
|
|
106
|
+
};
|
|
107
|
+
|
|
101
108
|
export function exportedCollections(tenantId: string): ExportedCollection[] {
|
|
102
109
|
const templates = GetModel(TemplateModel, tenantId);
|
|
103
110
|
const versions = GetModel(TemplateVersionModel, tenantId);
|
|
@@ -106,7 +113,7 @@ export function exportedCollections(tenantId: string): ExportedCollection[] {
|
|
|
106
113
|
{
|
|
107
114
|
entry: TEMPLATES_ENTRY,
|
|
108
115
|
load: (offset, size) =>
|
|
109
|
-
loadPage(templates.table,
|
|
116
|
+
loadPage(templates.table, templateCodec, offset, size),
|
|
110
117
|
},
|
|
111
118
|
{
|
|
112
119
|
entry: TEMPLATE_VERSIONS_ENTRY,
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declareTemplateCategory,
|
|
1
|
+
export {
|
|
2
|
+
declareTemplate as DeclareTemplate,
|
|
3
|
+
declareTemplateCategory as DeclareTemplateCategory,
|
|
5
4
|
} from "../../services/declarations";
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export { importSends as ImportSends } from "../../services/send-import";
|
|
6
|
+
export { declareSender as DeclareSender } from "../../services/sender-declaration";
|
|
8
7
|
export { recordEmailEvent as RecordEmailEvent } from "../../services/events";
|
|
9
8
|
export { sendUnlessSuppressed as SendTemplate } from "../../services/checked-send";
|
|
10
|
-
|
|
11
|
-
/*
|
|
12
|
-
* Projects call these from their `start()`, which runs in parallel with the
|
|
13
|
-
* DMS's: they wait for the database the DMS opens there rather than fail.
|
|
14
|
-
*/
|
|
15
|
-
export const DeclareTemplate = afterDatabaseReady(declareTemplate);
|
|
16
|
-
export const DeclareTemplateCategory = afterDatabaseReady(
|
|
17
|
-
declareTemplateCategory,
|
|
18
|
-
);
|
|
19
|
-
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,
|
|
@@ -16,13 +14,9 @@ import {
|
|
|
16
14
|
scheduleJob,
|
|
17
15
|
stopJob,
|
|
18
16
|
} from "./crons";
|
|
19
|
-
import {
|
|
20
|
-
registerDatabaseReady,
|
|
21
|
-
registerTenantExport,
|
|
22
|
-
unregisterTenantExport,
|
|
23
|
-
} from "./hooks";
|
|
17
|
+
import { registerTenantExport, unregisterTenantExport } from "./hooks";
|
|
24
18
|
import { registerRealtimeTopics } from "./realtime";
|
|
25
|
-
import {
|
|
19
|
+
import { frontendModule } from "./frontend-module";
|
|
26
20
|
|
|
27
21
|
export * from "./data";
|
|
28
22
|
export * from "./data-types";
|
|
@@ -33,22 +27,9 @@ export type { DmsMailingConfig } from "./config";
|
|
|
33
27
|
|
|
34
28
|
const CRON_JOBS = [RETENTION_JOB, ATTENTION_JOB];
|
|
35
29
|
|
|
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
30
|
let scheduledJobs: ScheduledJob[] = [];
|
|
49
31
|
|
|
50
32
|
export async function construct(config: DmsMailingConfig = {}): Promise<void> {
|
|
51
|
-
registerDatabaseReady();
|
|
52
33
|
setConfig(config);
|
|
53
34
|
ImplementInterface(
|
|
54
35
|
await import("@antelopejs/interface-dms-mailing"),
|
|
@@ -57,14 +38,7 @@ export async function construct(config: DmsMailingConfig = {}): Promise<void> {
|
|
|
57
38
|
registerAutomationNodes();
|
|
58
39
|
registerTenantExport();
|
|
59
40
|
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
|
-
});
|
|
41
|
+
await AddFrontendModule(frontendModule());
|
|
68
42
|
}
|
|
69
43
|
|
|
70
44
|
export function destroy(): void {
|
package/src/pages/overview.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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({
|
package/src/pages/sends.ts
CHANGED
|
@@ -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 =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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: "
|
|
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
|
{
|
|
@@ -7,6 +7,8 @@ import { ComponentBuilder } from "@antelopejs/interface-dms/component";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { API_BASE_PATH } from "../../constants";
|
|
9
9
|
import { TemplateCategoriesType } from "../../data-types";
|
|
10
|
+
import { missingVariablesPolicy } from "../../services/settings";
|
|
11
|
+
import { MODULE_DEFAULT_SETTINGS } from "../../types";
|
|
10
12
|
import {
|
|
11
13
|
MAX_RETENTION_DAYS,
|
|
12
14
|
MIN_RETENTION_DAYS,
|
|
@@ -37,6 +39,18 @@ const field = (id: string) => ({
|
|
|
37
39
|
description: key(`fields.${id}.description`),
|
|
38
40
|
});
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A field holding a module default offers it: "Module default · Use default"
|
|
44
|
+
* while it holds something else. The retention does not: one click there
|
|
45
|
+
* could shorten it, and lowering it deletes sends, which its input asks to
|
|
46
|
+
* confirm instead. Nor do the categories: putting the default list back
|
|
47
|
+
* would move every template of a custom category.
|
|
48
|
+
*/
|
|
49
|
+
const moduleDefault = (defaultValue: string) => ({
|
|
50
|
+
defaultValue,
|
|
51
|
+
offersDefault: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
40
54
|
const section = (
|
|
41
55
|
id: string,
|
|
42
56
|
icon: string,
|
|
@@ -84,12 +98,14 @@ const guards = section("guards", "i-ph-shield-check", [
|
|
|
84
98
|
],
|
|
85
99
|
}),
|
|
86
100
|
required: true,
|
|
101
|
+
...moduleDefault(missingVariablesPolicy(MODULE_DEFAULT_SETTINGS)),
|
|
87
102
|
},
|
|
88
103
|
{
|
|
89
104
|
...field("fallbackLocale"),
|
|
90
105
|
type: new DefaultDataTypes.StringType({}),
|
|
91
106
|
inputComponent: CustomComponent("MailingLocaleSelect").serializeSync(),
|
|
92
107
|
required: true,
|
|
108
|
+
...moduleDefault(MODULE_DEFAULT_SETTINGS.fallbackLocale),
|
|
93
109
|
},
|
|
94
110
|
]);
|
|
95
111
|
|
package/src/pages/templates.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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,
|
|
38
|
-
PublishMessage(MAILING_SENDS_TOPIC, type,
|
|
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
|
}
|