@antelopejs/dms-mailing 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +11 -11
  2. package/dist/data/read-only-routes.d.ts +14 -1
  3. package/dist/data/sends-table.d.ts +14 -1
  4. package/dist/data/suppressions-table.d.ts +14 -1
  5. package/dist/data/templates-table.d.ts +15 -2
  6. package/dist/db/models/send-events.model.d.ts +3 -3
  7. package/dist/db/models/sends.model.d.ts +3 -3
  8. package/dist/db/models/settings.model.d.ts +3 -3
  9. package/dist/db/models/suppressions.model.d.ts +3 -3
  10. package/dist/db/models/template-versions.model.d.ts +3 -3
  11. package/dist/db/models/template-versions.model.js +2 -3
  12. package/dist/db/models/template-versions.model.js.map +1 -1
  13. package/dist/db/models/templates.model.d.ts +4 -3
  14. package/dist/db/models/templates.model.js +14 -6
  15. package/dist/db/models/templates.model.js.map +1 -1
  16. package/dist/db/models/webhook-status.model.d.ts +3 -3
  17. package/dist/db/tables/templates.table.d.ts +4 -1
  18. package/dist/hooks/tenant-export.js +5 -1
  19. package/dist/hooks/tenant-export.js.map +1 -1
  20. package/dist/pages/editor.js +1 -1
  21. package/dist/pages/editor.js.map +1 -1
  22. package/dist/routes/sends.js +2 -1
  23. package/dist/routes/sends.js.map +1 -1
  24. package/dist/routes/templates.d.ts +2 -2
  25. package/dist/routes/templates.js +2 -2
  26. package/dist/routes/templates.js.map +1 -1
  27. package/dist/services/settings.js +2 -1
  28. package/dist/services/settings.js.map +1 -1
  29. package/dist/services/templates.js +10 -8
  30. package/dist/services/templates.js.map +1 -1
  31. package/dist/validation/declarations.schema.d.ts +1 -1
  32. package/dist/validation/stored.schema.d.ts +12 -0
  33. package/dist/validation/stored.schema.js +198 -0
  34. package/dist/validation/stored.schema.js.map +1 -0
  35. package/frontend-vue/app/build/components/FormFieldRow.vue +1 -1
  36. package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +3 -3
  37. package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +4 -4
  38. package/frontend-vue/app/components/CategoriesInput.vue +2 -2
  39. package/frontend-vue/app/components/CategoryInput.vue +1 -1
  40. package/frontend-vue/app/components/FunnelCard.vue +4 -4
  41. package/frontend-vue/app/components/LocaleSelect.vue +1 -1
  42. package/frontend-vue/app/components/NewTemplateModal.vue +1 -1
  43. package/frontend-vue/app/components/OverviewGate.vue +6 -6
  44. package/frontend-vue/app/components/RealSendModal.vue +6 -6
  45. package/frontend-vue/app/components/RenderingModal.vue +2 -2
  46. package/frontend-vue/app/components/RetentionInput.vue +1 -1
  47. package/frontend-vue/app/components/SendDrawer.vue +9 -9
  48. package/frontend-vue/app/components/StarterGallery.vue +2 -2
  49. package/frontend-vue/app/components/SystemVariables.vue +1 -1
  50. package/frontend-vue/app/components/TemplateCard.vue +2 -2
  51. package/frontend-vue/app/components/TemplateDrawer.vue +4 -4
  52. package/frontend-vue/app/components/TemplateGallery.vue +2 -2
  53. package/frontend-vue/app/components/TestSendModal.vue +4 -4
  54. package/frontend-vue/app/components/VersionPreview.vue +3 -3
  55. package/frontend-vue/app/components/editor/EditorBlockFields.vue +1 -1
  56. package/frontend-vue/app/components/editor/EditorCompare.vue +1 -1
  57. package/frontend-vue/app/components/editor/EditorEnvelope.vue +2 -2
  58. package/frontend-vue/app/components/editor/EditorLifecycle.vue +1 -1
  59. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +1 -1
  60. package/frontend-vue/app/components/editor/EditorPublishReview.vue +4 -4
  61. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +2 -2
  62. package/frontend-vue/app/components/editor/EditorTestDataPane.vue +1 -1
  63. package/frontend-vue/app/components/editor/EditorVariablesPane.vue +1 -1
  64. package/frontend-vue/app/components/editor/EditorViewSwitches.vue +2 -2
  65. package/frontend-vue/app/components/editor/EditorVisibility.vue +1 -1
  66. package/frontend-vue/app/composables/useEditorContext.ts +1 -1
  67. package/frontend-vue/build/backend-boundary.ts +7 -2
  68. package/frontend-vue/dms.frontend.ts +1 -4
  69. package/frontend-vue/eslint.config.mjs +9 -1
  70. package/frontend-vue/package.json +2 -2
  71. package/frontend-vue/pnpm-lock.yaml +24 -7
  72. package/frontend-vue/tests/backend-boundary.test.ts +4 -4
  73. package/frontend-vue/tests/categories-input.test.ts +1 -1
  74. package/frontend-vue/tests/column-displays.test.ts +13 -12
  75. package/frontend-vue/tests/dms-component-names.test.ts +12 -0
  76. package/frontend-vue/tests/footer-block.test.ts +1 -1
  77. package/frontend-vue/tests/funnel-card.test.ts +3 -3
  78. package/frontend-vue/tests/helpers/component-tags.ts +58 -0
  79. package/frontend-vue/tests/helpers/nuxt-globals.ts +1 -1
  80. package/frontend-vue/tests/overview-gate.test.ts +1 -1
  81. package/frontend-vue/tests/retention-input.test.ts +2 -2
  82. package/frontend-vue/tests/send-drawer.test.ts +2 -2
  83. package/frontend-vue/tests/send-modals.test.ts +1 -1
  84. package/frontend-vue/tests/template-drawer.test.ts +6 -6
  85. package/frontend-vue/tests/template-gallery-display.test.ts +6 -5
  86. package/frontend-vue/tests/template-gallery.test.ts +3 -3
  87. package/frontend-vue/tests/template-overlays.test.ts +4 -12
  88. package/package.json +16 -16
  89. package/src/db/models/template-versions.model.ts +8 -6
  90. package/src/db/models/templates.model.ts +25 -9
  91. package/src/hooks/tenant-export.ts +5 -2
  92. package/src/pages/editor.ts +1 -1
  93. package/src/routes/sends.ts +2 -3
  94. package/src/routes/templates.ts +3 -2
  95. package/src/services/settings.ts +9 -3
  96. package/src/services/templates.ts +35 -16
  97. package/src/validation/stored.schema.ts +257 -0
  98. /package/frontend-vue/app/components/{Editor.vue → TemplateEditor.vue} +0 -0
  99. /package/frontend-vue/app/components/blocks/{Button.vue → BlockButton.vue} +0 -0
  100. /package/frontend-vue/app/components/blocks/{Code.vue → BlockCode.vue} +0 -0
  101. /package/frontend-vue/app/components/blocks/{Divider.vue → BlockDivider.vue} +0 -0
  102. /package/frontend-vue/app/components/blocks/{Footer.vue → BlockFooter.vue} +0 -0
  103. /package/frontend-vue/app/components/blocks/{Heading.vue → BlockHeading.vue} +0 -0
  104. /package/frontend-vue/app/components/blocks/{Hero.vue → BlockHero.vue} +0 -0
  105. /package/frontend-vue/app/components/blocks/{List.vue → BlockList.vue} +0 -0
  106. /package/frontend-vue/app/components/blocks/{Paragraph.vue → BlockParagraph.vue} +0 -0
  107. /package/frontend-vue/app/components/blocks/{Total.vue → BlockTotal.vue} +0 -0
