@antelopejs/dms-mailing 0.5.0 → 0.5.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 (47) hide show
  1. package/README.md +61 -339
  2. package/dist/data/templates-table.js +2 -10
  3. package/dist/data/templates-table.js.map +1 -1
  4. package/dist/db/models/index.d.ts +1 -0
  5. package/dist/db/models/index.js +1 -0
  6. package/dist/db/models/index.js.map +1 -1
  7. package/dist/db/models/send-events.model.d.ts +1 -0
  8. package/dist/db/models/sends.model.d.ts +1 -0
  9. package/dist/db/models/settings.model.d.ts +1 -0
  10. package/dist/db/models/suppressions.model.d.ts +1 -0
  11. package/dist/db/models/template-slugs.model.d.ts +27 -0
  12. package/dist/db/models/template-slugs.model.js +43 -0
  13. package/dist/db/models/template-slugs.model.js.map +1 -0
  14. package/dist/db/models/template-versions.model.d.ts +1 -0
  15. package/dist/db/models/templates.model.d.ts +1 -0
  16. package/dist/db/models/webhook-status.model.d.ts +1 -0
  17. package/dist/db/tables/index.d.ts +1 -0
  18. package/dist/db/tables/index.js +1 -0
  19. package/dist/db/tables/index.js.map +1 -1
  20. package/dist/db/tables/template-slugs.table.d.ts +7 -0
  21. package/dist/db/tables/template-slugs.table.js +35 -0
  22. package/dist/db/tables/template-slugs.table.js.map +1 -0
  23. package/dist/routes/templates.js +1 -0
  24. package/dist/routes/templates.js.map +1 -1
  25. package/dist/services/templates.d.ts +2 -0
  26. package/dist/services/templates.js +26 -0
  27. package/dist/services/templates.js.map +1 -1
  28. package/frontend-vue/app/components/CategoriesInput.vue +12 -11
  29. package/frontend-vue/app/components/editor/EditorBlockList.vue +33 -30
  30. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +14 -14
  31. package/frontend-vue/app/composables/useSortableList.ts +159 -0
  32. package/frontend-vue/app/utils/blocks/dnd.ts +2 -0
  33. package/frontend-vue/i18n/locales/mailing-en-GB.json +1 -1
  34. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1 -1
  35. package/frontend-vue/package.json +4 -3
  36. package/frontend-vue/pnpm-lock.yaml +6 -13
  37. package/frontend-vue/tests/editor-block-list.test.ts +46 -6
  38. package/frontend-vue/tests/sortable-list.test.ts +253 -0
  39. package/package.json +31 -14
  40. package/skills/dms-mailing/SKILL.md +63 -0
  41. package/src/data/templates-table.ts +7 -27
  42. package/src/db/models/index.ts +1 -0
  43. package/src/db/models/template-slugs.model.ts +61 -0
  44. package/src/db/tables/index.ts +1 -0
  45. package/src/db/tables/template-slugs.table.ts +29 -0
  46. package/src/routes/templates.ts +2 -0
  47. package/src/services/templates.ts +49 -0
