@veltra/desktop 1.7.16 → 1.7.18

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 (36) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +6 -1
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit-list.js +37 -31
  4. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit.js +2 -2
  6. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  7. package/dist/components/batch-edit/di.js.map +1 -1
  8. package/dist/components/batch-edit/use-edit-state.js +14 -10
  9. package/dist/components/batch-edit/use-edit-state.js.map +1 -1
  10. package/dist/components/batch-edit/use-handlers.js +1 -0
  11. package/dist/components/batch-edit/use-handlers.js.map +1 -1
  12. package/dist/components/multi-tree-select/multi-tree-select.js +8 -2
  13. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  14. package/dist/components/tree/tree-node.js +1 -1
  15. package/dist/components/tree/tree-node.js.map +1 -1
  16. package/dist/components/tree/tree.js +3 -6
  17. package/dist/components/tree/tree.js.map +1 -1
  18. package/dist/components/tree/use-check.js +32 -9
  19. package/dist/components/tree/use-check.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/types/batch-edit.d.ts +0 -3
  22. package/dist/types/index.d.ts +2 -2
  23. package/package.json +1 -1
  24. package/src/components/batch-edit/__test__/batch-edit.test.ts +164 -38
  25. package/src/components/batch-edit/batch-edit-form.vue +7 -1
  26. package/src/components/batch-edit/batch-edit-list.vue +38 -24
  27. package/src/components/batch-edit/batch-edit.vue +2 -1
  28. package/src/components/batch-edit/di.ts +2 -0
  29. package/src/components/batch-edit/use-edit-state.ts +25 -32
  30. package/src/components/batch-edit/use-handlers.ts +2 -0
  31. package/src/components/multi-tree-select/__test__/multi-tree-select.test.ts +222 -0
  32. package/src/components/multi-tree-select/multi-tree-select.vue +3 -1
  33. package/src/components/tree/tree-node.vue +1 -1
  34. package/src/components/tree/tree.vue +8 -6
  35. package/src/components/tree/use-check.ts +63 -33
  36. package/src/types/batch-edit.ts +0 -4
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createApp, h, nextTick, ref, type Ref } from 'vue'
3
+
4
+ import UMultiTreeSelect from '../multi-tree-select.vue'
5
+
6
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7
+
8
+ const treeData = [
9
+ {
10
+ label: '亚洲',
11
+ value: 'asia',
12
+ children: [
13
+ { label: '中国', value: 'cn' },
14
+ { label: '日本', value: 'jp' }
15
+ ]
16
+ },
17
+ {
18
+ label: '欧洲',
19
+ value: 'europe',
20
+ children: [
21
+ { label: '英国', value: 'uk' },
22
+ { label: '法国', value: 'fr' }
23
+ ]
24
+ }
25
+ ]
26
+
27
+ function mountMultiTreeSelect(props: Record<string, unknown> = {}) {
28
+ const host = document.createElement('div')
29
+ document.body.appendChild(host)
30
+
31
+ const model = ref(props.modelValue ?? []) as Ref<(string | number)[]>
32
+ const data = ref(props.data ?? treeData)
33
+
34
+ const app = createApp({
35
+ render() {
36
+ return h(UMultiTreeSelect, {
37
+ ...props,
38
+ modelValue: model.value,
39
+ data: data.value,
40
+ expandAll: true,
41
+ 'onUpdate:modelValue': (value: (string | number)[]) => {
42
+ model.value = value
43
+ }
44
+ })
45
+ }
46
+ })
47
+
48
+ app.mount(host)
49
+
50
+ return {
51
+ host,
52
+ model,
53
+ data,
54
+ unmount() {
55
+ app.unmount()
56
+ host.remove()
57
+ }
58
+ }
59
+ }
60
+
61
+ async function openDropdown(host: HTMLElement) {
62
+ const trigger = host.querySelector('.u-multi-tree-select') as HTMLElement
63
+ trigger.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
64
+ for (let attempt = 0; attempt < 20; attempt++) {
65
+ await nextTick()
66
+ await sleep(25)
67
+ if (queryNodeContents().length > 0) return
68
+ }
69
+ }
70
+
71
+ function queryNodeContents() {
72
+ return [...document.body.querySelectorAll<HTMLElement>('.u-tree__node-content')]
73
+ }
74
+
75
+ function queryCheckboxes() {
76
+ return [...document.body.querySelectorAll<HTMLInputElement>('.u-tree .u-checkbox__native')]
77
+ }
78
+
79
+ async function clickNodeByLabel(label: string, ctrlKey = false) {
80
+ const node = queryNodeContents().find((el) => el.textContent?.includes(label))
81
+ expect(node, `tree node "${label}" not found`).toBeTruthy()
82
+ node!.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey }))
83
+ await nextTick()
84
+ await sleep(50)
85
+ }
86
+
87
+ describe('UMultiTreeSelect check-strictly and ctrl-key behavior', () => {
88
+ it('cascades check and uncheck to children by default (checkStrictly = false)', async () => {
89
+ const { host, model, unmount } = mountMultiTreeSelect()
90
+
91
+ try {
92
+ await openDropdown(host)
93
+ await clickNodeByLabel('亚洲')
94
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
95
+
96
+ // 取消勾选「亚洲」时级联取消子节点
97
+ await clickNodeByLabel('亚洲')
98
+ expect(model.value).toEqual([])
99
+ } finally {
100
+ unmount()
101
+ }
102
+ })
103
+
104
+ it('independently checks node when pressing ctrl in default mode', async () => {
105
+ const { host, model, unmount } = mountMultiTreeSelect()
106
+
107
+ try {
108
+ await openDropdown(host)
109
+ // 按住 ctrl 点击「亚洲」,仅勾选「亚洲」自身
110
+ await clickNodeByLabel('亚洲', true)
111
+ expect(model.value).toEqual(['asia'])
112
+
113
+ // 按住 ctrl 再次点击「亚洲」,仅取消「亚洲」自身
114
+ await clickNodeByLabel('亚洲', true)
115
+ expect(model.value).toEqual([])
116
+ } finally {
117
+ unmount()
118
+ }
119
+ })
120
+
121
+ it('cascades children on check, but only unchecks self on uncheck in checkStrictly mode', async () => {
122
+ const { host, model, unmount } = mountMultiTreeSelect({ 'check-strictly': true })
123
+
124
+ try {
125
+ await openDropdown(host)
126
+ // 勾选父节点「亚洲」-> 把所有子节点都勾上
127
+ await clickNodeByLabel('亚洲')
128
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
129
+
130
+ // 取消勾选父节点「亚洲」-> 只取消自己,子节点保持勾选
131
+ await clickNodeByLabel('亚洲')
132
+ expect(model.value).toEqual(['cn', 'jp'])
133
+ } finally {
134
+ unmount()
135
+ }
136
+ })
137
+
138
+ it('does not show indeterminate on parent when only child is checked in checkStrictly mode', async () => {
139
+ const { host, unmount } = mountMultiTreeSelect({ checkStrictly: true })
140
+
141
+ try {
142
+ await openDropdown(host)
143
+ await clickNodeByLabel('中国')
144
+
145
+ // 父节点「亚洲」不应有半选状态
146
+ const asiaNodeContent = queryNodeContents().find((el) => el.textContent?.includes('亚洲'))
147
+ const asiaCheckbox = asiaNodeContent?.closest('.u-tree__node')?.querySelector('.u-checkbox')
148
+ expect(asiaCheckbox?.classList.contains('is-indeterminate')).toBe(false)
149
+ expect(asiaCheckbox?.querySelector('.u-checkbox--indeterminate')).toBeNull()
150
+ } finally {
151
+ unmount()
152
+ }
153
+ })
154
+
155
+ it('selects all nodes when clicking select all in checkStrictly mode', async () => {
156
+ const { host, model, unmount } = mountMultiTreeSelect({ checkStrictly: true })
157
+
158
+ try {
159
+ await openDropdown(host)
160
+ // 点击全选 checkbox
161
+ const allCheckboxInput = document.body.querySelector(
162
+ '.u-multi-tree-select__content-header .u-checkbox__native'
163
+ ) as HTMLInputElement
164
+ expect(allCheckboxInput).toBeTruthy()
165
+ allCheckboxInput.click()
166
+ await nextTick()
167
+ await sleep(50)
168
+
169
+ expect(model.value).toEqual(['asia', 'cn', 'jp', 'europe', 'uk', 'fr'])
170
+
171
+ // 再次点击取消全选
172
+ allCheckboxInput.click()
173
+ await nextTick()
174
+ await sleep(50)
175
+ expect(model.value).toEqual([])
176
+ } finally {
177
+ unmount()
178
+ }
179
+ })
180
+
181
+ it('echoes only parent node when initialized with parent id in checkStrictly mode', async () => {
182
+ const { host, model, unmount } = mountMultiTreeSelect({
183
+ checkStrictly: true,
184
+ modelValue: ['asia']
185
+ })
186
+
187
+ try {
188
+ await openDropdown(host)
189
+ expect(model.value).toEqual(['asia'])
190
+
191
+ // tags 只有「亚洲」
192
+ const tags = host.querySelectorAll('.u-tag')
193
+ expect(tags.length).toBe(1)
194
+ expect(tags[0].textContent).toContain('亚洲')
195
+ } finally {
196
+ unmount()
197
+ }
198
+ })
199
+
200
+ it('directly clicking checkbox cascades on check and only unchecks self on uncheck in checkStrictly mode', async () => {
201
+ const { host, model, unmount } = mountMultiTreeSelect({ checkStrictly: true })
202
+
203
+ try {
204
+ await openDropdown(host)
205
+ const checkboxes = queryCheckboxes()
206
+ expect(checkboxes.length).toBeGreaterThan(0)
207
+ // 点击第一个 checkbox(亚洲)-> 勾选所有子节点
208
+ checkboxes[0].click()
209
+ await nextTick()
210
+ await sleep(50)
211
+ expect(model.value).toEqual(['asia', 'cn', 'jp'])
212
+
213
+ // 再次点击第一个 checkbox(亚洲)-> 仅取消亚洲自身
214
+ checkboxes[0].click()
215
+ await nextTick()
216
+ await sleep(50)
217
+ expect(model.value).toEqual(['cn', 'jp'])
218
+ } finally {
219
+ unmount()
220
+ }
221
+ })
222
+ })
@@ -131,7 +131,9 @@ const props = withDefaults(defineProps<MultiTreeSelectProps>(), {
131
131
  readonly: undefined,
132
132
  filterable: false,
133
133
  visibilityLimit: 3,
134
- minWidth: '280px'
134
+ minWidth: '280px',
135
+ checkStrictly: false,
136
+ checkOnClickNode: true
135
137
  })