@@ -22,8 +22,8 @@ importers:
22
22
  version: 3.5.42(typescript@5.9.3)
23
23
  devDependencies:
24
24
  '@antelopejs/tooling-configs':
25
- specifier: ^0.3.3
26
- version: 0.3.3
25
+ specifier: '>=0.7.1 <0.8.0'
26
+ version: 0.7.1
27
27
  '@nuxt/ui':
28
28
  specifier: 4.10.0
29
29
  version: 4.10.0(9bdbb06d8a0b4accef61070adc217622)
@@ -70,8 +70,8 @@ packages:
70
70
  resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==}
71
71
  engines: {node: '>=10'}
72
72
 
73
- '@antelopejs/tooling-configs@0.3.3':
74
- resolution: {integrity: sha512-jQ09zavCF5MMQtfyd8YOqpoxV6l6bp3m9tA5iN0z0XhQpVHM5VWr53yEuQ6Mc+1tseBW85N94ruP4TEfiGg8iQ==}
73
+ '@antelopejs/tooling-configs@0.7.1':
74
+ resolution: {integrity: sha512-X/mr/nQEgmFI2dMr3ThMS7HcPaU1amsTU/RNfEnQv46+l7au8cv7unpypCZ0BggnfJHjpk2QG2CTOOllpwj3cw==}
75
75
  engines: {node: '>=22.18.0'}
