@antelopejs/dms-mailing 0.9.2 → 0.10.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.
- package/README.md +11 -11
- package/dist/data/read-only-routes.d.ts +14 -1
- package/dist/data/sends-table.d.ts +14 -1
- package/dist/data/suppressions-table.d.ts +14 -1
- package/dist/data/templates-table.d.ts +15 -2
- package/dist/db/models/send-events.model.d.ts +3 -3
- package/dist/db/models/sends.model.d.ts +3 -3
- package/dist/db/models/settings.model.d.ts +3 -3
- package/dist/db/models/suppressions.model.d.ts +3 -3
- package/dist/db/models/template-versions.model.d.ts +3 -3
- package/dist/db/models/template-versions.model.js +2 -3
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +4 -3
- package/dist/db/models/templates.model.js +14 -6
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +3 -3
- package/dist/db/tables/templates.table.d.ts +4 -1
- package/dist/hooks/tenant-export.js +5 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/pages/editor.js +1 -1
- package/dist/pages/editor.js.map +1 -1
- package/dist/routes/sends.js +2 -1
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/templates.d.ts +2 -2
- package/dist/routes/templates.js +2 -2
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/settings.js +2 -1
- package/dist/services/settings.js.map +1 -1
- package/dist/services/templates.js +10 -8
- package/dist/services/templates.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +1 -1
- package/dist/validation/stored.schema.d.ts +12 -0
- package/dist/validation/stored.schema.js +198 -0
- package/dist/validation/stored.schema.js.map +1 -0
- package/frontend-vue/app/composables/useEditorContext.ts +1 -1
- package/frontend-vue/build/backend-boundary.ts +7 -2
- package/frontend-vue/dms.frontend.ts +1 -4
- package/frontend-vue/eslint.config.mjs +9 -1
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/pnpm-lock.yaml +24 -7
- package/frontend-vue/tests/backend-boundary.test.ts +4 -4
- package/frontend-vue/tests/column-displays.test.ts +13 -12
- package/frontend-vue/tests/footer-block.test.ts +1 -1
- package/frontend-vue/tests/template-gallery-display.test.ts +6 -5
- package/frontend-vue/tests/template-overlays.test.ts +4 -12
- package/package.json +15 -15
- package/src/db/models/template-versions.model.ts +8 -6
- package/src/db/models/templates.model.ts +25 -9
- package/src/hooks/tenant-export.ts +5 -2
- package/src/pages/editor.ts +1 -1
- package/src/routes/sends.ts +2 -3
- package/src/routes/templates.ts +3 -2
- package/src/services/settings.ts +9 -3
- package/src/services/templates.ts +35 -16
- package/src/validation/stored.schema.ts +257 -0
- /package/frontend-vue/app/components/{Editor.vue → TemplateEditor.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Button.vue → BlockButton.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Code.vue → BlockCode.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Divider.vue → BlockDivider.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Footer.vue → BlockFooter.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Heading.vue → BlockHeading.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Hero.vue → BlockHero.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{List.vue → BlockList.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Paragraph.vue → BlockParagraph.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Total.vue → BlockTotal.vue} +0 -0
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
writeFileSync,
|
|
8
8
|
} from 'node:fs'
|
|
9
9
|
import { tmpdir } from 'node:os'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
11
|
-
import { build as viteBuild, type Plugin } from 'vite'
|
|
10
|
+
import { dirname, join } from 'node:path/posix'
|
|
11
|
+
import { normalizePath, build as viteBuild, type Plugin } from 'vite'
|
|
12
12
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
13
13
|
import { backendBoundary } from '../build/backend-boundary'
|
|
14
14
|
|
|
@@ -22,8 +22,8 @@ function write(file: string, text: string): void {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
beforeAll(() => {
|
|
25
|
-
repository =
|
|
26
|
-
mkdtempSync(
|
|
25
|
+
repository = normalizePath(
|
|
26
|
+
realpathSync(mkdtempSync(`${tmpdir()}/dms-mailing-boundary-`)),
|
|
27
27
|
)
|
|
28
28
|
const backend = join(repository, 'packages/dms-mailing')
|
|
29
29
|
frontend = join(backend, 'frontend-vue')
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import type { VNode } from 'vue'
|
|
4
|
+
import CategoryLabel from '../app/components/CategoryLabel.vue'
|
|
5
|
+
import setup, {
|
|
6
|
+
renderCategory,
|
|
7
|
+
renderLocaleChips,
|
|
8
|
+
} from '../app/plugins/column-displays'
|
|
4
9
|
|
|
5
10
|
interface RegisteredType {
|
|
6
11
|
id: string
|
|
@@ -11,9 +16,12 @@ type PluginSetup = () => void
|
|
|
11
16
|
|
|
12
17
|
const registered: RegisteredType[] = []
|
|
13
18
|
|
|
19
|
+
vi.hoisted(() => {
|
|
20
|
+
vi.stubGlobal('defineDmsPlugin', (pluginSetup: PluginSetup) => pluginSetup)
|
|
21
|
+
})
|
|
22
|
+
|
|
14
23
|
beforeEach(() => {
|
|
15
24
|
registered.length = 0
|
|
16
|
-
vi.stubGlobal('defineDmsPlugin', (setup: PluginSetup) => setup)
|
|
17
25
|
vi.stubGlobal('useDataTypes', () => ({
|
|
18
26
|
registerDataType: (type: RegisteredType) => registered.push(type),
|
|
19
27
|
}))
|
|
@@ -24,7 +32,6 @@ beforeEach(() => {
|
|
|
24
32
|
|
|
25
33
|
afterEach(() => {
|
|
26
34
|
vi.unstubAllGlobals()
|
|
27
|
-
vi.resetModules()
|
|
28
35
|
})
|
|
29
36
|
|
|
30
37
|
function texts(node: VNode): string[] {
|
|
@@ -36,8 +43,7 @@ function texts(node: VNode): string[] {
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
describe('column displays plugin', () => {
|
|
39
|
-
it('registers the locale chips and category displays without a DOM',
|
|
40
|
-
const { default: setup } = await import('../app/plugins/column-displays')
|
|
46
|
+
it('registers the locale chips and category displays without a DOM', () => {
|
|
41
47
|
;(setup as unknown as PluginSetup)()
|
|
42
48
|
expect(registered.map((type) => type.id)).toEqual([
|
|
43
49
|
'mailing:locale-chips',
|
|
@@ -45,18 +51,14 @@ describe('column displays plugin', () => {
|
|
|
45
51
|
])
|
|
46
52
|
})
|
|
47
53
|
|
|
48
|
-
it('draws a category through its label component',
|
|
49
|
-
const { renderCategory } = await import('../app/plugins/column-displays')
|
|
50
|
-
const { default: CategoryLabel } =
|
|
51
|
-
await import('../app/components/CategoryLabel.vue')
|
|
54
|
+
it('draws a category through its label component', () => {
|
|
52
55
|
const cell = renderCategory('orders')
|
|
53
56
|
expect(cell.type).toBe(CategoryLabel)
|
|
54
57
|
expect(cell.props).toEqual({ value: 'orders' })
|
|
55
58
|
expect(renderCategory(undefined).props).toEqual({ value: null })
|
|
56
59
|
})
|
|
57
60
|
|
|
58
|
-
it('draws the locale chips with the missing workspace ones dashed',
|
|
59
|
-
const { default: setup } = await import('../app/plugins/column-displays')
|
|
61
|
+
it('draws the locale chips with the missing workspace ones dashed', () => {
|
|
60
62
|
;(setup as unknown as PluginSetup)()
|
|
61
63
|
const cell = registered[0]!.formatter.default('en,fr', 'en')
|
|
62
64
|
const chips = cell.children as VNode[]
|
|
@@ -65,8 +67,7 @@ describe('column displays plugin', () => {
|
|
|
65
67
|
expect(String(chips[0]!.props?.class)).not.toContain('border-dashed')
|
|
66
68
|
})
|
|
67
69
|
|
|
68
|
-
it('draws a dash for a row without locale information',
|
|
69
|
-
const { renderLocaleChips } = await import('../app/plugins/column-displays')
|
|
70
|
+
it('draws a dash for a row without locale information', () => {
|
|
70
71
|
expect(texts(renderLocaleChips('', ['en']))).toEqual(['—'])
|
|
71
72
|
expect(texts(renderLocaleChips(undefined, ['en']))).toEqual(['—'])
|
|
72
73
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import { defineComponent, h, provide, ref } from 'vue'
|
|
4
|
-
import FooterBlockView from '../app/components/blocks/
|
|
4
|
+
import FooterBlockView from '../app/components/blocks/BlockFooter.vue'
|
|
5
5
|
import type { FooterBlock } from '../app/types/mailing'
|
|
6
6
|
import { TOKEN_DATA_KEY } from '../app/utils/tokens'
|
|
7
7
|
import type { TokenData } from '../app/utils/tokens'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import type { Component } from 'vue'
|
|
4
|
+
import setup from '../app/plugins/template-gallery-display'
|
|
4
5
|
|
|
5
6
|
interface DisplayRegistration {
|
|
6
7
|
id: string
|
|
@@ -9,21 +10,21 @@ interface DisplayRegistration {
|
|
|
9
10
|
|
|
10
11
|
type PluginSetup = () => void
|
|
11
12
|
|
|
13
|
+
vi.hoisted(() => {
|
|
14
|
+
vi.stubGlobal('defineDmsPlugin', (pluginSetup: PluginSetup) => pluginSetup)
|
|
15
|
+
})
|
|
16
|
+
|
|
12
17
|
afterEach(() => {
|
|
13
18
|
vi.unstubAllGlobals()
|
|
14
|
-
vi.resetModules()
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
describe('template gallery display plugin', () => {
|
|
18
|
-
it('registers the gallery under the mailing namespace without a DOM',
|
|
22
|
+
it('registers the gallery under the mailing namespace without a DOM', () => {
|
|
19
23
|
const registrations: DisplayRegistration[] = []
|
|
20
|
-
vi.stubGlobal('defineDmsPlugin', (setup: PluginSetup) => setup)
|
|
21
24
|
vi.stubGlobal(
|
|
22
25
|
'registerTableViewDisplay',
|
|
23
26
|
(registration: DisplayRegistration) => registrations.push(registration),
|
|
24
27
|
)
|
|
25
|
-
const { default: setup } =
|
|
26
|
-
await import('../app/plugins/template-gallery-display')
|
|
27
28
|
setup()
|
|
28
29
|
expect(registrations.map((registration) => registration.id)).toEqual([
|
|
29
30
|
'mailing:gallery',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { useTemplateDrawer } from '../app/composables/useTemplateDrawer'
|
|
3
4
|
import type { TemplateRow } from '../app/types/mailing'
|
|
4
5
|
|
|
5
6
|
interface OpenCall {
|
|
@@ -53,7 +54,6 @@ beforeEach(() => {
|
|
|
53
54
|
|
|
54
55
|
afterEach(() => {
|
|
55
56
|
vi.unstubAllGlobals()
|
|
56
|
-
vi.resetModules()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
function row(id: string): TemplateRow {
|
|
@@ -68,9 +68,7 @@ function row(id: string): TemplateRow {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
describe('useTemplateDrawer', () => {
|
|
71
|
-
it('opens the test send with the editor draft and locale',
|
|
72
|
-
const { useTemplateDrawer } =
|
|
73
|
-
await import('../app/composables/useTemplateDrawer')
|
|
71
|
+
it('opens the test send with the editor draft and locale', () => {
|
|
74
72
|
const content = { locales: {} }
|
|
75
73
|
useTemplateDrawer().openTestSend('t1', { locale: 'fr', content })
|
|
76
74
|
expect(modalCalls[0]?.component).toEqual({ name: 'MailingTestSendModal' })
|
|
@@ -82,8 +80,6 @@ describe('useTemplateDrawer', () => {
|
|
|
82
80
|
})
|
|
83
81
|
|
|
84
82
|
it('calls back only when the dialog closes after a change', async () => {
|
|
85
|
-
const { useTemplateDrawer } =
|
|
86
|
-
await import('../app/composables/useTemplateDrawer')
|
|
87
83
|
const onDone = vi.fn()
|
|
88
84
|
useTemplateDrawer().openRealSend('t1', { onDone })
|
|
89
85
|
await Promise.resolve()
|
|
@@ -97,9 +93,7 @@ describe('useTemplateDrawer', () => {
|
|
|
97
93
|
expect(modalCalls[1]?.componentOptions).toEqual({ initialName: 'Refund' })
|
|
98
94
|
})
|
|
99
95
|
|
|
100
|
-
it('opens the details from the right and steps through the rows shown',
|
|
101
|
-
const { useTemplateDrawer } =
|
|
102
|
-
await import('../app/composables/useTemplateDrawer')
|
|
96
|
+
it('opens the details from the right and steps through the rows shown', () => {
|
|
103
97
|
const rows = [row('a'), row('b'), row('c')]
|
|
104
98
|
useTemplateDrawer().openDetails(rows[1]!, { rows })
|
|
105
99
|
const call = drawerCalls[0]!
|
|
@@ -115,9 +109,7 @@ describe('useTemplateDrawer', () => {
|
|
|
115
109
|
expect((options.navigation as Navigation).hasNext).toBe(false)
|
|
116
110
|
})
|
|
117
111
|
|
|
118
|
-
it('opens the details of a bare id without navigation',
|
|
119
|
-
const { useTemplateDrawer } =
|
|
120
|
-
await import('../app/composables/useTemplateDrawer')
|
|
112
|
+
it('opens the details of a bare id without navigation', () => {
|
|
121
113
|
useTemplateDrawer().openDetails('x')
|
|
122
114
|
expect(drawerCalls[0]?.componentOptions.templateId).toBe('x')
|
|
123
115
|
expect(drawerCalls[0]?.componentOptions.navigation).toBeUndefined()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-mailing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "E-mail templates for the AntelopeJS DMS: a block editor with drafts and versions, a send log, delivery metrics and a suppression list.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -46,23 +46,23 @@
|
|
|
46
46
|
"registry": "https://registry.npmjs.org/"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@antelopejs/interface-api": ">=0.3.
|
|
50
|
-
"@antelopejs/interface-api-util": ">=0.3.
|
|
51
|
-
"@antelopejs/interface-core": ">=0.
|
|
52
|
-
"@antelopejs/interface-data-api": ">=0.
|
|
53
|
-
"@antelopejs/interface-database": ">=0.
|
|
54
|
-
"@antelopejs/interface-database-decorators": ">=0.
|
|
55
|
-
"@antelopejs/interface-dms": ">=0.
|
|
56
|
-
"@antelopejs/interface-dms-mailing": ">=0.
|
|
57
|
-
"@antelopejs/interface-email": ">=0.0
|
|
49
|
+
"@antelopejs/interface-api": ">=0.3.3 <0.4.0",
|
|
50
|
+
"@antelopejs/interface-api-util": ">=0.3.2 <0.4.0",
|
|
51
|
+
"@antelopejs/interface-core": ">=0.5.0 <0.6.0",
|
|
52
|
+
"@antelopejs/interface-data-api": ">=0.8.0 <0.9.0",
|
|
53
|
+
"@antelopejs/interface-database": ">=0.4.0 <0.5.0",
|
|
54
|
+
"@antelopejs/interface-database-decorators": ">=0.5.0 <0.6.0",
|
|
55
|
+
"@antelopejs/interface-dms": ">=0.14.0 <0.15.0",
|
|
56
|
+
"@antelopejs/interface-dms-mailing": ">=0.11.0 <0.12.0",
|
|
57
|
+
"@antelopejs/interface-email": ">=0.2.0 <0.3.0",
|
|
58
58
|
"node-cron": "^4.2.1",
|
|
59
59
|
"zod": "~3.24.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@antelopejs/core": "
|
|
63
|
-
"@antelopejs/dms": ">=0.
|
|
64
|
-
"@antelopejs/mongodb": "
|
|
65
|
-
"@antelopejs/tooling-configs": "
|
|
62
|
+
"@antelopejs/core": "~1.23.2",
|
|
63
|
+
"@antelopejs/dms": ">=0.20.0 <0.21.0",
|
|
64
|
+
"@antelopejs/mongodb": "~1.8.1",
|
|
65
|
+
"@antelopejs/tooling-configs": ">=0.7.1 <0.8.0",
|
|
66
66
|
"@types/chai": "^4.3.20",
|
|
67
67
|
"@types/mocha": "^10",
|
|
68
68
|
"@types/node": "^24.0.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"typescript": "^6.0.3"
|
|
78
78
|
},
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"@antelopejs/interface-dms-automation": ">=0.6.0 <
|
|
80
|
+
"@antelopejs/interface-dms-automation": ">=0.6.0 <0.7.0"
|
|
81
81
|
},
|
|
82
82
|
"antelopeJs": {
|
|
83
83
|
"baseUrl": "dist",
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type MailingTemplate,
|
|
5
5
|
MailingTemplateVersion,
|
|
6
6
|
} from "../tables";
|
|
7
|
+
import {
|
|
8
|
+
parseStored,
|
|
9
|
+
storedVersionRowSchema,
|
|
10
|
+
} from "../../validation/stored.schema";
|
|
7
11
|
import { TemplateModel } from "./templates.model";
|
|
8
12
|
|
|
9
13
|
/** The latest committed history row, even before its separate insert completes. */
|
|
@@ -11,13 +15,11 @@ export function committedVersionOf(
|
|
|
11
15
|
template: Pick<MailingTemplate, "json_published_version">,
|
|
12
16
|
): MailingTemplateVersion | undefined {
|
|
13
17
|
if (!template.json_published_version) return undefined;
|
|
14
|
-
|
|
15
|
-
// JSON.stringify of the version row they insert; its publishedAt reads back as a string, and the next line restores the Date.
|
|
16
|
-
const row = JSON.parse(
|
|
18
|
+
return parseStored(
|
|
17
19
|
template.json_published_version,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
storedVersionRowSchema,
|
|
21
|
+
"a version row",
|
|
22
|
+
);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export class TemplateVersionModel extends BasicDataModel(
|
|
@@ -35,7 +35,9 @@ export class TemplateModel extends BasicDataModel(
|
|
|
35
35
|
) {
|
|
36
36
|
/** The row of `fields`, its name written in every language it holds. */
|
|
37
37
|
static fromFields(fields: TemplateFields): MailingTemplate {
|
|
38
|
-
|
|
38
|
+
const row = TemplateModel.fromPlainData(fields);
|
|
39
|
+
row.localize("*", ["name"]);
|
|
40
|
+
return row;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -57,19 +59,30 @@ export class TemplateModel extends BasicDataModel(
|
|
|
57
59
|
return this.insert(TemplateModel.fromFields(fields));
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The stored form of a patch. The row's modifier container (`_internal`,
|
|
64
|
+
* where the localized name lives) comes out empty when the patch has no
|
|
65
|
+
* name, and the database merges a row shallowly, so that empty container
|
|
66
|
+
* would wipe the stored name: it is left out then.
|
|
67
|
+
*/
|
|
68
|
+
private static storedPatch(row: MailingTemplate, fields: TemplateFields) {
|
|
69
|
+
const stored = TemplateModel.toDatabase(row);
|
|
70
|
+
if (fields.name === undefined) delete stored[MODIFIER_CONTAINER];
|
|
71
|
+
return stored;
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/**
|
|
61
75
|
* Updates a template, a name given holding every language. Every write of
|
|
62
|
-
* a template goes through here rather than `update
|
|
63
|
-
*
|
|
64
|
-
* lives) empty when the patch has no name, and the database merges a row
|
|
65
|
-
* shallowly, so that empty container would wipe the stored name.
|
|
76
|
+
* a template goes through here rather than `update`, which would store the
|
|
77
|
+
* patch's empty modifier container over the name.
|
|
66
78
|
*/
|
|
67
79
|
updateFields(id: string, fields: TemplateFields): Promise<number> {
|
|
68
80
|
const row = TemplateModel.fromFields(fields);
|
|
69
81
|
triggerEvent(row, "update");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
return this.table
|
|
83
|
+
.get(id)
|
|
84
|
+
.update(TemplateModel.storedPatch(row, fields))
|
|
85
|
+
.run();
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
/** Publishes only over the exact semantic revision that was inspected. */
|
|
@@ -77,7 +90,10 @@ export class TemplateModel extends BasicDataModel(
|
|
|
77
90
|
template: MailingTemplate,
|
|
78
91
|
patch: TemplateFields,
|
|
79
92
|
): Promise<boolean> {
|
|
80
|
-
const stored = TemplateModel.
|
|
93
|
+
const stored = TemplateModel.storedPatch(
|
|
94
|
+
TemplateModel.fromFields(patch),
|
|
95
|
+
patch,
|
|
96
|
+
);
|
|
81
97
|
const outcome = await this.table
|
|
82
98
|
.atomicMutation(template._id, {
|
|
83
99
|
type: "update",
|
|
@@ -102,8 +102,11 @@ async function loadPage<T extends object>(
|
|
|
102
102
|
|
|
103
103
|
/** A template as exported: its name in every language. */
|
|
104
104
|
const templateCodec: RowCodec<MailingTemplate> = {
|
|
105
|
-
fromDatabase: (
|
|
106
|
-
TemplateModel.fromDatabase(
|
|
105
|
+
fromDatabase: (stored) => {
|
|
106
|
+
const row = TemplateModel.fromDatabase(stored);
|
|
107
|
+
row?.localize("*", ["name"]);
|
|
108
|
+
return row;
|
|
109
|
+
},
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
export function exportedCollections(tenantId: string): ExportedCollection[] {
|
package/src/pages/editor.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class MailingEditorController extends PageController(
|
|
|
25
25
|
},
|
|
26
26
|
DefaultLayout({ fullWidth: true, hideHeader: true, fillHeight: true }),
|
|
27
27
|
) {
|
|
28
|
-
static editor = CustomComponent("
|
|
28
|
+
static editor = CustomComponent("MailingTemplateEditor").meta({
|
|
29
29
|
name: "$dms_mailing.permissions.editor.name",
|
|
30
30
|
description: "$dms_mailing.permissions.editor.description",
|
|
31
31
|
icon: "i-ph-pencil-simple",
|
package/src/routes/sends.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
} from "../services/templates";
|
|
71
71
|
import type { SendStatus, SuppressionSummary, TemplateStatus } from "../types";
|
|
72
72
|
import type { JsonObject } from "../types/json";
|
|
73
|
+
import { parseJsonObject } from "../validation/json";
|
|
73
74
|
import {
|
|
74
75
|
type previewVersionSchema,
|
|
75
76
|
realSendSchema,
|
|
@@ -578,9 +579,7 @@ function requestedVersionOf(
|
|
|
578
579
|
}
|
|
579
580
|
|
|
580
581
|
function variablesOf(send: MailingSend): JsonObject {
|
|
581
|
-
|
|
582
|
-
// JSON.stringify(send.variables) (services/send-import.ts), both a JsonObject.
|
|
583
|
-
return JSON.parse(send.json_variables || "{}") as JsonObject;
|
|
582
|
+
return parseJsonObject(send.json_variables || "{}");
|
|
584
583
|
}
|
|
585
584
|
|
|
586
585
|
function summarize(template: MailingTemplate): SendTemplateSummary {
|
package/src/routes/templates.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Parameter,
|
|
7
7
|
Post,
|
|
8
8
|
Put,
|
|
9
|
+
type QueryValue,
|
|
9
10
|
type RequestContext,
|
|
10
11
|
} from "@antelopejs/interface-api";
|
|
11
12
|
import { assert, assertValidation } from "@antelopejs/interface-api-util";
|
|
@@ -203,14 +204,14 @@ export class TemplatesController extends Controller(
|
|
|
203
204
|
@Get("/starters/:id/preview")
|
|
204
205
|
async starterPreview(
|
|
205
206
|
@Parameter("id", "param") id: string,
|
|
206
|
-
@Parameter("locale", "query") requested
|
|
207
|
+
@Parameter("locale", "query") requested: QueryValue,
|
|
207
208
|
) {
|
|
208
209
|
const starter = findStarter(id);
|
|
209
210
|
assert(starter, HTTP_NOT_FOUND, STARTER_NOT_FOUND);
|
|
210
211
|
const settings = await getSettings(this.tenantId);
|
|
211
212
|
const locale = pickLocale(
|
|
212
213
|
starter.content,
|
|
213
|
-
requested || undefined,
|
|
214
|
+
requested === true || requested === "" ? undefined : requested,
|
|
214
215
|
settings.fallbackLocale,
|
|
215
216
|
);
|
|
216
217
|
const localeContent = localeContentOf(starter.content, locale);
|
package/src/services/settings.ts
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
} from "../types";
|
|
11
11
|
import type { JsonObject } from "../types/json";
|
|
12
12
|
import type { SettingsPatch } from "../validation/settings.schema";
|
|
13
|
+
import {
|
|
14
|
+
parseStored,
|
|
15
|
+
storedCategoriesSchema,
|
|
16
|
+
} from "../validation/stored.schema";
|
|
13
17
|
|
|
14
18
|
export const WEBHOOK_SECRET_BYTES = 24;
|
|
15
19
|
|
|
@@ -48,9 +52,11 @@ function toValues(row: MailingSettings): MailingSettingsValues {
|
|
|
48
52
|
replyTo: row.replyTo,
|
|
49
53
|
webhookSecret: row.webhookSecret,
|
|
50
54
|
webhookSecretSource: storedSecretSource(row.webhookSecretSource),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
categories: parseStored(
|
|
56
|
+
row.json_categories,
|
|
57
|
+
storedCategoriesSchema,
|
|
58
|
+
"template categories",
|
|
59
|
+
),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -28,6 +28,13 @@ import type {
|
|
|
28
28
|
} from "../types";
|
|
29
29
|
import { DEFAULT_FALLBACK_LOCALE } from "../types";
|
|
30
30
|
import type { JsonObject } from "../types/json";
|
|
31
|
+
import { parseJsonObject } from "../validation/json";
|
|
32
|
+
import {
|
|
33
|
+
parseStored,
|
|
34
|
+
storedChangesSchema,
|
|
35
|
+
storedContentSchema,
|
|
36
|
+
storedVariablesSchema,
|
|
37
|
+
} from "../validation/stored.schema";
|
|
31
38
|
|
|
32
39
|
export interface TemplateAuthor {
|
|
33
40
|
name?: string;
|
|
@@ -74,8 +81,11 @@ export async function refuseTakenSlug<T>(write: Promise<T>): Promise<T> {
|
|
|
74
81
|
|
|
75
82
|
/** The content customers receive: the last published version. */
|
|
76
83
|
export function parseContent(template: MailingTemplate): TemplateContent {
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
return parseStored(
|
|
85
|
+
template.json_content || EMPTY_CONTENT,
|
|
86
|
+
storedContentSchema,
|
|
87
|
+
"template content",
|
|
88
|
+
);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
export function hasDraft(template: MailingTemplate): boolean {
|
|
@@ -85,8 +95,11 @@ export function hasDraft(template: MailingTemplate): boolean {
|
|
|
85
95
|
/** The content the editor works on: the draft, or the published content. */
|
|
86
96
|
export function workingContent(template: MailingTemplate): TemplateContent {
|
|
87
97
|
if (!template.json_draft) return parseContent(template);
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
return parseStored(
|
|
99
|
+
template.json_draft,
|
|
100
|
+
storedContentSchema,
|
|
101
|
+
"template content",
|
|
102
|
+
);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
/** The version customers receive; 0 for a template never published. */
|
|
@@ -105,8 +118,11 @@ export function isPublished(template: MailingTemplate): boolean {
|
|
|
105
118
|
export function parseVariables(
|
|
106
119
|
template: MailingTemplate,
|
|
107
120
|
): VariableDefinition[] {
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
return parseStored(
|
|
122
|
+
template.json_variables,
|
|
123
|
+
storedVariablesSchema,
|
|
124
|
+
"variable declarations",
|
|
125
|
+
);
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
@@ -118,8 +134,11 @@ export function workingVariables(
|
|
|
118
134
|
): VariableDefinition[] {
|
|
119
135
|
if (!hasDraft(template) || !template.json_variables_draft)
|
|
120
136
|
return parseVariables(template);
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
return parseStored(
|
|
138
|
+
template.json_variables_draft,
|
|
139
|
+
storedVariablesSchema,
|
|
140
|
+
"variable declarations",
|
|
141
|
+
);
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
/** A template's working state: what a publish would ship. */
|
|
@@ -175,8 +194,7 @@ export function pendingChanges(template: MailingTemplate): TemplateChange[] {
|
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
export function parseTestData(template: MailingTemplate): JsonObject {
|
|
178
|
-
|
|
179
|
-
return JSON.parse(template.json_test_data) as JsonObject;
|
|
197
|
+
return parseJsonObject(template.json_test_data);
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
interface FooterLabels {
|
|
@@ -297,16 +315,18 @@ export async function discardDraft(
|
|
|
297
315
|
export function parseVersionChanges(
|
|
298
316
|
row: MailingTemplateVersion,
|
|
299
317
|
): TemplateChange[] {
|
|
300
|
-
|
|
301
|
-
return JSON.parse(row.json_changes) as TemplateChange[];
|
|
318
|
+
return parseStored(row.json_changes, storedChangesSchema, "version changes");
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
/** The declarations a published version shipped with. */
|
|
305
322
|
export function parseVersionVariables(
|
|
306
323
|
row: MailingTemplateVersion,
|
|
307
324
|
): VariableDefinition[] {
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
return parseStored(
|
|
326
|
+
row.json_variables,
|
|
327
|
+
storedVariablesSchema,
|
|
328
|
+
"variable declarations",
|
|
329
|
+
);
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
/**
|
|
@@ -461,8 +481,7 @@ export async function contentOfVersion(
|
|
|
461
481
|
export function parseVersionContent(
|
|
462
482
|
row: MailingTemplateVersion,
|
|
463
483
|
): TemplateContent {
|
|
464
|
-
|
|
465
|
-
return JSON.parse(row.json_content) as TemplateContent;
|
|
484
|
+
return parseStored(row.json_content, storedContentSchema, "template content");
|
|
466
485
|
}
|
|
467
486
|
|
|
468
487
|
/**
|