@antelopejs/dms-mailing 0.5.0 → 0.6.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 (106) 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 +2 -0
  8. package/dist/db/models/send-events.model.js +11 -0
  9. package/dist/db/models/send-events.model.js.map +1 -1
  10. package/dist/db/models/sends.model.d.ts +3 -0
  11. package/dist/db/models/sends.model.js +13 -0
  12. package/dist/db/models/sends.model.js.map +1 -1
  13. package/dist/db/models/settings.model.d.ts +1 -0
  14. package/dist/db/models/suppressions.model.d.ts +1 -0
  15. package/dist/db/models/template-slugs.model.d.ts +27 -0
  16. package/dist/db/models/template-slugs.model.js +43 -0
  17. package/dist/db/models/template-slugs.model.js.map +1 -0
  18. package/dist/db/models/template-versions.model.d.ts +4 -1
  19. package/dist/db/models/template-versions.model.js +35 -2
  20. package/dist/db/models/template-versions.model.js.map +1 -1
  21. package/dist/db/models/templates.model.d.ts +2 -0
  22. package/dist/db/models/templates.model.js +22 -0
  23. package/dist/db/models/templates.model.js.map +1 -1
  24. package/dist/db/models/webhook-status.model.d.ts +1 -0
  25. package/dist/db/tables/index.d.ts +1 -0
  26. package/dist/db/tables/index.js +1 -0
  27. package/dist/db/tables/index.js.map +1 -1
  28. package/dist/db/tables/settings.table.d.ts +1 -0
  29. package/dist/db/tables/settings.table.js +4 -0
  30. package/dist/db/tables/settings.table.js.map +1 -1
  31. package/dist/db/tables/template-slugs.table.d.ts +7 -0
  32. package/dist/db/tables/template-slugs.table.js +35 -0
  33. package/dist/db/tables/template-slugs.table.js.map +1 -0
  34. package/dist/db/tables/templates.table.d.ts +5 -0
  35. package/dist/db/tables/templates.table.js +27 -0
  36. package/dist/db/tables/templates.table.js.map +1 -1
  37. package/dist/hooks/database-ready.d.ts +7 -0
  38. package/dist/hooks/database-ready.js +26 -0
  39. package/dist/hooks/database-ready.js.map +1 -0
  40. package/dist/hooks/index.d.ts +1 -0
  41. package/dist/hooks/index.js +1 -0
  42. package/dist/hooks/index.js.map +1 -1
  43. package/dist/implementations/dms-mailing/index.d.ts +3 -0
  44. package/dist/implementations/dms-mailing/index.js +7 -1
  45. package/dist/implementations/dms-mailing/index.js.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/routes/templates.js +1 -0
  49. package/dist/routes/templates.js.map +1 -1
  50. package/dist/services/declaration-error.d.ts +6 -0
  51. package/dist/services/declaration-error.js +26 -0
  52. package/dist/services/declaration-error.js.map +1 -0
  53. package/dist/services/declarations.d.ts +3 -0
  54. package/dist/services/declarations.js +244 -0
  55. package/dist/services/declarations.js.map +1 -0
  56. package/dist/services/events.d.ts +4 -1
  57. package/dist/services/events.js.map +1 -1
  58. package/dist/services/fingerprint.d.ts +1 -0
  59. package/dist/services/fingerprint.js +20 -0
  60. package/dist/services/fingerprint.js.map +1 -0
  61. package/dist/services/send-import.d.ts +2 -0
  62. package/dist/services/send-import.js +136 -0
  63. package/dist/services/send-import.js.map +1 -0
  64. package/dist/services/templates.d.ts +3 -0
  65. package/dist/services/templates.js +75 -48
  66. package/dist/services/templates.js.map +1 -1
  67. package/dist/validation/declarations.schema.d.ts +299 -0
  68. package/dist/validation/declarations.schema.js +55 -0
  69. package/dist/validation/declarations.schema.js.map +1 -0
  70. package/frontend-vue/app/components/CategoriesInput.vue +12 -11
  71. package/frontend-vue/app/components/editor/EditorBlockList.vue +33 -30
  72. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +14 -14
  73. package/frontend-vue/app/composables/useSortableList.ts +159 -0
  74. package/frontend-vue/app/utils/blocks/dnd.ts +2 -0
  75. package/frontend-vue/i18n/locales/mailing-en-GB.json +1 -1
  76. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1 -1
  77. package/frontend-vue/package.json +4 -3
  78. package/frontend-vue/pnpm-lock.yaml +6 -13
  79. package/frontend-vue/tests/editor-block-list.test.ts +46 -6
  80. package/frontend-vue/tests/sortable-list.test.ts +253 -0
  81. package/package.json +31 -14
  82. package/skills/dms-mailing/SKILL.md +101 -0
  83. package/src/data/templates-table.ts +7 -27
  84. package/src/db/models/index.ts +1 -0
  85. package/src/db/models/send-events.model.ts +13 -0
  86. package/src/db/models/sends.model.ts +22 -0
  87. package/src/db/models/template-slugs.model.ts +61 -0
  88. package/src/db/models/template-versions.model.ts +48 -2
  89. package/src/db/models/templates.model.ts +31 -0
  90. package/src/db/tables/index.ts +1 -0
  91. package/src/db/tables/settings.table.ts +8 -0
  92. package/src/db/tables/template-slugs.table.ts +29 -0
  93. package/src/db/tables/templates.table.ts +38 -0
  94. package/src/hooks/database-ready.ts +47 -0
  95. package/src/hooks/index.ts +1 -0
  96. package/src/implementations/dms-mailing/index.ts +17 -0
  97. package/src/index.ts +6 -1
  98. package/src/routes/templates.ts +2 -0
  99. package/src/services/declaration-error.ts +39 -0
  100. package/src/services/declarations.ts +450 -0
  101. package/src/services/events.ts +8 -2
  102. package/src/services/fingerprint.ts +22 -0
  103. package/src/services/send-import.ts +235 -0
  104. package/src/services/templates.ts +113 -72
  105. package/src/validation/declarations.schema.ts +77 -0
  106. package/frontend-vue/pnpm-workspace.yaml +0 -2