76
76
  hasBin: true
77
77
  peerDependencies:
@@ -79,7 +79,7 @@ packages:
79
79
  eslint-plugin-perfectionist: '>=5.0.0'
80
80
  knip: '>=6.0.0'
81
81
  mongodb-memory-server: '>=10.0.0'
82
- oxfmt: ^0.66.0
82
+ oxfmt: ^0.66.0 || ^0.77.0
83
83
  oxlint: 1.81.0
84
84
  pixelmatch: '>=7.0.0'
85
85
  playwright-core: '>=1.56.0'
@@ -1496,6 +1496,10 @@ packages:
1496
1496
  resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
1497
1497
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1498
1498
 
1499
+ '@typescript/typescript6@6.0.2':
1500
+ resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==}
1501
+ hasBin: true
1502
+
1499
1503
  '@unhead/vue@2.1.17':
1500
1504
  resolution: {integrity: sha512-pnC8x9HLV3qQXdvWfylUEU25uhfCAy3ly9nmpQz84j9py818DRfU8jOsQ5wjdWtxyU1vX/fW2udfm4jtxUK8Bg==}
1501
1505
  peerDependencies:
@@ -2967,6 +2971,11 @@ packages:
2967
2971
  engines: {node: '>=14.17'}
2968
2972
  hasBin: true
2969
2973
 
2974
+ typescript@6.0.3:
2975
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
2976
+ engines: {node: '>=14.17'}
2977
+ hasBin: true
2978
+
2970
2979
  ufo@1.6.4:
2971
2980
  resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
2972
2981
 
@@ -3390,11 +3399,13 @@ snapshots:
3390
3399
 
3391
3400
  '@alloc/quick-lru@5.3.0': {}
3392
3401
 
3393
- '@antelopejs/tooling-configs@0.3.3':
3402
+ '@antelopejs/tooling-configs@0.7.1':
3394
3403
  dependencies:
3395
3404
  '@oxlint/plugins': 1.81.0
3405
+ '@typescript/typescript6': 6.0.2
3396
3406
  dts-bundle-generator: 9.5.1
3397
- typescript: 5.9.3
3407
+ minimatch: 10.2.6
3408
+ semver: 7.8.5
3398
3409
  yaml: 2.9.1
3399
3410
 
3400
3411
  '@antfu/install-pkg@2.0.1':
@@ -4667,6 +4678,10 @@ snapshots:
4667
4678
  '@typescript-eslint/types': 8.70.0
4668
4679
  eslint-visitor-keys: 5.0.1
4669
4680
 
4681
+ '@typescript/typescript6@6.0.2':
4682
+ dependencies:
4683
+ '@typescript/old': typescript@6.0.3
4684
+
4670
4685
  '@unhead/vue@2.1.17(vue@3.5.42(typescript@5.9.3))':
4671
4686
  dependencies:
4672
4687
  hookable: 6.1.1
@@ -6170,6 +6185,8 @@ snapshots:
6170
6185
 
6171
6186
  typescript@5.9.3: {}
6172
6187
 
6188
+ typescript@6.0.3: {}
6189
+
6173
6190
  ufo@1.6.4: {}
6174
6191
 
6175
6192
  uncrypto@0.1.3: {}
@@ -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 = realpathSync(
26
- mkdtempSync(join(tmpdir(), 'dms-mailing-boundary-')),
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')
@@ -63,7 +63,7 @@ const GLOBALS = {
63
63
  UFormField: FormFieldStub,
64
64
  UButton: ButtonStub,
65
65
  UPopover: slotStub('UPopover'),
66
- DmsEyebrow: slotStub('DmsEyebrow'),
66
+ DmsEyebrowLabel: slotStub('DmsEyebrowLabel'),
67
67
  DmsInputText: slotStub('DmsInputText'),
68
68
  }
69
69
 
@@ -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', async () => {
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', async () => {
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', async () => {
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', async () => {
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
  })
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { dmsTags, knownComponents } from './helpers/component-tags'
3
+
4
+ describe('the DMS components the layer renders', () => {
5
+ it('names only components the installed DMS or this layer registers', async () => {
6
+ const known = await knownComponents()
7
+ const unknown = (await dmsTags()).filter(
8
+ (use) => !known.has(use.split(': ')[1] ?? ''),
9
+ )
10
+ expect(unknown).toEqual([])
11
+ })
12
+ })
@@ -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/Footer.vue'
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'
@@ -24,9 +24,9 @@ const EmptyStateStub = defineComponent({
24
24
  })
