@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.
- package/README.md +21 -4
- package/dist/crons/jobs.d.ts +1 -1
- package/dist/data/body.js +2 -1
- package/dist/data/body.js.map +1 -1
- package/dist/data/templates-table.js +4 -3
- package/dist/data/templates-table.js.map +1 -1
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.js +2 -1
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/engine/changes.js +2 -1
- package/dist/engine/changes.js.map +1 -1
- package/dist/engine/conditions.js +2 -3
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/fallback-html.js +8 -8
- package/dist/engine/fallback-html.js.map +1 -1
- package/dist/engine/formats.js +6 -6
- package/dist/engine/formats.js.map +1 -1
- package/dist/engine/paths.js +2 -3
- package/dist/engine/paths.js.map +1 -1
- package/dist/engine/system-variables.js +1 -1
- package/dist/engine/system-variables.js.map +1 -1
- package/dist/engine/tokens.js.map +1 -1
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/toolbar.js.map +1 -1
- package/dist/routes/events.js +2 -2
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/metrics.js +6 -5
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/sends.js +1 -1
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -6
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.js +4 -4
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.js +2 -1
- package/dist/services/attention.js.map +1 -1
- package/dist/services/categories.js +6 -6
- package/dist/services/categories.js.map +1 -1
- package/dist/services/checked-send.js +16 -9
- package/dist/services/checked-send.js.map +1 -1
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +11 -7
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +2 -1
- package/dist/services/localized-text.js +2 -1
- package/dist/services/localized-text.js.map +1 -1
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-import.js +7 -3
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send.js +4 -3
- package/dist/services/send.js.map +1 -1
- package/dist/services/starters.js +5 -5
- package/dist/services/starters.js.map +1 -1
- package/dist/services/template-stats.js +5 -2
- package/dist/services/template-stats.js.map +1 -1
- package/dist/services/templates.js +6 -2
- package/dist/services/templates.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +16 -16
- package/dist/validation/declarations.schema.js +2 -2
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/guards.d.ts +4 -0
- package/dist/validation/guards.js +19 -0
- package/dist/validation/guards.js.map +1 -0
- package/dist/validation/send-event-type.schema.d.ts +2 -0
- package/dist/validation/send-event-type.schema.js +11 -0
- package/dist/validation/send-event-type.schema.js.map +1 -0
- package/dist/validation/settings.schema.js +1 -1
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +19 -18
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/build/components/TemplateDrawerFooter.vue +45 -0
- package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +102 -0
- package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +110 -0
- package/frontend-vue/app/build/composables/useDrawerStats.ts +78 -0
- package/frontend-vue/app/components/DuplicateModal.vue +3 -11
- package/frontend-vue/app/components/FixAddressModal.vue +2 -1
- package/frontend-vue/app/components/NewTemplateModal.vue +3 -11
- package/frontend-vue/app/components/RealSendModal.vue +6 -17
- package/frontend-vue/app/components/RenderingModal.vue +2 -1
- package/frontend-vue/app/components/SendDrawer.vue +5 -4
- package/frontend-vue/app/components/StarterGallery.vue +3 -11
- package/frontend-vue/app/components/TemplateDrawer.vue +40 -269
- package/frontend-vue/app/components/TestSendModal.vue +7 -18
- package/frontend-vue/app/components/VersionHistory.vue +2 -7
- package/frontend-vue/app/components/WebhookStatus.vue +2 -1
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +2 -7
- package/frontend-vue/build/backend-boundary.ts +122 -0
- package/frontend-vue/dms.frontend.build.ts +7 -0
- package/frontend-vue/eslint.config.mjs +13 -0
- package/frontend-vue/package.json +4 -2
- package/frontend-vue/pnpm-lock.yaml +210 -61
- package/frontend-vue/tests/backend-boundary.test.ts +203 -0
- package/frontend-vue/tests/dnd.test.ts +1 -2
- package/frontend-vue/tests/editor-block-list.test.ts +136 -53
- package/frontend-vue/tests/helpers/auth-fetch.ts +123 -0
- package/frontend-vue/tests/helpers/nuxt-globals.ts +17 -1
- package/frontend-vue/tests/new-template-modal.test.ts +41 -6
- package/frontend-vue/tests/page-reads.test.ts +26 -6
- package/frontend-vue/tests/send-actions.test.ts +0 -1
- package/frontend-vue/tests/send-drawer.test.ts +2 -6
- package/frontend-vue/tests/send-modals.test.ts +8 -5
- package/frontend-vue/tests/sortable-list.test.ts +54 -62
- package/frontend-vue/tests/suppression.test.ts +1 -2
- package/frontend-vue/tests/template-drawer.test.ts +27 -18
- package/frontend-vue/tests/template-gallery.test.ts +7 -5
- package/frontend-vue/tests/template-preview-frame.test.ts +2 -5
- package/package.json +3 -8
- package/skills/dms-mailing/SKILL.md +3 -3
- package/skills/dms-mailing/cases.json +49 -0
- package/src/crons/jobs.ts +1 -1
- package/src/data/body.ts +2 -1
- package/src/data/templates-table.ts +12 -7
- package/src/db/models/template-versions.model.ts +5 -1
- package/src/db/models/templates.model.ts +6 -3
- package/src/engine/changes.ts +3 -2
- package/src/engine/conditions.ts +2 -4
- package/src/engine/fallback-html.ts +13 -13
- package/src/engine/formats.ts +6 -6
- package/src/engine/paths.ts +4 -3
- package/src/engine/system-variables.ts +4 -3
- package/src/engine/tokens.ts +1 -2
- package/src/pages/sends.ts +5 -3
- package/src/pages/templates.ts +1 -1
- package/src/pages/toolbar.ts +4 -2
- package/src/routes/events.ts +3 -6
- package/src/routes/metrics.ts +7 -5
- package/src/routes/sends.ts +10 -18
- package/src/routes/settings.ts +8 -9
- package/src/routes/suppressions.ts +1 -1
- package/src/routes/templates.ts +5 -5
- package/src/services/attention.ts +4 -3
- package/src/services/categories.ts +6 -6
- package/src/services/checked-send.ts +17 -19
- package/src/services/declarations.ts +3 -5
- package/src/services/events.ts +16 -13
- package/src/services/localized-text.ts +4 -2
- package/src/services/metrics.ts +2 -2
- package/src/services/notify.ts +1 -1
- package/src/services/render.ts +5 -1
- package/src/services/send-import.ts +16 -11
- package/src/services/send.ts +7 -7
- package/src/services/starters.ts +6 -6
- package/src/services/template-stats.ts +11 -4
- package/src/services/templates.ts +10 -8
- package/src/validation/declarations.schema.ts +2 -2
- package/src/validation/guards.ts +25 -0
- package/src/validation/send-event-type.schema.ts +10 -0
- package/src/validation/settings.schema.ts +7 -2
- package/src/validation/templates.schema.ts +2 -1
- package/frontend-vue/tests/i18n-editor.test.ts +0 -50
- 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(
|
|
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({
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
73
|
+
const domIds = (sortable: Sortable) =>
|
|
99
74
|
[...sortable.el.children].map((child) => child.getAttribute('data-id'))
|
|
100
|
-
|
|
101
|
-
|
|
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] =
|
|
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] =
|
|
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] =
|
|
188
|
-
expect(from!.options.group).toBe(
|
|
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] =
|
|
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
|
-
|
|
239
|
-
|
|
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 ['
|
|
243
|
-
expect(list!.options).
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 {
|
|
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) => ({
|
|
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
|
-
|
|
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
|
-
'
|
|
399
|
-
'
|
|
400
|
-
|
|
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.
|
|
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/
|
|
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.
|
|
27
|
-
0.
|
|
28
|
-
interface 0.9
|
|
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,
|