136
138
 
137
139
  const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
@@ -27,7 +27,7 @@
27
27
  v-if="treeProps.checkable"
28
28
  :class="cls.e('checkbox')"
29
29
  :model-value="node.checked"
30
- :indeterminate="node.indeterminate"
30
+ :indeterminate="!treeProps.checkStrictly && node.indeterminate"
31
31
  :disabled="node.disabled"
32
32
  @change="handleChange"
33
33
  @click.stop
@@ -102,7 +102,13 @@ const { filter } = useFilter({ forest, getFlattedNodes })
102
102
 
103
103
  const { handleSelect, selectedData } = useSelect({ props, emit, nodeDict, getFlattedNodes })
104
104
 
105
- const { checkedData, toggleCheck } = useCheck({ props, emit, nodeDict, getFlattedNodes })
105
+ const { checkedData, toggleCheck, checkAll } = useCheck({
106
+ props,
107
+ emit,
108
+ forest,
109
+ nodeDict,
110
+ getFlattedNodes
111
+ })
106
112
 
107
113
  const estimateSize = (): number => {
108
114
  // 基于 style.scss 中节点稳态高度推算的默认值;
@@ -196,11 +202,7 @@ defineExpose<_TreeExposed>({
196
202
  nodes,
197
203
  checkNode: toggleCheck,
198
204
  selectNode: handleSelect,
199
- checkAll(check: boolean) {
200
- forest.value.roots.forEach((node) => {
201
- toggleCheck(node, check)
202
- })
203
- },
205
+ checkAll,
204
206
  getSelected() {
205
207
  return selectedData
206
208
  },
@@ -1,3 +1,4 @@
1
+ import type { Forest } from '@cat-kit/core'
1
2
  import { dfs, o } from '@cat-kit/core'
2
3
  import { fieldKey } from '@veltra/utils'
3
4
  import { nextTick, watch, type ComputedRef } from 'vue'
@@ -8,6 +9,7 @@ import type { TreeNode } from './tree-node'
8
9
  interface Options {
9
10
  emit: TreeEmit
10
11
  props: TreeProps
12
+ forest: ComputedRef<Forest<Record<string, unknown>, any>>
11
13
  nodeDict: ComputedRef<Map<any, TreeNode>>
12
14
  getFlattedNodes: () => void
13
15
  }
@@ -15,6 +17,7 @@ interface Options {
15
17
  interface UseCheckReturned {
16
18
  checkedData: Set<Record<string, any>>
17
19
  toggleCheck: (node: TreeNode, check: boolean, ctrlKey?: boolean) => void
20
+ checkAll: (check: boolean) => void
18
21
  }
19
22
 
20
23
  /**
@@ -33,7 +36,7 @@ function expandAncestors(node: TreeNode): boolean {
33
36
  }
34
37
 
35
38
  export function useCheck(options: Options): UseCheckReturned {
36
- const { emit, props, nodeDict, getFlattedNodes } = options
39
+ const { emit, props, forest, nodeDict, getFlattedNodes } = options
37
40
 
38
41
  const checkedData = new Set<Record<string, any>>()
39
42
 
@@ -48,7 +51,7 @@ export function useCheck(options: Options): UseCheckReturned {
48
51
  function checkNode(node: TreeNode): void {
49
52
  if (node.checked) return
50
53
  node.checked = true
51
- if (node.parent) {
54
+ if (!props.checkStrictly && node.parent) {
52
55
  node.parent.childrenCheckCount++
53
56
  }
54
57
  checkedData.add(node.data)
@@ -57,7 +60,7 @@ export function useCheck(options: Options): UseCheckReturned {
57
60
  function uncheckNode(node: TreeNode): void {
58
61
  if (!node.checked) return
59
62
  node.checked = false
60
- if (node.parent) {
63
+ if (!props.checkStrictly && node.parent) {
61
64
  node.parent.childrenCheckCount--
62
65
  }
63
66
  checkedData.delete(node.data)
@@ -129,18 +132,19 @@ export function useCheck(options: Options): UseCheckReturned {
129
132
  const { checkStrictly } = props
130
133
 
131
134
  if (ctrlKey) {
132
- checkNode(node)
133
- } else {
134
- dfs(
135
- node as unknown as Record<string, unknown>,
136
- (n) => {
137
- const tn = n as unknown as TreeNode
138
- !tn.disabled && checkNode(tn)
139
- },
140
- props.childrenKey ?? 'children'
141
- )
135
+ !node.disabled && checkNode(node)
136
+ return
142
137
  }
143
138
 
139
+ dfs(
140
+ node as unknown as Record<string, unknown>,
141
+ (n) => {
142
+ const tn = n as unknown as TreeNode
143
+ !tn.disabled && checkNode(tn)
144
+ },
145
+ props.childrenKey ?? 'children'
146
+ )
147
+
144
148
  // 非严格选择时还需要更新祖先节点,
145
149
  // 一旦子节点全部选中,父节点也要设置为选中状态
146
150
  if (!checkStrictly) {
@@ -158,28 +162,28 @@ export function useCheck(options: Options): UseCheckReturned {
158
162
 
159
163
  function handleUncheck(node: TreeNode, ctrlKey?: boolean) {
160
164
  const { checkStrictly } = props
161
- if (ctrlKey) {
162
- uncheckNode(node)
163
- } else {
164
- dfs(
165
- node as unknown as Record<string, unknown>,
166
- (n) => {
167
- const tn = n as unknown as TreeNode
168
- !tn.disabled && uncheckNode(tn)
169
- },
170
- props.childrenKey ?? 'children'
171
- )
165
+
166
+ if (ctrlKey || checkStrictly) {
167
+ !node.disabled && uncheckNode(node)
168
+ return
172
169
  }
173
170
 
171
+ dfs(
172
+ node as unknown as Record<string, unknown>,
173
+ (n) => {
174
+ const tn = n as unknown as TreeNode
175
+ !tn.disabled && uncheckNode(tn)
176
+ },
177
+ props.childrenKey ?? 'children'
178
+ )
179
+
174
180
  // 非严格模式下,取消选中时,需要更新父节点
175
- if (!checkStrictly) {
176
- node.bubbleSet((node) => {
177
- const { parent } = node
178
- if (parent) {
179
- uncheckNode(parent)
180
- }
181
- })
182
- }
181
+ node.bubbleSet((node) => {
182
+ const { parent } = node
183
+ if (parent) {
184
+ uncheckNode(parent)
185
+ }
186
+ })
183
187
  }
184
188
 
185
189
  function toggleCheck(node: TreeNode, check: boolean, ctrlKey?: boolean) {
@@ -202,5 +206,31 @@ export function useCheck(options: Options): UseCheckReturned {
202
206
  })
203
207
  }
204
208
 
205
- return { checkedData, toggleCheck }
209
+ function checkAll(check: boolean) {
210
+ checkedByEvent = true
211
+
212
+ forest.value.dfs((node) => {
213
+ const tn = node as unknown as TreeNode
214
+ if (!tn.disabled) {
215
+ check ? checkNode(tn) : uncheckNode(tn)
216
+ }
217
+ })
218
+
219
+ const checkedArr = Array.from(checkedData)
220
+
221
+ const valueKey = fieldKey(props.valueKey, 'value')
222
+ const nextValues = checkedArr.map((item) => o(item).get(valueKey) as unknown)
223
+
224
+ lastCheckedSet = new Set(nextValues)
225
+
226
+ emit('update:checked', nextValues as any[], checkedArr)
227
+
228
+ getFlattedNodes()
229
+
230
+ nextTick(() => {
231
+ checkedByEvent = false
232
+ })
233
+ }
234
+
235
+ return { checkedData, toggleCheck, checkAll }
206
236
  }
@@ -8,8 +8,6 @@ export interface BatchEditColumn extends TableColumn {}
8
8
 
9
9
  export type BatchEditFeature = 'create' | 'update' | 'delete' | 'view' | 'createChild'
10
10
 
11
- export type BatchEditFormStatus = 'hidden'
12
-
13
11
  /** 批量编辑状态 */
14
12
  export interface BatchEditStates {
15
13
  /** 层级 */
@@ -35,8 +33,6 @@ export interface BatchEditProps extends TableProps {
35
33
  * @description 与右侧 UForm 绑定的 reactive 对象
36
34
  */
37
35
  model?: Record<string, any>
38
- /** 表格标题 */
39
- title?: string
40
36
  /**
41
37
  * 列的宽度定义
42
38
  */