25
25
 
26
26
  const GLOBALS = {
27
- DmsCard: slotStub('DmsCard'),
28
- DmsSegmented: slotStub('DmsSegmented'),
29
- DmsMeter: slotStub('DmsMeter'),
27
+ DmsCardPanel: slotStub('DmsCardPanel'),
28
+ DmsSegmentedControl: slotStub('DmsSegmentedControl'),
29
+ DmsMeterBar: slotStub('DmsMeterBar'),
30
30
  DmsEmptyState: EmptyStateStub,
31
31
  USkeleton: slotStub('USkeleton'),
32
32
  UIcon: slotStub('UIcon'),
@@ -0,0 +1,58 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import { basename, join, sep } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const LAYER_ROOT = fileURLToPath(new URL('../../', import.meta.url))
6
+ const DMS_LAYERS = fileURLToPath(
7
+ new URL(
8
+ '../../../node_modules/@antelopejs/dms/frontend-vue/layers',
9
+ import.meta.url,
10
+ ),
11
+ )
12
+ const DMS_TAG = /<\/?(Dms[A-Z][\w]*|dms-[a-z][\w-]*)(?=[\s/>])/g
13
+
14
+ async function vueFiles(root: string): Promise<string[]> {
15
+ const paths = await readdir(root, { recursive: true, encoding: 'utf8' })
16
+ return paths
17
+ .filter((path) => path.endsWith('.vue') && !path.includes('node_modules'))
18
+ .map((path) => join(root, path))
19
+ }
20
+
21
+ function pascal(tag: string): string {
22
+ return tag
23
+ .split('-')
24
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
25
+ .join('')
26
+ }
27
+
28
+ /** Names a component directory registers under `prefix`: the prefix and each file's base name, as `dms.frontend.ts` registers them. */
29
+ function registered(prefix: string, files: string[]): string[] {
30
+ return files.map((file) => `${prefix}${basename(file, '.vue')}`)
31
+ }
32
+
33
+ /** Every component the installed DMS layers and this layer's `app/components` register. */
34
+ export async function knownComponents(): Promise<Set<string>> {
35
+ const dms = await vueFiles(DMS_LAYERS)
36
+ const own = await vueFiles(join(LAYER_ROOT, 'app', 'components'))
37
+ return new Set([
38
+ ...registered(
39
+ 'Dms',
40
+ dms.filter((file) => file.includes(`${sep}app${sep}components${sep}`)),
41
+ ),
42
+ ...registered('Mailing', own),
43
+ ])
44
+ }
45
+
46
+ /** Every `Dms*` or `dms-*` tag of this layer's `.vue` files, as `file: Name`, in Pascal case. */
47
+ export async function dmsTags(): Promise<string[]> {
48
+ const files = await vueFiles(join(LAYER_ROOT, 'app'))
49
+ const sources = await Promise.all(
50
+ files.map(async (file) => ({ file, text: await readFile(file, 'utf8') })),
51
+ )
52
+ return sources.flatMap(({ file, text }) =>
53
+ Array.from(
54
+ text.matchAll(DMS_TAG),
55
+ (match) => `${file.slice(LAYER_ROOT.length)}: ${pascal(match[1] ?? '')}`,
56
+ ),
57
+ )
58
+ }
@@ -231,7 +231,7 @@ export const FORM_STUBS: Record<string, Component> = {
231
231
  DmsInputText: InputStub,
232
232
  DmsInputEmail: InputStub,
233
233
  UCheckbox: CheckboxStub,
234
- DmsCheckbox: CheckboxStub,
234
+ DmsInputCheckbox: CheckboxStub,
235
235
  DmsInputTags: TagsStub,
236
236
  UFormField: slotStub('UFormField'),
237
237
  DmsFieldRow: slotStub('DmsFieldRow'),
@@ -57,7 +57,7 @@ const Gate = defineComponent({
57
57
  })
58
58
 
59
59
  const GLOBALS = {
60
- DmsCard: slotStub('DmsCard'),
60
+ DmsCardPanel: slotStub('DmsCardPanel'),
61
61
  DmsEmptyState: EmptyStateStub,
62
62
  DmsIconWell: slotStub('DmsIconWell'),
63
63
  DmsListRow: ListRowStub,
@@ -56,7 +56,7 @@ function mountRetention(): Harness {
56
56
  'onUpdate:modelValue': (value: number) => (state.days = value),
57
57
  }),
58
58
  })