@@ -1,8 +1,12 @@
1
1
  <script setup lang="ts">
2
- import draggable from 'vuedraggable'
2
+ import { ref } from 'vue'
3
3
  import type { Block } from '../../types/mailing'
4
4
  import { useEditorContext } from '../../composables/useEditorContext'
5
- import { DRAG_GROUP, DRAG_HANDLE } from '../../utils/blocks'
5
+ import {
6
+ useSortableList,
7
+ type SortableListChange,
8
+ } from '../../composables/useSortableList'
9
+ import { DRAG_GROUP, DRAG_HANDLE, DRAG_ITEM } from '../../utils/blocks'
6
10
  import EditorBlock from './EditorBlock.vue'
7
11
  import EditorInsertPoint from './EditorInsertPoint.vue'
8
12
 
@@ -12,40 +16,39 @@ interface Props {
12
16
 
13
17
  const props = defineProps<Props>()
14
18
 
15
- interface DraggableChange {
16
- added?: { element: Block }
17
- }
18
-
19
19
  const { editor } = useEditorContext()
20
20
 
21
- function onChange(change: DraggableChange): void {
21
+ function onChange(change: SortableListChange<Block>): void {
22
22
  editor.touch()
23
23
  if (change.added) editor.select(change.added.element.id)
24
24
  }
25
+
26
+ const root = ref<HTMLElement | null>(null)
27
+
28
+ // The list is the editor's: a drop changes it in place, as a click does.
29
+ useSortableList(root, () => props.list, {
30
+ group: DRAG_GROUP,
31
+ handle: DRAG_HANDLE,
32
+ draggable: DRAG_ITEM,
33
+ ghostClass: 'opacity-30',
34
+ onChange,
35
+ })
25
36
  </script>
26
37
 
27
38
  <template>
28
- <draggable
29
- :list="props.list"
30
- :group="DRAG_GROUP"
31
- :handle="DRAG_HANDLE"
32
- item-key="id"
33
- ghost-class="opacity-30"
34
- class="flex flex-col"
35
- @change="onChange"
36
- >
37
- <template #item="{ element, index }">
38
- <div>
39
- <EditorInsertPoint :list="props.list" :index="index" />
40
- <EditorBlock :block="element" :index="index" :list="props.list" />
41
- </div>
42
- </template>
43
- <template #footer>
44
- <EditorInsertPoint
45
- :list="props.list"
46
- :index="props.list.length"
47
- :is-empty="props.list.length === 0"
48
- />
49
- </template>
50
- </draggable>
39
+ <div ref="root" class="flex flex-col">
40
+ <div
41
+ v-for="(element, index) in props.list"
42
+ :key="element.id"
43
+ data-drag-item
44
+ >
45
+ <EditorInsertPoint :list="props.list" :index="index" />
46
+ <EditorBlock :block="element" :index="index" :list="props.list" />
47
+ </div>
48
+ <EditorInsertPoint
49
+ :list="props.list"
50
+ :index="props.list.length"
51
+ :is-empty="props.list.length === 0"
52
+ />
53
+ </div>
51
54
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import draggable from 'vuedraggable'
2
+ import { ref } from 'vue'
3
+ import { useSortableList } from '../../composables/useSortableList'
3
4
  import {
4
5
  PALETTE_GROUP,
5
6
  cloneFromPalette,
@@ -23,23 +24,22 @@ const emit = defineEmits<{ add: [PaletteEntry] }>()
23
24
 
24
25
  const { t } = useI18n()
25
26
 
26
- const clone = (entry: PaletteEntry) =>
27
- cloneFromPalette(entry, props.blockContext())
27
+ const root = ref<HTMLElement | null>(null)
28
+
29
+ // A tile stays in the palette: the canvas receives the block it stands for.
30
+ useSortableList(root, () => props.entries, {
31
+ group: PALETTE_GROUP,
32
+ sort: false,
33
+ disabled: () => props.disabled,
34
+ clone: (entry) => cloneFromPalette(entry, props.blockContext()),
35
+ })
28
36
  </script>
29
37
 
30
38
  <template>
31
39
  <section>
32
40
  <DmsEyebrow :label="title" class="mb-2 block" />
33
- <draggable
34
- :list="entries"
35
- :group="PALETTE_GROUP"
36
- :clone="clone"
37
- :sort="false"
38
- :disabled="disabled"
39
- item-key="type"
40
- class="grid grid-cols-2 gap-1.5"
41
- >
42
- <template #item="{ element }">
41
+ <div ref="root" class="grid grid-cols-2 gap-1.5">
42
+ <template v-for="element in entries" :key="element.type">
43
43
  <button
44
44
  type="button"
45
45
  class="border-default bg-default text-toned hover:border-primary/60 hover:text-highlighted group flex min-h-9 cursor-grab items-center gap-2 rounded-md border px-2.5 py-1 text-left text-xs transition disabled:cursor-not-allowed disabled:opacity-50"
@@ -61,6 +61,6 @@ const clone = (entry: PaletteEntry) =>
61
61
  </span>
62
62
  </button>
63
63
  </template>
64
- </draggable>
64
+ </div>
65
65
  </section>
66
66
  </template>
@@ -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 {