@antelopejs/dms-mailing 0.4.0 → 0.5.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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/pages/sends.js +2 -2
  3. package/dist/pages/sends.js.map +1 -1
  4. package/dist/pages/suppressions.js +2 -2
  5. package/dist/pages/suppressions.js.map +1 -1
  6. package/dist/pages/templates.js +3 -3
  7. package/dist/pages/templates.js.map +1 -1
  8. package/frontend-vue/app/components/CategoriesInput.vue +7 -14
  9. package/frontend-vue/app/components/DuplicateModal.vue +9 -3
  10. package/frontend-vue/app/components/Editor.vue +3 -3
  11. package/frontend-vue/app/components/NewTemplateModal.vue +9 -3
  12. package/frontend-vue/app/components/OverviewGate.vue +20 -29
  13. package/frontend-vue/app/components/PageHeader.vue +8 -12
  14. package/frontend-vue/app/components/RealSendModal.vue +3 -1
  15. package/frontend-vue/app/components/RenderingModal.vue +5 -7
  16. package/frontend-vue/app/components/RetentionInput.vue +13 -23
  17. package/frontend-vue/app/components/SendDrawer.vue +5 -2
  18. package/frontend-vue/app/components/StarterGallery.vue +11 -9
  19. package/frontend-vue/app/components/TemplateCard.vue +19 -4
  20. package/frontend-vue/app/components/TemplateDrawer.vue +8 -2
  21. package/frontend-vue/app/components/TemplateGallery.vue +25 -20
  22. package/frontend-vue/app/components/TemplatePreviewFrame.vue +8 -6
  23. package/frontend-vue/app/components/TestSendModal.vue +3 -1
  24. package/frontend-vue/app/components/VersionHistory.vue +3 -3
  25. package/frontend-vue/app/components/WebhookStatus.vue +20 -17
  26. package/frontend-vue/app/components/editor/EditorWorkspace.vue +5 -0
  27. package/frontend-vue/app/composables/useEditorActions.ts +3 -1
  28. package/frontend-vue/app/composables/useEditorSession.ts +6 -4
  29. package/frontend-vue/app/composables/useMailingApi.ts +5 -58
  30. package/frontend-vue/app/composables/useMailingReads.ts +229 -0
  31. package/frontend-vue/app/composables/useOverviewSetup.ts +15 -7
  32. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -27
  33. package/frontend-vue/app/utils/gallery.ts +19 -7
  34. package/frontend-vue/app/utils/period.ts +0 -5
  35. package/frontend-vue/package.json +1 -1
  36. package/frontend-vue/tests/block-components.test.ts +0 -3
  37. package/frontend-vue/tests/categories-input.test.ts +44 -13
  38. package/frontend-vue/tests/conditions.test.ts +0 -2
  39. package/frontend-vue/tests/editor-block-list.test.ts +1 -0
  40. package/frontend-vue/tests/editor-session.test.ts +5 -0
  41. package/frontend-vue/tests/footer-block.test.ts +1 -0
  42. package/frontend-vue/tests/funnel-card.test.ts +1 -0
  43. package/frontend-vue/tests/gallery.test.ts +16 -0
  44. package/frontend-vue/tests/helpers/api-data.ts +173 -0
  45. package/frontend-vue/tests/i18n-editor.test.ts +0 -9
  46. package/frontend-vue/tests/new-template-modal.test.ts +18 -5
  47. package/frontend-vue/tests/on-demand-reads.test.ts +81 -0
  48. package/frontend-vue/tests/overview-gate.test.ts +60 -21
  49. package/frontend-vue/tests/page-reads.test.ts +157 -0
  50. package/frontend-vue/tests/period.test.ts +0 -9
  51. package/frontend-vue/tests/retention-input.test.ts +31 -9
  52. package/frontend-vue/tests/send-modals.test.ts +9 -1
  53. package/frontend-vue/tests/shortcuts.test.ts +1 -0
  54. package/frontend-vue/tests/template-drawer.test.ts +31 -14
  55. package/frontend-vue/tests/template-gallery.test.ts +46 -24
  56. package/frontend-vue/tests/variable-path.test.ts +1 -0
  57. package/frontend-vue/vitest.config.ts +4 -1
  58. package/package.json +5 -5
  59. package/src/pages/sends.ts +2 -2
  60. package/src/pages/suppressions.ts +2 -2
  61. package/src/pages/templates.ts +3 -3
  62. package/frontend-vue/tests/smoke.test.ts +0 -8
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
3
  import { computed, createApp, h, nextTick, reactive, type App } from 'vue'