59
- app.component('DmsSegmented', slotStub('DmsSegmented'))
59
+ app.component('DmsSegmentedControl', slotStub('DmsSegmentedControl'))
60
60
  app.component('UInputNumber', slotStub('UInputNumber'))
61
61
  app.component('UAlert', slotStub('UAlert'))
62
62
  app.config.warnHandler = () => undefined
@@ -65,7 +65,7 @@ function mountRetention(): Harness {
65
65
  }
66
66
 
67
67
  function segmented(host: HTMLElement): HTMLElement {
68
- return host.querySelector('[data-stub="DmsSegmented"]') as HTMLElement
68
+ return host.querySelector('[data-stub="DmsSegmentedControl"]') as HTMLElement
69
69
  }
70
70
 
71
71
  describe('RetentionInput', () => {
@@ -29,10 +29,10 @@ const STUB_NAMES = [
29
29
  'USkeleton',
30
30
  'UButton',
31
31
  'UIcon',
32
- 'DmsEyebrow',
32
+ 'DmsEyebrowLabel',
33
33
  'DmsStatusPill',
34
34
  'DmsStatGroup',
35
- 'DmsBanner',
35
+ 'DmsNoticeBanner',
36
36
  'DmsActivityItem',
37
37
  'DmsListRow',
38
38
  'DmsKeyValueList',
@@ -93,7 +93,7 @@ const globals = {
93
93
  ...FORM_STUBS,
94
94
  MailingRecipientsField: RecipientsField,
95
95
  DmsKeyValueList: KeyValueListStub,
96
- DmsBanner: slotStub('DmsBanner'),
96
+ DmsNoticeBanner: slotStub('DmsNoticeBanner'),
97
97
  }
98
98
 
99
99
  let stubs: NuxtStubs
@@ -97,9 +97,9 @@ const DropdownStub = {
97
97
  const globals = {
98
98
  ...FORM_STUBS,
99
99
  UDropdownMenu: DropdownStub,
100
- DmsBanner: slotStub('DmsBanner'),
100
+ DmsNoticeBanner: slotStub('DmsNoticeBanner'),
101
101
  MailingTemplatePreviewFrame: slotStub('MailingTemplatePreviewFrame'),
102
- DmsCard: slotStub('DmsCard'),
102
+ DmsCardPanel: slotStub('DmsCardPanel'),
103
103
  DmsEmptyState: slotStub('DmsEmptyState'),
104
104
  }
105
105
 
@@ -271,7 +271,7 @@ describe('TemplateDrawer', () => {
271
271
  )
272
272
  expect(buttons).not.toContain('dms_mailing.templates.drawer.declare')
273
273
  const banners = [
274
- ...view.host.querySelectorAll('[data-stub="DmsBanner"]'),
274
+ ...view.host.querySelectorAll('[data-stub="DmsNoticeBanner"]'),
275
275
  ].map((banner) => banner.getAttribute('title'))
276
276
  expect(banners).toContain('dms_mailing.templates.drawer.draft_declarations')
277
277
  view.unmount()
@@ -362,7 +362,7 @@ describe('TemplateDrawer', () => {
362
362
  api.templateContent.mockResolvedValue(detail(row('live')))
363
363
  const view = mount(TemplateDrawer, { templateId: 't1' }, globals)
364
364
  await flush()
365
- const segmented = view.host.querySelector('dmssegmented')
365
+ const segmented = view.host.querySelector('dmssegmentedcontrol')
366
366
  expect(segmented).not.toBeNull()
367
367
  expect(
368
368
  view.host.querySelectorAll('dmslistrow')[0]?.getAttribute('title'),
@@ -446,7 +446,7 @@ describe('StarterGallery', () => {
446
446
  api.createTemplate.mockResolvedValue({ _id: 'new-id' })
447
447
  const view = mount(StarterGallery, {}, globals)
448
448
  await flush()
449
- ;(view.host.querySelector('[data-stub="DmsCard"]') as HTMLElement).click()
449
+ ;(view.host.querySelector('[data-stub="DmsCardPanel"]') as HTMLElement).click()
450
450
  await flush()
451
451
  expect(api.createTemplate).toHaveBeenCalledWith({
452
452
  name: 'Order confirmation',
@@ -464,7 +464,7 @@ describe('StarterGallery', () => {
464
464
  api.createTemplate.mockRejectedValue({ statusCode: 409 })
465
465
  const view = mount(StarterGallery, {}, globals)
466
466
  await flush()
467
- ;(view.host.querySelector('[data-stub="DmsCard"]') as HTMLElement).click()
467
+ ;(view.host.querySelector('[data-stub="DmsCardPanel"]') as HTMLElement).click()
468
468
  await flush()
469
469
  expect(stubs.modalCalls[0]?.componentOptions).toEqual({
470
470
  initialName: 'Order confirmation',
@@ -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', async () => {
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',
@@ -361,7 +361,7 @@ describe('TemplateGallery', () => {
361
361
  const ctx = context({ items: [LIVE] })
362
362
  const view = mount(TemplateGallery, { context: ctx }, globals)
363
363
  await flush()
364
- const card = view.host.querySelector('dmscard') as HTMLElement
364
+ const card = view.host.querySelector('dmscardpanel') as HTMLElement
365
365
  card.dispatchEvent(
366
366
  new KeyboardEvent('keydown', { key: 't', bubbles: true }),
367
367
  )
@@ -426,7 +426,7 @@ describe('TemplateGallery', () => {
426
426
  const skeleton = loading.host.querySelector(
427
427
  '[data-gallery-skeleton]',
428
428
  ) as HTMLElement
429
- const skeletonCard = skeleton.querySelector('dmscard') as HTMLElement
429
+ const skeletonCard = skeleton.querySelector('dmscardpanel') as HTMLElement
430
430
  const skeletonHeading = skeleton.firstElementChild as HTMLElement
431
431
  const skeletonBody = skeletonCard.lastElementChild as HTMLElement
432
432
 
@@ -443,7 +443,7 @@ describe('TemplateGallery', () => {
443
443
  await flush()
444
444
  expect(loaded.host.querySelector('[data-gallery-skeleton]')).toBeNull()
445
445
  const section = loaded.host.querySelector('section') as HTMLElement
446
- const card = section.querySelector('article, dmscard') as HTMLElement
446
+ const card = section.querySelector('article, dmscardpanel') as HTMLElement
447
447
  const body = card.lastElementChild as HTMLElement
448
448
 
449
449
  expect(skeleton.className).toBe(section.className)
@@ -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', async () => {
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', async () => {
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', async () => {
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.9.2",
3
+ "version": "0.10.1",
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.0 <1.0.0",
50
- "@antelopejs/interface-api-util": ">=0.3.0 <1.0.0",
51
- "@antelopejs/interface-core": ">=0.4.0 <1.0.0",
52
- "@antelopejs/interface-data-api": ">=0.6.1 <1.0.0",
53
- "@antelopejs/interface-database": ">=0.3.0 <1.0.0",
54
- "@antelopejs/interface-database-decorators": ">=0.2.0 <1.0.0",
55
- "@antelopejs/interface-dms": ">=0.12.0 <1.0.0",
56
- "@antelopejs/interface-dms-mailing": ">=0.10.0 <0.11.0",
57
- "@antelopejs/interface-email": ">=0.0.6 <1.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
- "zod": "~3.24.2"
59
+ "zod": "^3.24.2"
60
60
  },
61
61
  "devDependencies": {
62
- "@antelopejs/core": "^1.21.0",
63
- "@antelopejs/dms": ">=0.19.0 <1.0.0",
64
- "@antelopejs/mongodb": "^1.6.2",
65
- "@antelopejs/tooling-configs": "^0.5.1",
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 <1.0.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
- // SAFETY: json_published_version is written only by publishSnapshot and publishedAtCreation as the
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
- ) as MailingTemplateVersion;
19
- row.publishedAt = new Date(row.publishedAt);
20
- return row;
20
+ storedVersionRowSchema,
21
+ "a version row",
22
+ );
21
23
  }
22
24
 
23
25
  export class TemplateVersionModel extends BasicDataModel(