@antelopejs/dms-mailing 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +21 -4
  2. package/dist/crons/jobs.d.ts +1 -1
  3. package/dist/data/body.js +2 -1
  4. package/dist/data/body.js.map +1 -1
  5. package/dist/data/templates-table.js +4 -3
  6. package/dist/data/templates-table.js.map +1 -1
  7. package/dist/db/models/template-versions.model.js.map +1 -1
  8. package/dist/db/models/templates.model.js +2 -1
  9. package/dist/db/models/templates.model.js.map +1 -1
  10. package/dist/engine/changes.js +2 -1
  11. package/dist/engine/changes.js.map +1 -1
  12. package/dist/engine/conditions.js +2 -3
  13. package/dist/engine/conditions.js.map +1 -1
  14. package/dist/engine/fallback-html.js +8 -8
  15. package/dist/engine/fallback-html.js.map +1 -1
  16. package/dist/engine/formats.js +6 -6
  17. package/dist/engine/formats.js.map +1 -1
  18. package/dist/engine/paths.js +2 -3
  19. package/dist/engine/paths.js.map +1 -1
  20. package/dist/engine/system-variables.js +1 -1
  21. package/dist/engine/system-variables.js.map +1 -1
  22. package/dist/engine/tokens.js.map +1 -1
  23. package/dist/pages/sends.js.map +1 -1
  24. package/dist/pages/toolbar.js.map +1 -1
  25. package/dist/routes/events.js +2 -2
  26. package/dist/routes/events.js.map +1 -1
  27. package/dist/routes/metrics.js +6 -5
  28. package/dist/routes/metrics.js.map +1 -1
  29. package/dist/routes/sends.js +1 -1
  30. package/dist/routes/sends.js.map +1 -1
  31. package/dist/routes/settings.js +6 -6
  32. package/dist/routes/settings.js.map +1 -1
  33. package/dist/routes/templates.js +4 -4
  34. package/dist/routes/templates.js.map +1 -1
  35. package/dist/services/attention.js +2 -1
  36. package/dist/services/attention.js.map +1 -1
  37. package/dist/services/categories.js +6 -6
  38. package/dist/services/categories.js.map +1 -1
  39. package/dist/services/checked-send.js +16 -9
  40. package/dist/services/checked-send.js.map +1 -1
  41. package/dist/services/declarations.js.map +1 -1
  42. package/dist/services/events.js +11 -7
  43. package/dist/services/events.js.map +1 -1
  44. package/dist/services/localized-text.d.ts +2 -1
  45. package/dist/services/localized-text.js +2 -1
  46. package/dist/services/localized-text.js.map +1 -1
  47. package/dist/services/metrics.js.map +1 -1
  48. package/dist/services/render.js.map +1 -1
  49. package/dist/services/send-import.js +7 -3
  50. package/dist/services/send-import.js.map +1 -1
  51. package/dist/services/send.js +4 -3
  52. package/dist/services/send.js.map +1 -1
  53. package/dist/services/starters.js +5 -5
  54. package/dist/services/starters.js.map +1 -1
  55. package/dist/services/template-stats.js +5 -2
  56. package/dist/services/template-stats.js.map +1 -1
  57. package/dist/services/templates.js +6 -2
  58. package/dist/services/templates.js.map +1 -1
  59. package/dist/validation/declarations.schema.d.ts +16 -16
  60. package/dist/validation/declarations.schema.js +2 -2
  61. package/dist/validation/declarations.schema.js.map +1 -1
  62. package/dist/validation/guards.d.ts +4 -0
  63. package/dist/validation/guards.js +19 -0
  64. package/dist/validation/guards.js.map +1 -0
  65. package/dist/validation/send-event-type.schema.d.ts +2 -0
  66. package/dist/validation/send-event-type.schema.js +11 -0
  67. package/dist/validation/send-event-type.schema.js.map +1 -0
  68. package/dist/validation/settings.schema.js +1 -1
  69. package/dist/validation/settings.schema.js.map +1 -1
  70. package/dist/validation/templates.schema.d.ts +19 -18
  71. package/dist/validation/templates.schema.js.map +1 -1
  72. package/frontend-vue/app/build/components/TemplateDrawerFooter.vue +45 -0
  73. package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +102 -0
  74. package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +110 -0
  75. package/frontend-vue/app/build/composables/useDrawerStats.ts +78 -0
  76. package/frontend-vue/app/components/DuplicateModal.vue +3 -11
  77. package/frontend-vue/app/components/FixAddressModal.vue +2 -1
  78. package/frontend-vue/app/components/NewTemplateModal.vue +3 -11
  79. package/frontend-vue/app/components/RealSendModal.vue +6 -17
  80. package/frontend-vue/app/components/RenderingModal.vue +2 -1
  81. package/frontend-vue/app/components/SendDrawer.vue +5 -4
  82. package/frontend-vue/app/components/StarterGallery.vue +3 -11
  83. package/frontend-vue/app/components/TemplateDrawer.vue +40 -269
  84. package/frontend-vue/app/components/TestSendModal.vue +7 -18
  85. package/frontend-vue/app/components/VersionHistory.vue +2 -7
  86. package/frontend-vue/app/components/WebhookStatus.vue +2 -1
  87. package/frontend-vue/app/components/editor/EditorWorkspace.vue +2 -7
  88. package/frontend-vue/build/backend-boundary.ts +122 -0
  89. package/frontend-vue/dms.frontend.build.ts +7 -0
  90. package/frontend-vue/eslint.config.mjs +13 -0
  91. package/frontend-vue/package.json +4 -2
  92. package/frontend-vue/pnpm-lock.yaml +210 -61
  93. package/frontend-vue/tests/backend-boundary.test.ts +203 -0
  94. package/frontend-vue/tests/dnd.test.ts +1 -2
  95. package/frontend-vue/tests/editor-block-list.test.ts +136 -53
  96. package/frontend-vue/tests/helpers/auth-fetch.ts +123 -0
  97. package/frontend-vue/tests/helpers/nuxt-globals.ts +17 -1
  98. package/frontend-vue/tests/new-template-modal.test.ts +41 -6
  99. package/frontend-vue/tests/page-reads.test.ts +26 -6
  100. package/frontend-vue/tests/send-actions.test.ts +0 -1
  101. package/frontend-vue/tests/send-drawer.test.ts +2 -6
  102. package/frontend-vue/tests/send-modals.test.ts +8 -5
  103. package/frontend-vue/tests/sortable-list.test.ts +54 -62
  104. package/frontend-vue/tests/suppression.test.ts +1 -2
  105. package/frontend-vue/tests/template-drawer.test.ts +27 -18
  106. package/frontend-vue/tests/template-gallery.test.ts +7 -5
  107. package/frontend-vue/tests/template-preview-frame.test.ts +2 -5
  108. package/package.json +3 -8
  109. package/skills/dms-mailing/SKILL.md +3 -3
  110. package/skills/dms-mailing/cases.json +49 -0
  111. package/src/crons/jobs.ts +1 -1
  112. package/src/data/body.ts +2 -1
  113. package/src/data/templates-table.ts +12 -7
  114. package/src/db/models/template-versions.model.ts +5 -1
  115. package/src/db/models/templates.model.ts +6 -3
  116. package/src/engine/changes.ts +3 -2
  117. package/src/engine/conditions.ts +2 -4
  118. package/src/engine/fallback-html.ts +13 -13
  119. package/src/engine/formats.ts +6 -6
  120. package/src/engine/paths.ts +4 -3
  121. package/src/engine/system-variables.ts +4 -3
  122. package/src/engine/tokens.ts +1 -2
  123. package/src/pages/sends.ts +5 -3
  124. package/src/pages/templates.ts +1 -1
  125. package/src/pages/toolbar.ts +4 -2
  126. package/src/routes/events.ts +3 -6
  127. package/src/routes/metrics.ts +7 -5
  128. package/src/routes/sends.ts +10 -18
  129. package/src/routes/settings.ts +8 -9
  130. package/src/routes/suppressions.ts +1 -1
  131. package/src/routes/templates.ts +5 -5
  132. package/src/services/attention.ts +4 -3
  133. package/src/services/categories.ts +6 -6
  134. package/src/services/checked-send.ts +17 -19
  135. package/src/services/declarations.ts +3 -5
  136. package/src/services/events.ts +16 -13
  137. package/src/services/localized-text.ts +4 -2
  138. package/src/services/metrics.ts +2 -2
  139. package/src/services/notify.ts +1 -1
  140. package/src/services/render.ts +5 -1
  141. package/src/services/send-import.ts +16 -11
  142. package/src/services/send.ts +7 -7
  143. package/src/services/starters.ts +6 -6
  144. package/src/services/template-stats.ts +11 -4
  145. package/src/services/templates.ts +10 -8
  146. package/src/validation/declarations.schema.ts +2 -2
  147. package/src/validation/guards.ts +25 -0
  148. package/src/validation/send-event-type.schema.ts +10 -0
  149. package/src/validation/settings.schema.ts +7 -2
  150. package/src/validation/templates.schema.ts +2 -1
  151. package/frontend-vue/tests/i18n-editor.test.ts +0 -50
  152. package/frontend-vue/tests/i18n.test.ts +0 -138