3
4
  import RetentionInput from '../app/components/RetentionInput.vue'
@@ -7,15 +8,14 @@ import {
7
8
  stubNuxtGlobals,
8
9
  type NuxtStubs,
9
10
  } from './helpers/nuxt-globals'
11
+ import {
12
+ stubApiData,
13
+ stubRegionalFormat,
14
+ type ApiDataStub,
15
+ } from './helpers/api-data'
10
16
  import type { RetentionPreview } from '../app/types/mailing'
11
17
 
12
- const api = {
13
- retentionPreview: vi.fn(),
14
- }
15
-
16
- vi.mock('../app/composables/useMailingApi', () => ({
17
- useMailingApi: () => api,
18
- }))
18
+ const PREVIEW_URL = '/api/mailing/settings/retention-preview'
19
19
 
20
20
  const SAVED_DAYS = 90
21
21
  const DEBOUNCE_MS = 400
@@ -70,15 +70,37 @@ function segmented(host: HTMLElement): HTMLElement {
70
70
 
71
71
  describe('RetentionInput', () => {
72
72
  let stubs: NuxtStubs
73
+ let api: ApiDataStub
73
74
  let harness: Harness | undefined
74
75
 
75
76
  beforeEach(() => {
76
77
  vi.useFakeTimers()
77
78
  stubs = stubNuxtGlobals()
78
79
  vi.stubGlobal('computed', computed)
79
- api.retentionPreview.mockImplementation(async (days: number) =>
80
- preview(days, days < SAVED_DAYS ? 3 : 0),
80
+ stubRegionalFormat()
81
+ api = stubApiData()
82
+ api.respond(({ query }) => {
83
+ const days = Number(query.days)
84
+ return preview(days, days < SAVED_DAYS ? 3 : 0)
85
+ })
86
+ })
87
+
88
+ it('reads the saved days at once, then the days picked once they hold still', async () => {
89
+ harness = mountRetention()
90
+ await flush()
91
+ expect(api.reads).toEqual([
92
+ { url: PREVIEW_URL, query: { days: String(SAVED_DAYS) } },
93
+ ])
94
+ expect(harness.host.textContent).toContain(
95
+ 'dms_mailing.settings.retention.preview_none',
81
96
  )
97
+ harness.state.days = 30
98
+ await nextTick()
99
+ harness.state.days = 365
100
+ await nextTick()
101
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_MS)
102
+ await flush()
103
+ expect(api.reads.map((read) => read.query.days)).toEqual(['90', '365'])
82
104
  })
83
105
 
84
106
  afterEach(() => {
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
3
  import RealSendModal from '../app/components/RealSendModal.vue'
3
4
  import RecipientsField from '../app/components/RecipientsField.vue'
@@ -16,6 +17,7 @@ import {
16
17
  typeInto,
17
18
  type NuxtStubs,
18
19
  } from './helpers/nuxt-globals'
20
+ import { mailingReads, stubApiData } from './helpers/api-data'
19
21
 
20
22
  const api = {
21
23
  templateContent: vi.fn(),
@@ -24,7 +26,8 @@ const api = {
24
26
  realSend: vi.fn(),
25
27
  }
26
28
 
27
- vi.mock('../app/composables/useMailingApi', () => ({
29
+ vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
30
+ ...(await importOriginal<object>()),
28
31
  useMailingApi: () => api,
29
32
  }))
30
33
 
@@ -101,6 +104,11 @@ let stubs: NuxtStubs
101
104
 
102
105
  beforeEach(() => {
103
106
  stubs = stubNuxtGlobals('camille@acme.com')
107
+ stubApiData().respond(
108
+ mailingReads({ templateContent: api.templateContent }, ({ url }) => {
109
+ throw new Error(`unexpected read ${url}`)
110
+ }),
111
+ )
104
112
  api.preview.mockResolvedValue({
105
113
  html: '',
106
114
  subject: '20% off every chair',
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { describe, expect, it } from 'vitest'
2
3
  import { isTypingTarget, matchShortcut } from '../app/utils/shortcuts'
3
4
  import type { KeyStroke } from '../app/utils/shortcuts'
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
3
  import DuplicateModal from '../app/components/DuplicateModal.vue'
3
4
  import StarterGallery from '../app/components/StarterGallery.vue'
@@ -16,6 +17,7 @@ import {
16
17
  typeInto,
17
18
  type NuxtStubs,
18
19
  } from './helpers/nuxt-globals'
20
+ import { mailingReads, stubApiData } from './helpers/api-data'
19
21
 
20
22
  const api = {
21
23
  templateContent: vi.fn(),
@@ -28,12 +30,12 @@ const api = {
28
30
  listTemplates: vi.fn(),
29
31
  duplicate: vi.fn(),
30
32
  starters: vi.fn(),
31
- listCategories: vi.fn(),
32
33
  createTemplate: vi.fn(),
33
34
  saveTemplateDetails: vi.fn(),
34
35
  }
35
36
 
36
- vi.mock('../app/composables/useMailingApi', () => ({
37
+ vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
38
+ ...(await importOriginal<object>()),
37
39
  useMailingApi: () => api,
38
40
  }))
39
41
 
@@ -107,9 +109,27 @@ const globals = {
107
109
 
108
110
  let stubs: NuxtStubs
109
111
 
112
+ let starters: unknown[] = []
113
+
110
114
  beforeEach(() => {
111
115
  stubs = stubNuxtGlobals()
112
116
  menuGroups = []
117
+ starters = []
118
+ stubApiData().respond(
119
+ mailingReads(
120
+ {
121
+ templateContent: api.templateContent,
122
+ performance: api.performance,
123
+ listTemplates: api.listTemplates,
124
+ },
125
+ ({ url }) => {
126
+ if (url === '/api/mailing/templates/categories')
127
+ return { categories: [] }
128
+ if (url === '/api/mailing/templates/starters') return { starters }
129
+ throw new Error(`unexpected read ${url}`)
130
+ },
131
+ ),
132
+ )
113
133
  api.performance.mockResolvedValue({
114
134
  id: 't1',
115
135
  slug: 'order-confirmed',
@@ -402,18 +422,15 @@ describe('DuplicateModal', () => {
402
422
 
403
423
  describe('StarterGallery', () => {
404
424
  beforeEach(() => {
405
- api.starters.mockResolvedValue({
406
- starters: [
407
- {
408
- id: 'order-confirmed',
409
- name: 'Order confirmation',
410
- slug: 'order-confirmed',
411
- category: 'orders',
412
- locales: ['en'],
413
- },
414
- ],
415
- })
416
- api.listCategories.mockResolvedValue({ categories: [] })
425
+ starters = [
426
+ {
427
+ id: 'order-confirmed',
428
+ name: 'Order confirmation',
429
+ slug: 'order-confirmed',
430
+ category: 'orders',
431
+ locales: ['en'],
432
+ },
433
+ ]
417
434
  })
418
435
 
419
436
  it('creates a starter as a draft and opens it', async () => {
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { reactive } from 'vue'
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
4
  import TemplateGallery from '../app/components/TemplateGallery.vue'
@@ -11,15 +12,15 @@ import {
11
12
  type NuxtStubs,
12
13
  } from './helpers/nuxt-globals'
13
14
 
14
- const api = {
15
- listCategories: vi.fn(),
16
- templatesOverview: vi.fn(),
17
- listTemplates: vi.fn(),
18
- }
15
+ import {
16
+ stubApiData,
17
+ stubRegionalFormat,
18
+ type ApiDataStub,
19
+ } from './helpers/api-data'
19
20
 
20
- vi.mock('../app/composables/useMailingApi', () => ({
21
- useMailingApi: () => api,
22
- }))
21
+ const CATEGORIES_URL = '/api/mailing/templates/categories'
22
+ const OVERVIEW_URL = '/api/mailing/templates/overview'
23
+ const LIST_URL = '/api/mailing/tables/templates/list'
23
24
 
24
25
  const RECENT = new Date(Date.now() - 3_600_000).toISOString()
25
26
 
@@ -113,22 +114,32 @@ const globals = {
113
114
  }
114
115
 
115
116
  let stubs: NuxtStubs
117
+ let api: ApiDataStub
116
118
  let routeQuery: Record<string, unknown> = {}
119
+ let templateTotal = 3
120
+
121
+ const readsOf = (url: string) => api.reads.filter((read) => read.url === url)
117
122
 
118
123
  beforeEach(() => {
119
124
  stubs = stubNuxtGlobals()
125
+ stubRegionalFormat()
120
126
  routeQuery = {}
127
+ templateTotal = 3
121
128
  vi.stubGlobal('useDmsRoute', () => ({ query: routeQuery }))
122
- api.listCategories.mockResolvedValue({
123
- categories: [
124
- { id: 'orders', label: 'Orders', icon: 'i-ph-package' },
125
- { id: 'billing', label: 'Billing', icon: 'i-ph-file-text' },
126
- ],
127
- })
128
- api.templatesOverview.mockResolvedValue({
129
- items: [stats('live', 6812), stats('missing', 12)],
129
+ api = stubApiData()
130
+ api.respond(({ url }) => {
131
+ if (url === CATEGORIES_URL)
132
+ return {
133
+ categories: [
134
+ { id: 'orders', label: 'Orders', icon: 'i-ph-package' },
135
+ { id: 'billing', label: 'Billing', icon: 'i-ph-file-text' },
136
+ ],
137
+ }
138
+ if (url === OVERVIEW_URL)
139
+ return { items: [stats('live', 6812), stats('missing', 12)] }
140
+ if (url === LIST_URL) return { results: [LIVE], total: templateTotal }
141
+ throw new Error(`unexpected read ${url}`)
130
142
  })
131
- api.listTemplates.mockResolvedValue({ results: [], total: 3 })
132
143
  })
133
144
 
134
145
  afterEach(() => {
@@ -205,21 +216,32 @@ describe('TemplateGallery', () => {
205
216
  cleared.unmount()
206
217
  })
207
218
 
219
+ it('reads the template count alone, not the rows', async () => {
220
+ const view = mount(
221
+ TemplateGallery,
222
+ { context: context({ items: [LIVE] }) },
223
+ globals,
224
+ )
225
+ await flush()
226
+ expect(readsOf(LIST_URL)).toEqual([
227
+ { url: LIST_URL, query: { limit: '1' } },
228
+ ])
229
+ view.unmount()
230
+ })
231
+
208
232
  it('reloads the figures when the table refreshes, not when it pages', async () => {
209
- api.listTemplates.mockClear()
210
- api.templatesOverview.mockClear()
211
233
  const ctx = reactive(context({ items: [LIVE] }))
212
234
  const view = mount(TemplateGallery, { context: ctx }, globals)
213
235
  await flush()
214
- expect(api.listTemplates).toHaveBeenCalledTimes(1)
236
+ expect(readsOf(LIST_URL)).toHaveLength(1)
215
237
  ctx.items = [LIVE]
216
238
  await flush()
217
- expect(api.listTemplates).toHaveBeenCalledTimes(2)
218
- expect(api.templatesOverview).toHaveBeenCalledTimes(2)
239
+ expect(readsOf(LIST_URL)).toHaveLength(2)
240
+ expect(readsOf(OVERVIEW_URL)).toHaveLength(2)
219
241
  ctx.pagination.pageIndex = 1
220
242
  ctx.items = [MISSING_DE]
221
243
  await flush()
222
- expect(api.listTemplates).toHaveBeenCalledTimes(2)
244
+ expect(readsOf(LIST_URL)).toHaveLength(2)
223
245
  view.unmount()
224
246
  })
225
247
 
@@ -261,7 +283,7 @@ describe('TemplateGallery', () => {
261
283
  })
262
284
 
263
285
  it('shows the starters on a first run', async () => {
264
- api.listTemplates.mockResolvedValue({ results: [], total: 0 })
286
+ templateTotal = 0
265
287
  const view = mount(
266
288
  TemplateGallery,
267
289
  { context: context({ items: [] }) },
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
3
  import VariablePath from '../app/components/VariablePath.vue'
3
4
  import { mount, stubNuxtGlobals } from './helpers/nuxt-globals'
@@ -4,7 +4,10 @@ import { defineConfig } from 'vitest/config'
4
4
  export default defineConfig({
5
5
  plugins: [vue()],
6
6
  test: {
7
- environment: 'jsdom',
7
+ // Most suites need no DOM, and setting jsdom up per file is the bulk of
8
+ // the run; the files that mount components opt in with a
9
+ // `// @vitest-environment jsdom` header.
10
+ environment: 'node',
8
11
  include: ['tests/**/*.test.ts'],
9
12
  },
10
13
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-mailing",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "@antelopejs/interface-data-api": ">=0.2.1 <1.0.0",
39
39
  "@antelopejs/interface-database": ">=0.1.9 <1.0.0",
40
40
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
41
- "@antelopejs/interface-dms": ">=0.5.13 <1.0.0",
41
+ "@antelopejs/interface-dms": ">=0.7.0 <1.0.0",
42
42
  "@antelopejs/interface-dms-mailing": ">=0.7.0 <0.8.0",
43
43
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
44
44
  "node-cron": "^4.2.1",
@@ -86,9 +86,9 @@
86
86
  "knip:all": "knip",
87
87
  "release": "pnpm --filter @antelopejs/interface-dms-mailing build && pnpm --dir frontend-vue install --frozen-lockfile && release-it",
88
88
  "test": "pnpm run test:backend && pnpm run test:layer",
89
- "test:backend": "pnpm run build && ajs module test .",
89
+ "test:backend": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && ajs module test .",
90
90
  "test:layer": "cd frontend-vue && pnpm test",
91
- "test:unit": "pnpm run build && DMS_MAILING_TEST_DIR=dist/test/unit ajs module test .",
92
- "test:integration": "pnpm run build && DMS_MAILING_TEST_DIR=dist/test/integration ajs module test ."
91
+ "test:unit": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && DMS_MAILING_TEST_DIR=dist/test/unit ajs module test .",
92
+ "test:integration": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && DMS_MAILING_TEST_DIR=dist/test/integration ajs module test ."
93
93
  }
94
94
  }
@@ -122,7 +122,7 @@ const sendAgainAction = (isTest: boolean): SendRowAction => ({
122
122
  },
123
123
  target: {
124
124
  type: "api",
125
- url: `${API_BASE_PATH}/sends/{_id}/replay`,
125
+ url: `${API_BASE_PATH}/sends/{{row._id}}/replay`,
126
126
  method: "POST",
127
127
  successMessage: "$dms_mailing.sends.actions.sent_again",
128
128
  },
@@ -152,7 +152,7 @@ const suppressedAddressAction: SendRowAction = {
152
152
  },
153
153
  target: {
154
154
  type: "page",
155
- url: `${SUPPRESSIONS_PAGE}?email={suppressionAddress}`,
155
+ url: `${SUPPRESSIONS_PAGE}?email={{row.suppressionAddress}}`,
156
156
  },
157
157
  };
158
158
 
@@ -133,7 +133,7 @@ export class SuppressionsPageController extends PageController(
133
133
  showLabel: true,
134
134
  target: {
135
135
  type: "api",
136
- url: `${SUPPRESSIONS_URL}/{_id}`,
136
+ url: `${SUPPRESSIONS_URL}/{{row._id}}`,
137
137
  method: "DELETE",
138
138
  successMessage: key("actions.reactivated"),
139
139
  },
@@ -149,7 +149,7 @@ export class SuppressionsPageController extends PageController(
149
149
  label: key("actions.see_send"),
150
150
  icon: "i-ph-paper-plane-tilt",
151
151
  rule: { field: "sendId", notEquals: "" },
152
- target: { type: "page", url: `${SENDS_PATH}?record={sendId}` },
152
+ target: { type: "page", url: `${SENDS_PATH}?record={{row.sendId}}` },
153
153
  },
154
154
  ],
155
155
  },
@@ -144,7 +144,7 @@ export class TemplatesPageController extends PageController(
144
144
  label: "$dms_mailing.templates.actions.open_editor",
145
145
  icon: "i-ph-pencil-simple",
146
146
  isDefault: true,
147
- target: { type: "page", url: `${TEMPLATES_PATH}/{_id}` },
147
+ target: { type: "page", url: `${TEMPLATES_PATH}/{{row._id}}` },
148
148
  },
149
149
  {
150
150
  label: "$dms_mailing.templates.actions.details",
@@ -184,7 +184,7 @@ export class TemplatesPageController extends PageController(
184
184
  rule: { field: "status", notEquals: ARCHIVED_STATUS },
185
185
  target: {
186
186
  type: "api",
187
- url: `${API_BASE_PATH}/templates/{_id}/archive`,
187
+ url: `${API_BASE_PATH}/templates/{{row._id}}/archive`,
188
188
  method: "POST",
189
189
  successMessage: "$dms_mailing.templates.actions.archived",
190
190
  },
@@ -200,7 +200,7 @@ export class TemplatesPageController extends PageController(
200
200
  rule: { field: "status", equals: ARCHIVED_STATUS },
201
201
  target: {
202
202
  type: "api",
203
- url: `${API_BASE_PATH}/templates/{_id}/restore`,
203
+ url: `${API_BASE_PATH}/templates/{{row._id}}/restore`,
204
204
  method: "POST",
205
205
  successMessage: "$dms_mailing.templates.actions.restored",
206
206
  },
@@ -1,8 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { SEND_STATUSES } from '../app/types/mailing'
3
-
4
- describe('types', () => {
5
- it('exposes the send statuses', () => {
6
- expect(SEND_STATUSES).toContain('bounced')
7
- })
8
- })