@@ -0,0 +1,159 @@
1
+ import { useSortable } from '@vueuse/integrations/useSortable'
2
+ import { toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue'
3
+
4
+ /** What a drop did to a list, as vuedraggable's `change` event said it. */
5
+ export interface SortableListChange<Item> {
6
+ added?: { element: Item; newIndex: number }
7
+ removed?: { element: Item; oldIndex: number }
8
+ moved?: { element: Item; oldIndex: number; newIndex: number }
9
+ }
10
+
11
+ export interface SortableGroup {
12
+ name: string
13
+ /** `'clone'`: the list keeps its items and hands a copy to the other list. */
14
+ pull?: boolean | 'clone'
15
+ /** `false`: no item of another list lands here. */
16
+ put?: boolean
17
+ }
18
+
19
+ export interface SortableListOptions<Item> {
20
+ /** Lists sharing a group name exchange items. */
21
+ group?: string | SortableGroup
22
+ /** Read again whenever it changes. */
23
+ disabled?: MaybeRefOrGetter<boolean>
24
+ /** Only an item grabbed by an element matching this selector moves. */
25
+ handle?: string
26
+ /**
27
+ * The children that are items, when the list element holds others (a
28
+ * footer): the indexes SortableJS reports count these only.
29
+ */
30
+ draggable?: string
31
+ /** `false`: the items cannot be reordered within the list. */
32
+ sort?: boolean
33
+ /** Class of the slot the dragged item will land in. */
34
+ ghostClass?: string
35
+ /** What another list receives in place of the dragged item. */
36
+ clone?: (item: Item) => unknown
37
+ onChange?: (change: SortableListChange<Item>) => void
38
+ /** After every drag, whatever it changed. */
39
+ onEnd?: () => void
40
+ }
41
+
42
+ /** What the list reads of SortableJS's events. */
43
+ interface SortableEvent {
44
+ item: HTMLElement
45
+ clone: HTMLElement
46
+ oldIndex?: number
47
+ newIndex?: number
48
+ pullMode?: boolean | 'clone'
49
+ }
50
+
51
+ /**
52
+ * The options the list sets. SortableJS keeps its default only for an option
53
+ * absent from the object: a key set to `undefined` replaces it (an undefined
54
+ * `draggable` makes no child an item, so nothing drags).
55
+ */
56
+ function defined<Options extends object>(options: Options): Partial<Options> {
57
+ return Object.fromEntries(
58
+ Object.entries(options).filter(([, value]) => value !== undefined),
59
+ ) as Partial<Options>
60
+ }
61
+
62
+ /**
63
+ * The item being dragged, by group. A drop on another list only names the
64
+ * element, which belongs to the list it left: the list it lands in reads the
65
+ * item the other one stored when the drag started.
66
+ */
67
+ const dragged = new Map<string, unknown>()
68
+
69
+ /**
70
+ * Lets the user reorder the items of `list` rendered in `root`, or move them
71
+ * to another list of the same group: SortableJS, through VueUse's
72
+ * `useSortable`.
73
+ *
74
+ * SortableJS moves the dragged element in the DOM itself. Vue owns these
75
+ * elements, so each drop puts the element back where Vue rendered it and
76
+ * changes `list` in place instead, from which Vue renders the new order.
77
+ */
78
+ export function useSortableList<Item>(
79
+ root: Ref<HTMLElement | null>,
80
+ list: () => Item[],
81
+ options: SortableListOptions<Item> = {},
82
+ ): void {
83
+ const groupName =
84
+ typeof options.group === 'string'
85
+ ? options.group
86
+ : (options.group?.name ?? '')
87
+
88
+ /**
89
+ * Where the dragged element stood: next to the node that followed it, not
90
+ * at an index, as Vue removes an item by the markers around it.
91
+ */
92
+ let origin: { parent: Node; next: Node | null } | undefined
93
+
94
+ function restore(item: HTMLElement): void {
95
+ if (origin) origin.parent.insertBefore(item, origin.next)
96
+ }
97
+
98
+ const { option } = useSortable(root, list, {
99
+ ...defined({
100
+ group: options.group,
101
+ handle: options.handle,
102
+ draggable: options.draggable,
103
+ ghostClass: options.ghostClass,
104
+ }),
105
+ disabled: toValue(options.disabled) ?? false,
106
+ sort: options.sort ?? true,
107
+ onStart: (event: SortableEvent) => {
108
+ const { parentNode, nextSibling } = event.item
109
+ origin = parentNode
110
+ ? { parent: parentNode, next: nextSibling }
111
+ : undefined
112
+ const element = list()[event.oldIndex ?? -1]
113
+ if (element === undefined) return
114
+ dragged.set(groupName, options.clone ? options.clone(element) : element)
115
+ },
116
+ onUpdate: (event: SortableEvent) => {
117
+ const { oldIndex, newIndex } = event
118
+ if (oldIndex === undefined || newIndex === undefined) return
119
+ restore(event.item)
120
+ const items = list()
121
+ const [element] = items.splice(oldIndex, 1)
122
+ if (element === undefined) return
123
+ items.splice(newIndex, 0, element)
124
+ options.onChange?.({ moved: { element, oldIndex, newIndex } })
125
+ },
126
+ onAdd: (event: SortableEvent) => {
127
+ const element = dragged.get(groupName) as Item | undefined
128
+ const { newIndex } = event
129
+ if (element === undefined || newIndex === undefined) return
130
+ // The element goes back to the list it left, below.
131
+ event.item.remove()
132
+ list().splice(newIndex, 0, element)
133
+ options.onChange?.({ added: { element, newIndex } })
134
+ },
135
+ onRemove: (event: SortableEvent) => {
136
+ restore(event.item)
137
+ // A copy left: the list keeps its item, and SortableJS's stand-in goes.
138
+ if (event.pullMode === 'clone') {
139
+ event.clone.remove()
140
+ return
141
+ }
142
+ const { oldIndex } = event
143
+ if (oldIndex === undefined) return
144
+ const [element] = list().splice(oldIndex, 1)
145
+ if (element === undefined) return
146
+ options.onChange?.({ removed: { element, oldIndex } })
147
+ },
148
+ onEnd: () => {
149
+ origin = undefined
150
+ dragged.delete(groupName)
151
+ options.onEnd?.()
152
+ },
153
+ })
154
+
155
+ watch(
156
+ () => toValue(options.disabled) ?? false,
157
+ (disabled) => option('disabled', disabled),
158
+ )
159
+ }
@@ -9,6 +9,8 @@ export const PALETTE_GROUP = {
9
9
  put: false,
10
10
  }
11
11
  export const DRAG_HANDLE = '.mailing-drag-handle'
12
+ /** The blocks of a list, not the insert point that ends it. */
13
+ export const DRAG_ITEM = '>[data-drag-item]'
12
14
 
13
15
  /** The block a tile dropped on the canvas becomes, shaped by its template. */
14
16
  export function cloneFromPalette(
@@ -804,7 +804,7 @@
804
804
  },
805
805
  "webhookUrl": {
806
806
  "label": "Webhook URL",
807
- "description": "Your provider, or the relay that translates its events, posts them here with the secret below. The module README describes the format."
807
+ "description": "Your provider, or the relay that translates its events, posts them here with the secret below. The module documentation describes the format."
808
808
  },
809
809
  "webhookStatus": {
810
810
  "label": "Webhook status",
@@ -804,7 +804,7 @@
804
804
  },
805
805
  "webhookUrl": {
806
806
  "label": "URL du webhook",
807
- "description": "Votre fournisseur, ou le relais qui traduit ses événements, les envoie ici avec le secret ci-dessous. Le README du module décrit le format."
807
+ "description": "Votre fournisseur, ou le relais qui traduit ses événements, les envoie ici avec le secret ci-dessous. La documentation du module décrit le format."
808
808
  },
809
809
  "webhookStatus": {
810
810
  "label": "État du webhook",
@@ -6,7 +6,7 @@
6
6
  "license": "Apache-2.0",
7
7
  "packageManager": "pnpm@10.2.0",
8
8
  "engines": {
9
- "@antelopejs/dms-frontend": ">=0.6.0 <0.7.0"
9
+ "@antelopejs/dms-frontend": ">=0.6.3 <0.7.0"
10
10
  },
11
11
  "scripts": {
12
12
  "format": "prettier --write .",
@@ -16,8 +16,9 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@vueuse/core": "^14.3.0",
19
- "vue": "^3.5.18",
20
- "vuedraggable": "^4.1.0"
19
+ "@vueuse/integrations": "^14.3.0",
20
+ "sortablejs": "^1.14.0",
21
+ "vue": "^3.5.18"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@nuxt/ui": "4.10.0",
@@ -11,12 +11,15 @@ importers:
11
11
  '@vueuse/core':
12
12
  specifier: ^14.3.0
13
13
  version: 14.4.0(vue@3.5.42(typescript@5.9.3))
14
+ '@vueuse/integrations':
15
+ specifier: ^14.3.0
16
+ version: 14.4.0(fuse.js@7.5.0)(sortablejs@1.14.0)(vue@3.5.42(typescript@5.9.3))
17
+ sortablejs:
18
+ specifier: ^1.14.0
19
+ version: 1.14.0
14
20
  vue:
15
21
  specifier: ^3.5.18
16
22
  version: 3.5.42(typescript@5.9.3)
17
- vuedraggable:
18
- specifier: ^4.1.0
19
- version: 4.1.0(vue@3.5.42(typescript@5.9.3))
20
23
  devDependencies:
21
24
  '@nuxt/ui':
22
25
  specifier: 4.10.0
@@ -3154,11 +3157,6 @@ packages:
3154
3157
  typescript:
3155
3158
  optional: true
3156
3159
 
3157
- vuedraggable@4.1.0:
3158
- resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
3159
- peerDependencies:
3160
- vue: ^3.0.1
3161
-
3162
3160
  w3c-keyname@2.2.8:
3163
3161
  resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
3164
3162
 
@@ -6245,11 +6243,6 @@ snapshots:
6245
6243
  optionalDependencies:
6246
6244
  typescript: 5.9.3
6247
6245
 
6248
- vuedraggable@4.1.0(vue@3.5.42(typescript@5.9.3)):
6249
- dependencies:
6250
- sortablejs: 1.14.0
6251
- vue: 3.5.42(typescript@5.9.3)
6252
-
6253
6246
  w3c-keyname@2.2.8: {}
6254
6247
 
6255
6248
  w3c-xmlserializer@5.0.0:
@@ -12,6 +12,24 @@ import EditorBlockList from '../app/components/editor/EditorBlockList.vue'
12
12
  import type { Block } from '../app/types/mailing'
13
13
 
14
14
  const editor = vi.hoisted(() => ({ touch: vi.fn(), select: vi.fn() }))
15
+ const sortables = vi.hoisted(
16
+ () => [] as { el: HTMLElement; options: Record<string, unknown> }[],
17
+ )
18
+
19
+ // What SortableJS would be handed: the test calls it as SortableJS does.
20
+ vi.mock('@vueuse/integrations/useSortable', async () => {
21
+ const { onMounted, unref } = await import('vue')
22
+ return {
23
+ useSortable: (
24
+ target: { value: HTMLElement },
25
+ _list: unknown,
26
+ options: Record<string, unknown>,
27
+ ) => {
28
+ onMounted(() => sortables.push({ el: unref(target), options }))
29
+ return { option: () => {} }
30
+ },
31
+ }
32
+ })
15
33
 
16
34
  vi.mock('../app/composables/useEditorContext', () => ({
17
35
  useEditorContext: () => ({ editor }),
@@ -60,14 +78,36 @@ describe('editor draggable items', () => {
60
78
  })
61
79
  it('selects a block dropped into the list', async () => {
62
80
  vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
63
- const list = reactive<Block[]>([])
64
- const app = createApp(EditorBlockList, { list })
81
+ sortables.length = 0
82
+ const source = reactive<Block[]>([
83
+ { id: 'dropped', type: 'divider', visibleIf: null },
84
+ ])
85
+ const target = reactive<Block[]>([])
86
+ const app = createApp({
87
+ render: () => [
88
+ h(EditorBlockList, { list: source }),
89
+ h(EditorBlockList, { list: target }),
90
+ ],
91
+ })
65
92
  app.mount(document.createElement('div'))
66
93
  try {
67
- const draggable = app._instance?.subTree.props as Record<string, unknown>
68
- const onChange = draggable.onChange as (change: unknown) => void
69
- onChange({ added: { element: { id: 'dropped' }, newIndex: 0 } })
70
- onChange({ moved: { element: { id: 'other' } } })
94
+ const [from, to] = sortables
95
+ expect(from!.options).toMatchObject({
96
+ group: { name: 'mailing-blocks' },
97
+ handle: '.mailing-drag-handle',
98
+ draggable: '>[data-drag-item]',
99
+ })
100
+ const item = from!.el.querySelector('[data-drag-item]') as HTMLElement
101
+ const handler = (sortable: typeof from, name: string) =>
102
+ sortable!.options[name] as (event: unknown) => void
103
+ handler(from, 'onStart')({ item, oldIndex: 0 })
104
+ to!.el.prepend(item)
105
+ handler(to, 'onAdd')({ item, newIndex: 0 })
106
+ handler(from, 'onRemove')({ item, oldIndex: 0 })
107
+ handler(from, 'onEnd')({})
108
+ await nextTick()
109
+ expect(source).toEqual([])
110
+ expect(target.map((block) => block.id)).toEqual(['dropped'])
71
111
  expect(editor.touch).toHaveBeenCalledTimes(2)
72
112
  expect(editor.select).toHaveBeenCalledExactlyOnceWith('dropped')
73
113
  } finally {
@@ -0,0 +1,253 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The drag and drop of the categories, the canvas blocks and the palette:
4
+ * SortableJS moves the dragged element in the DOM, the list puts it back and
5
+ * changes its array, and Vue renders the new order. The drags are replayed as
6
+ * SortableJS reports them, DOM moves included.
7
+ */
8
+ import { readFileSync } from 'node:fs'
9
+ import { resolve } from 'node:path'
10
+ import {
11
+ createApp,
12
+ defineComponent,
13
+ h,
14
+ nextTick,
15
+ reactive,
16
+ ref,
17
+ type App,
18
+ } from 'vue'
19
+ import { afterEach, describe, expect, it, vi } from 'vitest'
20
+
21
+ type Options = Record<string, unknown>
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
+ interface Item {
57
+ id: string
58
+ }
59
+
60
+ let app: App | undefined
61
+
62
+ afterEach(() => {
63
+ app?.unmount()
64
+ app = undefined
65
+ instances.length = 0
66
+ })
67
+
68
+ const List = defineComponent({
69
+ props: {
70
+ items: { type: Array as () => Item[], required: true },
71
+ options: { type: Object, default: () => ({}) },
72
+ },
73
+ setup(props) {
74
+ const root = ref<HTMLElement | null>(null)
75
+ useSortableList(root, () => props.items, props.options)
76
+ return () =>
77
+ h(
78
+ 'div',
79
+ { ref: root },
80
+ props.items.map((item) =>
81
+ h('p', { key: item.id, 'data-id': item.id }, item.id),
82
+ ),
83
+ )
84
+ },
85
+ })
86
+
87
+ function mountLists(lists: { items: Item[]; options: Options }[]) {
88
+ const host = document.createElement('div')
89
+ document.body.append(host)
90
+ app = createApp({
91
+ render: () => lists.map((list) => h(List, list)),
92
+ })
93
+ app.mount(host)
94
+ return host
95
+ }
96
+
97
+ const ids = (items: Item[]) => items.map((item) => item.id)
98
+ const domIds = (sortable: FakeSortable) =>
99
+ [...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)
102
+
103
+ describe('useSortableList', () => {
104
+ it('reorders a list in place and renders the new order', async () => {
105
+ const items = reactive([{ id: 'a' }, { id: 'b' }, { id: 'c' }])
106
+ const changes: unknown[] = []
107
+ const ended = vi.fn()
108
+ mountLists([
109
+ {
110
+ items,
111
+ options: { onChange: (c: unknown) => changes.push(c), onEnd: ended },
112
+ },
113
+ ])
114
+ const [list] = instances
115
+ const item = list!.el.children[0] as HTMLElement
116
+ call(list!, 'onStart', { item, oldIndex: 0 })
117
+ // SortableJS has moved the element already when it reports the update.
118
+ list!.el.append(item)
119
+ call(list!, 'onUpdate', { item, oldIndex: 0, newIndex: 2 })
120
+ call(list!, 'onEnd', {})
121
+ await nextTick()
122
+ expect(ids(items)).toEqual(['b', 'c', 'a'])
123
+ expect(domIds(list!)).toEqual(['b', 'c', 'a'])
124
+ expect(changes).toEqual([
125
+ { moved: { element: { id: 'a' }, oldIndex: 0, newIndex: 2 } },
126
+ ])
127
+ expect(ended).toHaveBeenCalledOnce()
128
+ })
129
+
130
+ it('moves an item to another list of its group', async () => {
131
+ const source = reactive([{ id: 'a' }, { id: 'b' }])
132
+ const target = reactive([{ id: 'x' }])
133
+ const sourceChanges: unknown[] = []
134
+ const targetChanges: unknown[] = []
135
+ mountLists([
136
+ {
137
+ items: source,
138
+ options: {
139
+ group: 'g',
140
+ onChange: (c: unknown) => sourceChanges.push(c),
141
+ },
142
+ },
143
+ {
144
+ items: target,
145
+ options: {
146
+ group: 'g',
147
+ onChange: (c: unknown) => targetChanges.push(c),
148
+ },
149
+ },
150
+ ])
151
+ const [from, to] = instances
152
+ const item = from!.el.children[1] as HTMLElement
153
+ call(from!, 'onStart', { item, oldIndex: 1 })
154
+ to!.el.prepend(item)
155
+ // SortableJS reports the add on the target first, then the remove.
156
+ call(to!, 'onAdd', { item, newIndex: 0 })
157
+ call(from!, 'onRemove', { item, oldIndex: 1 })
158
+ call(from!, 'onEnd', {})
159
+ await nextTick()
160
+ expect(ids(source)).toEqual(['a'])
161
+ expect(ids(target)).toEqual(['b', 'x'])
162
+ expect(domIds(from!)).toEqual(['a'])
163
+ expect(domIds(to!)).toEqual(['b', 'x'])
164
+ expect(sourceChanges).toEqual([
165
+ { removed: { element: { id: 'b' }, oldIndex: 1 } },
166
+ ])
167
+ expect(targetChanges).toEqual([
168
+ { added: { element: { id: 'b' }, newIndex: 0 } },
169
+ ])
170
+ })
171
+
172
+ it('hands a copy to the other list and keeps its own item (pull: clone)', async () => {
173
+ const palette = reactive([{ id: 'tile' }])
174
+ const canvas = reactive<Item[]>([])
175
+ const group = { name: 'g', pull: 'clone', put: false }
176
+ mountLists([
177
+ {
178
+ items: palette,
179
+ options: {
180
+ group,
181
+ sort: false,
182
+ clone: (item: Item) => ({ id: `${item.id}-copy` }),
183
+ },
184
+ },
185
+ { items: canvas, options: { group: { name: 'g' } } },
186
+ ])
187
+ const [from, to] = instances
188
+ expect(from!.options.group).toBe(group)
189
+ expect(from!.options.sort).toBe(false)
190
+ const item = from!.el.children[0] as HTMLElement
191
+ call(from!, 'onStart', { item, oldIndex: 0 })
192
+ // SortableJS leaves a stand-in where the tile was and drags the tile.
193
+ const stand = item.cloneNode(true) as HTMLElement
194
+ from!.el.insertBefore(stand, item)
195
+ to!.el.append(item)
196
+ call(to!, 'onAdd', { item, newIndex: 0 })
197
+ call(from!, 'onRemove', {
198
+ item,
199
+ clone: stand,
200
+ oldIndex: 0,
201
+ pullMode: 'clone',
202
+ })
203
+ call(from!, 'onEnd', {})
204
+ await nextTick()
205
+ expect(ids(palette)).toEqual(['tile'])
206
+ expect(ids(canvas)).toEqual(['tile-copy'])
207
+ expect(domIds(from!)).toEqual(['tile'])
208
+ expect(domIds(to!)).toEqual(['tile-copy'])
209
+ })
210
+
211
+ it('passes the handle, the item selector and a disabled state it follows', async () => {
212
+ const disabled = ref(false)
213
+ mountLists([
214
+ {
215
+ items: reactive([{ id: 'a' }]),
216
+ options: {
217
+ handle: '[data-drag-handle]',
218
+ draggable: '>[data-drag-item]',
219
+ ghostClass: 'opacity-30',
220
+ disabled: () => disabled.value,
221
+ },
222
+ },
223
+ ])
224
+ const [list] = instances
225
+ expect(list!.options).toMatchObject({
226
+ handle: '[data-drag-handle]',
227
+ draggable: '>[data-drag-item]',
228
+ ghostClass: 'opacity-30',
229
+ disabled: false,
230
+ sort: true,
231
+ })
232
+ disabled.value = true
233
+ await nextTick()
234
+ expect(list!.options.disabled).toBe(true)
235
+ })
236
+
237
+ it('leaves SortableJS its defaults for what the list does not set', () => {
238
+ mountLists([{ items: reactive([{ id: 'a' }]), options: {} }])
239
+ const [list] = instances
240
+ // A key present with `undefined` would replace the default: an
241
+ // 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')
252
+ })
253
+ })
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-mailing",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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
+ "keywords": [
6
+ "antelopejs",
7
+ "dms",
8
+ "dms-module",
9
+ "email",
10
+ "mailing",
11
+ "templates"
12
+ ],
13
+ "homepage": "https://dms.antelopejs.com/docs/modules/mailing",
14
+ "bugs": {
15
+ "url": "https://github.com/AntelopeJS/dms-mailing/issues"
16
+ },
4
17
  "license": "Apache-2.0",
5
18
  "repository": {
6
19
  "type": "git",
@@ -14,6 +27,7 @@
14
27
  "!dist/.tsbuildinfo",
15
28
  "!dist/test",
16
29
  "frontend-vue",
30
+ "skills",
17
31
  "src",
18
32
  "!src/test"
19
33
  ],
@@ -32,21 +46,21 @@
32
46
  "registry": "https://registry.npmjs.org/"
33
47
  },
34
48
  "dependencies": {
35
- "@antelopejs/interface-api": ">=0.0.13 <1.0.0",
36
- "@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
37
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
38
- "@antelopejs/interface-data-api": ">=0.2.1 <1.0.0",
39
- "@antelopejs/interface-database": ">=0.1.9 <1.0.0",
40
- "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
41
- "@antelopejs/interface-dms": ">=0.7.0 <1.0.0",
42
- "@antelopejs/interface-dms-mailing": ">=0.7.0 <0.8.0",
43
- "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
49
+ "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
50
+ "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
51
+ "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
52
+ "@antelopejs/interface-data-api": ">=0.2.2 <1.0.0",
53
+ "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
54
+ "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0",
55
+ "@antelopejs/interface-dms": ">=0.7.2 <1.0.0",
56
+ "@antelopejs/interface-dms-mailing": ">=0.7.1 <0.8.0",
57
+ "@antelopejs/interface-email": ">=0.0.6 <1.0.0",
44
58
  "node-cron": "^4.2.1",
45
59
  "zod": "~3.24.2"
46
60
  },
47
61
  "devDependencies": {
48
- "@antelopejs/core": ">=1.13.4 <2",
49
- "@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
62
+ "@antelopejs/core": ">=1.16.0 <2",
63
+ "@antelopejs/tooling-configs": ">=0.0.9 <1.0.0",
50
64
  "@types/chai": "^4.3.20",
51
65
  "@types/mocha": "^10",
52
66
  "@types/node": "^24.0.0",
@@ -64,14 +78,17 @@
64
78
  "typescript": "^6.0.3"
65
79
  },
66
80
  "optionalDependencies": {
67
- "@antelopejs/interface-dms-automation": ">=0.1.0 <1.0.0"
81
+ "@antelopejs/interface-dms-automation": ">=0.2.1 <1.0.0"
68
82
  },
69
83
  "antelopeJs": {
70
84
  "baseUrl": "dist",
71
85
  "implements": [
72
86
  "@antelopejs/interface-dms-mailing"
73
87
  ],
74
- "test": "src/test/antelope.test.ts"
88
+ "test": "src/test/antelope.test.ts",
89
+ "skills": [
90
+ "./skills"
91
+ ]
75
92
  },
76
93
  "scripts": {
77
94
  "build": "rimraf dist && tsc",