@@ -11,6 +11,11 @@ import {
11
11
  type NuxtStubs,
12
12
  } from './helpers/nuxt-globals'
13
13
  import { mailingReads, stubApiData } from './helpers/api-data'
14
+ import {
15
+ mailingWrites,
16
+ stubAuthFetch,
17
+ type AuthFetchStub,
18
+ } from './helpers/auth-fetch'
14
19
 
15
20
  const api = {
16
21
  listTemplates: vi.fn(),
@@ -18,20 +23,18 @@ const api = {
18
23
  createTemplate: vi.fn(),
19
24
  }
20
25
 
21
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
22
- ...(await importOriginal<object>()),
23
- useMailingApi: () => api,
24
- }))
25
-
26
26
  const globals = {
27
27
  ...FORM_STUBS,
28
28
  MailingTemplatePreviewFrame: slotStub('MailingTemplatePreviewFrame'),
29
29
  }
30
30
 
31
31
  let stubs: NuxtStubs
32
+ let writes: AuthFetchStub
32
33
 
33
34
  beforeEach(() => {
34
35
  stubs = stubNuxtGlobals()
36
+ writes = stubAuthFetch()
37
+ writes.respond(mailingWrites({ createTemplate: api.createTemplate }))
35
38
  api.listTemplates.mockResolvedValue({
36
39
  results: [
37
40
  {
@@ -82,6 +85,38 @@ const fields = (host: HTMLElement) =>
82
85
  [...host.querySelectorAll('[data-stub="UFormField"]')] as HTMLElement[]
83
86
 
84
87
  describe('NewTemplateModal', () => {
88
+ it.each([false, true])(
89
+ 'shows one translated warning for a JSON 403, core already presented %s',
90
+ async (alreadyPresented) => {
91
+ const data = { message: 'error.read_only_demo', severity: 'warning' }
92
+ const error = {
93
+ response: new Response(JSON.stringify(data), { status: 403 }),
94
+ data,
95
+ }
96
+ api.createTemplate.mockImplementation(() => {
97
+ if (alreadyPresented)
98
+ globalThis.useApiErrorPresenter().presentError(error, {
99
+ fallbackKey: 'error.500.description',
100
+ })
101
+ return Promise.reject(error)
102
+ })
103
+ const view = mount(NewTemplateModal, { initialName: 'Refund' }, globals)
104
+ await flush()
105
+ view.host.querySelector('form')!.dispatchEvent(new Event('submit'))
106
+ await flush()
107
+ expect(stubs.toasts).toHaveLength(1)
108
+ expect(stubs.toasts[0]).toMatchObject({
109
+ title: 'Read-only demo, changes are not saved',
110
+ color: 'warning',
111
+ })
112
+ expect(stubs.navigations).toEqual([])
113
+ expect(
114
+ view.host.querySelector('[type="submit"]')?.getAttribute('loading'),
115
+ ).toBe('false')
116
+ view.unmount()
117
+ },
118
+ )
119
+
85
120
  it('derives the slug from the name until the slug is edited', async () => {
86
121
  const view = mount(NewTemplateModal, {}, globals)
87
122
  await flush()
@@ -148,7 +183,7 @@ describe('NewTemplateModal', () => {
148
183
  new Event('submit'),
149
184
  )
150
185
  await flush()
151
- expect(api.createTemplate).not.toHaveBeenCalled()
186
+ expect(writes.calls).toEqual([])
152
187
  view.unmount()
153
188
  })
154
189
 
@@ -9,15 +9,11 @@ import {
9
9
  stubRegionalFormat,
10
10
  type ApiDataStub,
11
11
  } from './helpers/api-data'
12
+ import { mailingWrites, stubAuthFetch } from './helpers/auth-fetch'
12
13
  import { flush, mount, slotStub, stubNuxtGlobals } from './helpers/nuxt-globals'
13
14
 
14
15
  const api = { webhookTest: vi.fn() }
15
16
 
16
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
17
- ...(await importOriginal<object>()),
18
- useMailingApi: () => api,
19
- }))
20
-
21
17
  const PROVIDER_URL = '/api/mailing/provider'
22
18
  const STATUS_URL = '/api/mailing/settings/webhook-status'
23
19
 
@@ -55,9 +51,9 @@ beforeEach(() => {
55
51
  stubNuxtGlobals()
56
52
  stubRegionalFormat()
57
53
  vi.stubGlobal('resolveComponent', (name: string) => slotStub(name))
58
- vi.stubGlobal('useApiError', vi.fn())
59
54
  api$ = stubApiData()
60
55
  api.webhookTest.mockReset()
56
+ stubAuthFetch().respond(mailingWrites({ webhookTest: api.webhookTest }))
61
57
  })
62
58
 
63
59
  afterEach(() => {
@@ -95,6 +91,30 @@ describe('ProviderPill', () => {
95
91
  })
96
92
 
97
93
  describe('WebhookStatus', () => {
94
+ it('shows one translated warning when a test event is refused', async () => {
95
+ const { toasts } = stubNuxtGlobals()
96
+ api$.respond(() => NEVER)
97
+ const data = { message: 'error.read_only_demo', severity: 'warning' }
98
+ api.webhookTest.mockRejectedValue({
99
+ response: new Response(JSON.stringify(data), { status: 403 }),
100
+ data,
101
+ })
102
+ const view = mount(
103
+ WebhookStatus,
104
+ {},
105
+ { UButton: ButtonStub, USkeleton: slotStub('USkeleton') },
106
+ )
107
+ await flush()
108
+ view.host.querySelector('button')?.click()
109
+ await flush()
110
+ expect(toasts).toHaveLength(1)
111
+ expect(toasts[0]).toMatchObject({
112
+ title: 'Read-only demo, changes are not saved',
113
+ color: 'warning',
114
+ })
115
+ view.unmount()
116
+ })
117
+
98
118
  it('shows the status read, then the one a test event answers', async () => {
99
119
  api$.respond(() => NEVER)
100
120
  api.webhookTest.mockResolvedValue({
@@ -75,7 +75,6 @@ describe('SEND_STATUS_TONES', () => {
75
75
  expect(SEND_STATUSES.every((status) => SEND_STATUS_TONES[status])).toBe(
76
76
  true,
77
77
  )
78
- expect(SEND_STATUS_TONES.bounced).toBe('error')
79
78
  })
80
79
  })
81
80
 
@@ -4,11 +4,7 @@ import SendDrawer from '../app/components/SendDrawer.vue'
4
4
  import type { SendRow } from '../app/types/mailing'
5
5
  import { flush, mount, slotStub, stubNuxtGlobals } from './helpers/nuxt-globals'
6
6
  import { mailingReads, stubApiData } from './helpers/api-data'
7
-
8
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
9
- ...(await importOriginal<object>()),
10
- useMailingApi: () => ({ replay: vi.fn() }),
11
- }))
7
+ import { stubAuthFetch } from './helpers/auth-fetch'
12
8
 
13
9
  const SEND: SendRow = {
14
10
  _id: '041f125a00000000',
@@ -55,7 +51,7 @@ async function render(): Promise<HTMLElement> {
55
51
  describe('send drawer layout', () => {
56
52
  beforeEach(() => {
57
53
  stubNuxtGlobals()
58
- vi.stubGlobal('useApiError', () => undefined)
54
+ stubAuthFetch()
59
55
  stubApiData().respond(
60
56
  mailingReads({ send: () => ({ send: SEND, events: [] }) }),
61
57
  )
@@ -18,6 +18,7 @@ import {
18
18
  type NuxtStubs,
19
19
  } from './helpers/nuxt-globals'
20
20
  import { mailingReads, stubApiData } from './helpers/api-data'
21
+ import { mailingWrites, stubAuthFetch } from './helpers/auth-fetch'
21
22
 
22
23
  const api = {
23
24
  templateContent: vi.fn(),
@@ -26,11 +27,6 @@ const api = {
26
27
  realSend: vi.fn(),
27
28
  }
28
29
 
29
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
30
- ...(await importOriginal<object>()),
31
- useMailingApi: () => api,
32
- }))
33
-
34
30
  const DRAFT_CONTENT = {
35
31
  locales: { en: { subject: 'Hi', preheader: '', blocks: [] } },
36
32
  }
@@ -104,6 +100,13 @@ let stubs: NuxtStubs
104
100
 
105
101
  beforeEach(() => {
106
102
  stubs = stubNuxtGlobals('camille@acme.com')
103
+ stubAuthFetch().respond(
104
+ mailingWrites({
105
+ preview: api.preview,
106
+ testSend: api.testSend,
107
+ realSend: api.realSend,
108
+ }),
109
+ )
107
110
  stubApiData().respond(
108
111
  mailingReads({ templateContent: api.templateContent }, ({ url }) => {
109
112
  throw new Error(`unexpected read ${url}`)
@@ -5,8 +5,7 @@
5
5
  * changes its array, and Vue renders the new order. The drags are replayed as
6
6
  * SortableJS reports them, DOM moves included.
7
7
  */
8
- import { readFileSync } from 'node:fs'
9
- import { resolve } from 'node:path'
8
+ import Sortable from 'sortablejs'
10
9
  import {
11
10
  createApp,
12
11
  defineComponent,
@@ -17,42 +16,10 @@ import {
17
16
  type App,
18
17
  } from 'vue'
19
18
  import { afterEach, describe, expect, it, vi } from 'vitest'
19
+ import { useSortableList } from '../app/composables/useSortableList'
20
20
 
21
21
  type Options = Record<string, unknown>
22
22
 
23
- interface FakeSortable {
24
- el: HTMLElement
25
- options: Options
26
- }
27
-
28
- const instances: FakeSortable[] = []
29
-
30
- // SortableJS needs a pointer to drag with: what the test drives is the
31
- // options the list hands it, called as SortableJS calls them.
32
- vi.mock('@vueuse/integrations/useSortable', async () => {
33
- const { onMounted, unref } = await import('vue')
34
- return {
35
- useSortable: (
36
- target: { value: HTMLElement },
37
- _list: unknown,
38
- options: Options,
39
- ) => {
40
- const sortable = { el: undefined as unknown as HTMLElement, options }
41
- onMounted(() => {
42
- sortable.el = unref(target)
43
- instances.push(sortable)
44
- })
45
- return {
46
- option: (name: string, value: unknown) => {
47
- sortable.options[name] = value
48
- },
49
- }
50
- },
51
- }
52
- })
53
-
54
- const { useSortableList } = await import('../app/composables/useSortableList')
55
-
56
23
  interface Item {
57
24
  id: string
58
25
  }
@@ -62,7 +29,6 @@ let app: App | undefined
62
29
  afterEach(() => {
63
30
  app?.unmount()
64
31
  app = undefined
65
- instances.length = 0
66
32
  })
67
33
 
68
34
  const List = defineComponent({
@@ -84,34 +50,58 @@ const List = defineComponent({
84
50
  },
85
51
  })
86
52
 
87
- function mountLists(lists: { items: Item[]; options: Options }[]) {
53
+ /**
54
+ * Mounts the lists and returns the SortableJS instance VueUse created on each
55
+ * list element. SortableJS needs a pointer to drag with: the tests call the
56
+ * handlers of its options as it calls them, DOM moves included.
57
+ */
58
+ function mountLists(lists: { items: Item[]; options: Options }[]): Sortable[] {
88
59
  const host = document.createElement('div')
89
60
  document.body.append(host)
90
61
  app = createApp({
91
62
  render: () => lists.map((list) => h(List, list)),
92
63
  })
93
64
  app.mount(host)
94
- return host
65
+ return [...host.children].map((element) => {
66
+ const sortable = element instanceof HTMLElement && Sortable.get(element)
67
+ if (!sortable) throw new Error('no SortableJS instance on a list')
68
+ return sortable
69
+ })
95
70
  }
96
71
 
97
72
  const ids = (items: Item[]) => items.map((item) => item.id)
98
- const domIds = (sortable: FakeSortable) =>
73
+ const domIds = (sortable: Sortable) =>
99
74
  [...sortable.el.children].map((child) => child.getAttribute('data-id'))
100
- const call = (sortable: FakeSortable, name: string, event: unknown) =>
101
- (sortable.options[name] as (event: unknown) => void)(event)
75
+ function call(sortable: Sortable, name: string, event: unknown): void {
76
+ const handler: unknown = Reflect.get(sortable.options, name)
77
+ if (typeof handler !== 'function') throw new Error(`no ${name} handler`)
78
+ handler(event)
79
+ }
80
+
81
+ /** Asks the group SortableJS built from the list's options, as a drag does. */
82
+ function groupCheck(
83
+ sortable: Sortable,
84
+ name: 'checkPull' | 'checkPut',
85
+ to: Sortable,
86
+ from: Sortable,
87
+ ): unknown {
88
+ const check: unknown = Reflect.get(Object(sortable.options.group), name)
89
+ if (typeof check !== 'function') throw new Error(`no ${name} in the group`)
90
+ return check(to, from)
91
+ }
102
92
 
103
93
  describe('useSortableList', () => {
104
94
  it('reorders a list in place and renders the new order', async () => {
105
95
  const items = reactive([{ id: 'a' }, { id: 'b' }, { id: 'c' }])
106
96
  const changes: unknown[] = []
107
97
  const ended = vi.fn()
108
- mountLists([
98
+ const sortables = mountLists([
109
99
  {
110
100
  items,
111
101
  options: { onChange: (c: unknown) => changes.push(c), onEnd: ended },
112
102
  },
113
103
  ])
114
- const [list] = instances
104
+ const [list] = sortables
115
105
  const item = list!.el.children[0] as HTMLElement
116
106
  call(list!, 'onStart', { item, oldIndex: 0 })
117
107
  // SortableJS has moved the element already when it reports the update.
@@ -132,7 +122,7 @@ describe('useSortableList', () => {
132
122
  const target = reactive([{ id: 'x' }])
133
123
  const sourceChanges: unknown[] = []
134
124
  const targetChanges: unknown[] = []
135
- mountLists([
125
+ const sortables = mountLists([
136
126
  {
137
127
  items: source,
138
128
  options: {
@@ -148,7 +138,7 @@ describe('useSortableList', () => {
148
138
  },
149
139
  },
150
140
  ])
151
- const [from, to] = instances
141
+ const [from, to] = sortables
152
142
  const item = from!.el.children[1] as HTMLElement
153
143
  call(from!, 'onStart', { item, oldIndex: 1 })
154
144
  to!.el.prepend(item)
@@ -173,7 +163,7 @@ describe('useSortableList', () => {
173
163
  const palette = reactive([{ id: 'tile' }])
174
164
  const canvas = reactive<Item[]>([])
175
165
  const group = { name: 'g', pull: 'clone', put: false }
176
- mountLists([
166
+ const sortables = mountLists([
177
167
  {
178
168
  items: palette,
179
169
  options: {
@@ -184,8 +174,10 @@ describe('useSortableList', () => {
184
174
  },
185
175
  { items: canvas, options: { group: { name: 'g' } } },
186
176
  ])
187
- const [from, to] = instances
188
- expect(from!.options.group).toBe(group)
177
+ const [from, to] = sortables
178
+ expect(Reflect.get(Object(from!.options.group), 'name')).toBe('g')
179
+ expect(groupCheck(from!, 'checkPull', to!, from!)).toBe('clone')
180
+ expect(groupCheck(from!, 'checkPut', from!, to!)).toBe(false)
189
181
  expect(from!.options.sort).toBe(false)
190
182
  const item = from!.el.children[0] as HTMLElement
191
183
  call(from!, 'onStart', { item, oldIndex: 0 })
@@ -210,7 +202,7 @@ describe('useSortableList', () => {
210
202
 
211
203
  it('passes the handle, the item selector and a disabled state it follows', async () => {
212
204
  const disabled = ref(false)
213
- mountLists([
205
+ const sortables = mountLists([
214
206
  {
215
207
  items: reactive([{ id: 'a' }]),
216
208
  options: {
@@ -221,7 +213,7 @@ describe('useSortableList', () => {
221
213
  },
222
214
  },
223
215
  ])
224
- const [list] = instances
216
+ const [list] = sortables
225
217
  expect(list!.options).toMatchObject({
226
218
  handle: '[data-drag-handle]',
227
219
  draggable: '>[data-drag-item]',
@@ -235,19 +227,19 @@ describe('useSortableList', () => {
235
227
  })
236
228
 
237
229
  it('leaves SortableJS its defaults for what the list does not set', () => {
238
- mountLists([{ items: reactive([{ id: 'a' }]), options: {} }])
239
- const [list] = instances
230
+ const sortables = mountLists([
231
+ { items: reactive([{ id: 'a' }]), options: {} },
232
+ ])
233
+ const [list] = sortables
234
+ const bare = new Sortable(document.createElement('div'))
240
235
  // A key present with `undefined` would replace the default: an
241
236
  // undefined `draggable` makes no child an item, and nothing drags.
242
- for (const name of ['group', 'handle', 'draggable', 'ghostClass'])
243
- expect(list!.options).not.toHaveProperty(name)
244
- })
245
-
246
- it('no longer ships vuedraggable, which bundled the Vue compiler', () => {
247
- const manifest = JSON.parse(
248
- readFileSync(resolve(__dirname, '../package.json'), 'utf8'),
249
- ) as { dependencies: Record<string, string> }
250
- expect(manifest.dependencies).not.toHaveProperty('vuedraggable')
251
- expect(manifest.dependencies).toHaveProperty('sortablejs')
237
+ for (const name of ['handle', 'draggable', 'ghostClass'] as const)
238
+ expect(list!.options[name]).toBe(bare.options[name])
239
+ expect(list!.options.draggable).toBe('>*')
240
+ expect(Reflect.get(Object(list!.options.group), 'name')).toBe(
241
+ Reflect.get(Object(bare.options.group), 'name'),
242
+ )
243
+ bare.destroy()
252
244
  })
253
245
  })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { SEND_STATUS_TONES, sendFooterActions } from '../app/utils/send-actions'
2
+ import { sendFooterActions } from '../app/utils/send-actions'
3
3
  import {
4
4
  isSuppressedStatus,
5
5
  reasonKey,
@@ -119,7 +119,6 @@ describe('a suppressed send in the drawer', () => {
119
119
  'rendering',
120
120
  'send_again',
121
121
  ])
122
- expect(SEND_STATUS_TONES.suppressed).toBe('neutral')
123
122
  })
124
123
 
125
124
  it('draws one step, without a queued one nor a raw reason', () => {
@@ -18,6 +18,7 @@ import {
18
18
  type NuxtStubs,
19
19
  } from './helpers/nuxt-globals'
20
20
  import { mailingReads, stubApiData } from './helpers/api-data'
21
+ import { mailingWrites, stubAuthFetch } from './helpers/auth-fetch'
21
22
 
22
23
  const api = {
23
24
  templateContent: vi.fn(),
@@ -34,11 +35,6 @@ const api = {
34
35
  saveTemplateDetails: vi.fn(),
35
36
  }
36
37
 
37
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
38
- ...(await importOriginal<object>()),
39
- useMailingApi: () => api,
40
- }))
41
-
42
38
  function row(
43
39
  status: TemplateStatus,
44
40
  overrides: Partial<TemplateRow> = {},
@@ -113,6 +109,18 @@ let starters: unknown[] = []
113
109
 
114
110
  beforeEach(() => {
115
111
  stubs = stubNuxtGlobals()
112
+ stubAuthFetch().respond(
113
+ mailingWrites({
114
+ publish: api.publish,
115
+ unpublish: api.unpublish,
116
+ archive: api.archive,
117
+ restore: api.restore,
118
+ saveVariables: api.saveVariables,
119
+ duplicate: api.duplicate,
120
+ createTemplate: api.createTemplate,
121
+ saveTemplateDetails: api.saveTemplateDetails,
122
+ }),
123
+ )
116
124
  menuGroups = []
117
125
  starters = []
118
126
  stubApiData().respond(
@@ -145,7 +153,10 @@ beforeEach(() => {
145
153
  api.archive.mockResolvedValue({ status: 'archived' })
146
154
  api.restore.mockResolvedValue({ status: 'draft' })
147
155
  api.saveVariables.mockImplementation(
148
- async (_id: string, variables: unknown) => ({ variables, revision: 4 }),
156
+ async (_id: string, body: { variables: unknown }) => ({
157
+ variables: body.variables,
158
+ revision: 4,
159
+ }),
149
160
  )
150
161
  })
151
162
 
@@ -186,7 +197,7 @@ describe('TemplateDrawer', () => {
186
197
  expect(stubs.confirmations[0]?.title).toContain('lifecycle.publish_title')
187
198
  const onConfirm = stubs.confirmations[0]?.onConfirm as () => Promise<void>
188
199
  await onConfirm()
189
- expect(api.publish).toHaveBeenCalledWith('t1')
200
+ expect(api.publish).toHaveBeenCalledWith('t1', { revision: undefined })
190
201
  await flush()
191
202
  expect(onChanged).toHaveBeenCalled()
192
203
  expect(api.templateContent).toHaveBeenCalledTimes(2)
@@ -205,7 +216,7 @@ describe('TemplateDrawer', () => {
205
216
  menuGroups.flat()[0]!.onSelect()
206
217
  await flush()
207
218
  expect(stubs.confirmations).toEqual([])
208
- expect(api.restore).toHaveBeenCalledWith('t1')
219
+ expect(api.restore).toHaveBeenCalledWith('t1', {})
209
220
  expect(onSuccessCallback).toHaveBeenCalled()
210
221
  view.unmount()
211
222
  })
@@ -227,13 +238,12 @@ describe('TemplateDrawer', () => {
227
238
  ) as HTMLElement
228
239
  declare.click()
229
240
  await flush()
230
- expect(api.saveVariables).toHaveBeenCalledWith(
231
- 't1',
232
- expect.arrayContaining([
241
+ expect(api.saveVariables).toHaveBeenCalledWith('t1', {
242
+ variables: expect.arrayContaining([
233
243
  { path: 'order.trackingUrl', type: 'string', required: false },
234
244
  ]),
235
- 3,
236
- )
245
+ revision: 3,
246
+ })
237
247
  view.unmount()
238
248
  })
239
249
 
@@ -394,11 +404,10 @@ describe('DuplicateModal', () => {
394
404
  new Event('submit'),
395
405
  )
396
406
  await flush()
397
- expect(api.duplicate).toHaveBeenCalledWith(
398
- 't1',
399
- 'order-confirmation-b2b',
400
- 'Order confirmation (B2B)',
401
- )
407
+ expect(api.duplicate).toHaveBeenCalledWith('t1', {
408
+ slug: 'order-confirmation-b2b',
409
+ name: 'Order confirmation (B2B)',
410
+ })
402
411
  expect(stubs.navigations).toEqual([
403
412
  { path: '/modules/mailing/templates/copy-id', query: {} },
404
413
  ])
@@ -16,16 +16,12 @@ import {
16
16
  stubRegionalFormat,
17
17
  type ApiDataStub,
18
18
  } from './helpers/api-data'
19
+ import { stubAuthFetch, type AuthFetchStub } from './helpers/auth-fetch'
19
20
 
20
21
  const CATEGORIES_URL = '/api/mailing/templates/categories'
21
22
  const OVERVIEW_URL = '/api/mailing/templates/overview'
22
23
  const LIST_URL = '/api/mailing/tables/templates/list'
23
24
 
24
- vi.mock('../app/components/TemplatePreviewFrame.vue', async () => {
25
- const { slotStub } = await import('./helpers/nuxt-globals')
26
- return { default: slotStub('MailingTemplatePreviewFrame') }
27
- })
28
-
29
25
  const RECENT = new Date(Date.now() - 3_600_000).toISOString()
30
26
 
31
27
  function row(overrides: Partial<TemplateRow>): TemplateRow {
@@ -116,6 +112,7 @@ const globals = {
116
112
  }
117
113
 
118
114
  let stubs: NuxtStubs
115
+ let writes: AuthFetchStub
119
116
  let api: ApiDataStub
120
117
  let routeQuery: Record<string, unknown> = {}
121
118
  let templateTotal = 3
@@ -125,6 +122,7 @@ const readsOf = (url: string) => api.reads.filter((read) => read.url === url)
125
122
  beforeEach(() => {
126
123
  stubs = stubNuxtGlobals()
127
124
  stubRegionalFormat()
125
+ writes = stubAuthFetch()
128
126
  routeQuery = {}
129
127
  templateTotal = 3
130
128
  vi.stubGlobal('useDmsRoute', () => ({ query: routeQuery }))
@@ -169,6 +167,10 @@ describe('TemplateGallery', () => {
169
167
  'Shipping update',
170
168
  'Delivery delayed',
171
169
  ])
170
+ expect(
171
+ view.host.querySelectorAll('[data-preview-placeholder]'),
172
+ ).toHaveLength(3)
173
+ expect(writes.calls).toEqual([])
172
174
  view.unmount()
173
175
  })
174
176
 
@@ -3,17 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
3
  import TemplatePreviewFrame from '../app/components/TemplatePreviewFrame.vue'
4
4
  import { flush, mount, stubNuxtGlobals } from './helpers/nuxt-globals'
5
5
  import { stubApiData } from './helpers/api-data'
6
+ import { mailingWrites, stubAuthFetch } from './helpers/auth-fetch'
6
7
 
7
8
  /** Past the frame's debounce before it reads. */
8
9
  const DEBOUNCE_WAIT_MS = 400
9
10
 
10
11
  const api = { preview: vi.fn() }
11
12
 
12
- vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
13
- ...(await importOriginal<object>()),
14
- useMailingApi: () => api,
15
- }))
16
-
17
13
  const skeleton = (host: HTMLElement) => host.querySelector('uskeleton')
18
14
  const placeholder = (host: HTMLElement) =>
19
15
  host.querySelector('[data-preview-placeholder]')
@@ -30,6 +26,7 @@ beforeEach(() => {
30
26
  stubNuxtGlobals()
31
27
  stubApiData()
32
28
  api.preview.mockReset()
29
+ stubAuthFetch().respond(mailingWrites({ preview: api.preview }))
33
30
  })
34
31
 
35
32
  afterEach(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-mailing",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",
@@ -60,7 +60,8 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@antelopejs/core": ">=1.17.1 <2",
63
- "@antelopejs/tooling-configs": ">=0.0.9 <1.0.0",
63
+ "@antelopejs/dms": ">=0.13.1 <1.0.0",
64
+ "@antelopejs/tooling-configs": "^0.2.12",
64
65
  "@types/chai": "^4.3.20",
65
66
  "@types/mocha": "^10",
66
67
  "@types/node": "^24.0.0",
@@ -69,9 +70,6 @@
69
70
  "chai": "^4.5.0",
70
71
  "knip": "^6.34.0",
71
72
  "mongodb-memory-server-core": "^11.0.1",
72
- "oxfmt": "^0.66.0",
73
- "oxlint": "1.81.0",
74
- "oxlint-tsgolint": "^7.0.2001",
75
73
  "release-it": "^19.2.4",
76
74
  "release-it-changelogen": "^0.1.0",
77
75
  "rimraf": "^6.1.3",
@@ -96,9 +94,6 @@
96
94
  "dev": "ajs project dev -w -p playground",
97
95
  "dev:frontend": "cd playground && pnpm frontend:dev",
98
96
  "frontend:dev": "cd playground && pnpm frontend:dev",
99
- "lint": "oxlint && oxfmt --check . && cd frontend-vue && pnpm lint && antelopejs-check-interface-ranges",
100
- "lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && cd frontend-vue && pnpm lint:fix",
101
- "format": "oxfmt . && cd frontend-vue && pnpm format",
102
97
  "typecheck": "tsc --noEmit",
103
98
  "knip": "knip --dependencies",
104
99
  "knip:all": "knip",
@@ -23,9 +23,9 @@ https://dms.antelopejs.com/docs/modules/mailing (sending from code and the webho
23
23
 
24
24
  - `ajs project modules add @antelopejs/dms-mailing`, plus a module implementing `@antelopejs/interface-email`
25
25
  (`@antelopejs/nodemailer`, `config: { ethereal: true }` in dev).
26
- - Mailing 0.6 requires `@antelopejs/dms` >=0.9 (interface-dms 0.7, renderer `@antelopejs/dms-frontend` 0.6);
27
- 0.4 is the DMS 0.8 line. Mailing 0.6 implements `@antelopejs/interface-dms-mailing` `>=0.8.0 <0.9.0`, adding declaration and import calls;
28
- interface 0.9 adds `DeclareSender`; `SendTemplate` / `RecordEmailEvent` callers do not change.
26
+ - Mailing 0.7.1 requires `@antelopejs/dms` `>=0.11.0 <1.0.0`, so DMS 0.11 or later (interface-dms 0.9, renderer `@antelopejs/dms-frontend` `>=0.9.1 <0.10.0`);
27
+ 0.7.0 is the DMS 0.10 line. It implements `@antelopejs/interface-dms-mailing` `>=0.9.0 <0.10.0`: declaration and import calls,
28
+ and `DeclareSender` since interface 0.9; `SendTemplate` / `RecordEmailEvent` callers do not change.
29
29
  - Module option: `useHtmlRender` (default `true`, DMS html-render chain; `false` = built-in standalone HTML).
30
30
  - Set the DMS `clientBaseUrl`: the webhook URL and the e-mail logo are built on it.
31
31
  - Per-workspace settings (sender, missing-variable guard, fallback locale, webhook secret, retention,