@vobs/table 1.0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/package.json +33 -0
- package/src/column-settings.ts +381 -0
- package/src/data-table.test.ts +536 -0
- package/src/data-table.ts +686 -0
- package/src/index.ts +28 -0
- package/src/persistence.ts +99 -0
- package/src/styles/styles.css +1 -0
- package/src/styles/table.css +209 -0
- package/src/types.ts +183 -0
- package/src/utils.ts +172 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { state } from '@vobs/reactivity'
|
|
3
|
+
import { addEventListener, createDOMRenderer, createElement, createVobs, setAttribute, setRenderer } from '@vobs/vobs'
|
|
4
|
+
import {
|
|
5
|
+
createColumnSettingsPersistence,
|
|
6
|
+
createLocalColumnSettingsPersistence,
|
|
7
|
+
KitColumnSettings,
|
|
8
|
+
KitDataTable
|
|
9
|
+
} from './index'
|
|
10
|
+
import type {
|
|
11
|
+
DataTableColumn,
|
|
12
|
+
DataTableColumnSettings,
|
|
13
|
+
DataTableColumnSettingsPersistence,
|
|
14
|
+
DataTableColumnSettingsStorage,
|
|
15
|
+
DataTableQuery
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
interface User { id: number; name: string; role: string }
|
|
19
|
+
|
|
20
|
+
const columns: readonly DataTableColumn<User>[] = [
|
|
21
|
+
{ id: 'name', label: 'Name', key: 'name', sortable: true, filterable: true },
|
|
22
|
+
{ id: 'role', label: 'Role', key: 'role', sortable: true }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
describe('@vobs/table', () => {
|
|
26
|
+
beforeEach(() => setRenderer(createDOMRenderer()))
|
|
27
|
+
|
|
28
|
+
it('支持客户端筛选、排序和分页,并通过 query 回调交给宿主控制', () => {
|
|
29
|
+
const container = document.createElement('div')
|
|
30
|
+
const queries: DataTableQuery[] = []
|
|
31
|
+
const app = createVobs({
|
|
32
|
+
render: () => KitDataTable<User>({
|
|
33
|
+
columns,
|
|
34
|
+
rows: [
|
|
35
|
+
{ id: 1, name: 'Ada', role: 'Admin' },
|
|
36
|
+
{ id: 2, name: 'Lin', role: 'Viewer' }
|
|
37
|
+
],
|
|
38
|
+
pageSize: 1,
|
|
39
|
+
sort: null,
|
|
40
|
+
onQueryChange: query => queries.push(query)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
app.mount(container)
|
|
44
|
+
|
|
45
|
+
expect(container.querySelectorAll('tbody tr')).toHaveLength(1)
|
|
46
|
+
expect(container.querySelector('.vobs-data-table__filters')).toBeNull()
|
|
47
|
+
expect(container.querySelector('.vobs-data-table__filter')).toBeNull()
|
|
48
|
+
expect(container.textContent).toContain('Ada')
|
|
49
|
+
;(container.querySelector('button.vobs-data-table__sort') as HTMLButtonElement).click()
|
|
50
|
+
expect(queries[0]?.sort).toEqual({ columnId: 'name', direction: 'asc' })
|
|
51
|
+
;(container.querySelector('.vobs-data-table__page-button:not([disabled])') as HTMLButtonElement).click()
|
|
52
|
+
expect(queries[1]?.page).toBe(2)
|
|
53
|
+
app.destroy()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('客户端排序会实际改变行顺序,并支持升序、降序和取消排序', async () => {
|
|
57
|
+
const container = document.createElement('div')
|
|
58
|
+
const app = createVobs({
|
|
59
|
+
render: () => KitDataTable<User>({
|
|
60
|
+
columns,
|
|
61
|
+
rows: [
|
|
62
|
+
{ id: 1, name: 'Zoe', role: 'Admin' },
|
|
63
|
+
{ id: 2, name: 'Ada', role: 'Viewer' },
|
|
64
|
+
{ id: 3, name: 'Lin', role: 'Editor' }
|
|
65
|
+
]
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
app.mount(container)
|
|
69
|
+
|
|
70
|
+
const names = (): string[] => [...container.querySelectorAll('td[data-column-id="name"]')]
|
|
71
|
+
.map(cell => cell.textContent ?? '')
|
|
72
|
+
const sortButton = container.querySelector('button.vobs-data-table__sort') as HTMLButtonElement
|
|
73
|
+
const header = container.querySelector('th[data-column-id="name"]') as HTMLTableCellElement
|
|
74
|
+
|
|
75
|
+
expect(names()).toEqual(['Zoe', 'Ada', 'Lin'])
|
|
76
|
+
sortButton.click()
|
|
77
|
+
await Promise.resolve()
|
|
78
|
+
expect(names()).toEqual(['Ada', 'Lin', 'Zoe'])
|
|
79
|
+
expect(header.getAttribute('aria-sort')).toBe('ascending')
|
|
80
|
+
expect(header.getAttribute('data-sort-direction')).toBe('asc')
|
|
81
|
+
|
|
82
|
+
sortButton.click()
|
|
83
|
+
await Promise.resolve()
|
|
84
|
+
expect(names()).toEqual(['Zoe', 'Lin', 'Ada'])
|
|
85
|
+
expect(header.getAttribute('aria-sort')).toBe('descending')
|
|
86
|
+
|
|
87
|
+
sortButton.click()
|
|
88
|
+
await Promise.resolve()
|
|
89
|
+
expect(names()).toEqual(['Zoe', 'Ada', 'Lin'])
|
|
90
|
+
expect(header.hasAttribute('aria-sort')).toBe(false)
|
|
91
|
+
expect(header.hasAttribute('data-sort-direction')).toBe(false)
|
|
92
|
+
app.destroy()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('客户端排序支持数字、日期、空值置后和自定义 sortValue', async () => {
|
|
96
|
+
interface Metric {
|
|
97
|
+
readonly id: number
|
|
98
|
+
readonly score: number | null
|
|
99
|
+
readonly createdAt: string
|
|
100
|
+
}
|
|
101
|
+
const metricColumns: readonly DataTableColumn<Metric>[] = [
|
|
102
|
+
{ id: 'score', label: 'Score', key: 'score', sortable: true, sortType: 'number', sortValue: row => row.score },
|
|
103
|
+
{ id: 'createdAt', label: 'Created', sortable: true, sortType: 'date', key: 'createdAt' }
|
|
104
|
+
]
|
|
105
|
+
const container = document.createElement('div')
|
|
106
|
+
const app = createVobs({
|
|
107
|
+
render: () => KitDataTable<Metric>({
|
|
108
|
+
columns: metricColumns,
|
|
109
|
+
rows: [
|
|
110
|
+
{ id: 1, score: 10, createdAt: '2026-01-02' },
|
|
111
|
+
{ id: 2, score: null, createdAt: '2025-12-01' },
|
|
112
|
+
{ id: 3, score: 2, createdAt: '2026-01-01' }
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
app.mount(container)
|
|
117
|
+
|
|
118
|
+
const scores = (): string[] => [...container.querySelectorAll('td[data-column-id="score"]')]
|
|
119
|
+
.map(cell => cell.textContent ?? '')
|
|
120
|
+
const scoreButton = container.querySelector('th[data-column-id="score"] button') as HTMLButtonElement
|
|
121
|
+
scoreButton.click()
|
|
122
|
+
await Promise.resolve()
|
|
123
|
+
expect(scores()).toEqual(['2', '10', ''])
|
|
124
|
+
scoreButton.click()
|
|
125
|
+
await Promise.resolve()
|
|
126
|
+
expect(scores()).toEqual(['10', '2', ''])
|
|
127
|
+
|
|
128
|
+
const dates = (): string[] => [...container.querySelectorAll('td[data-column-id="createdAt"]')]
|
|
129
|
+
.map(cell => cell.textContent ?? '')
|
|
130
|
+
const dateButton = container.querySelector('th[data-column-id="createdAt"] button') as HTMLButtonElement
|
|
131
|
+
dateButton.click()
|
|
132
|
+
await Promise.resolve()
|
|
133
|
+
expect(dates()).toEqual(['2025-12-01', '2026-01-01', '2026-01-02'])
|
|
134
|
+
dateButton.click()
|
|
135
|
+
await Promise.resolve()
|
|
136
|
+
expect(dates()).toEqual(['2026-01-02', '2026-01-01', '2025-12-01'])
|
|
137
|
+
app.destroy()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('服务端排序模式只发出 sort 查询,不重排当前页数据', () => {
|
|
141
|
+
const queries: DataTableQuery[] = []
|
|
142
|
+
const serverColumns: readonly DataTableColumn<User>[] = [
|
|
143
|
+
{ id: 'name', label: 'Name', key: 'name', sortable: true, sortKey: 'display_name' },
|
|
144
|
+
{ id: 'role', label: 'Role', key: 'role' }
|
|
145
|
+
]
|
|
146
|
+
const container = document.createElement('div')
|
|
147
|
+
const app = createVobs({
|
|
148
|
+
render: () => KitDataTable<User>({
|
|
149
|
+
columns: serverColumns,
|
|
150
|
+
rows: [
|
|
151
|
+
{ id: 1, name: 'Zoe', role: 'Admin' },
|
|
152
|
+
{ id: 2, name: 'Ada', role: 'Viewer' }
|
|
153
|
+
],
|
|
154
|
+
total: 20,
|
|
155
|
+
sortingMode: 'server',
|
|
156
|
+
onQueryChange: query => queries.push(query)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
app.mount(container)
|
|
160
|
+
|
|
161
|
+
const names = (): string[] => [...container.querySelectorAll('td[data-column-id="name"]')]
|
|
162
|
+
.map(cell => cell.textContent ?? '')
|
|
163
|
+
;(container.querySelector('button.vobs-data-table__sort') as HTMLButtonElement).click()
|
|
164
|
+
|
|
165
|
+
expect(names()).toEqual(['Zoe', 'Ada'])
|
|
166
|
+
expect(queries[0]?.sort).toEqual({ columnId: 'name', direction: 'asc', sortKey: 'display_name' })
|
|
167
|
+
app.destroy()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('接受外部 filters 和列 filter 谓词,但不渲染表格内筛选输入', () => {
|
|
171
|
+
const container = document.createElement('div')
|
|
172
|
+
const app = createVobs({
|
|
173
|
+
render: () => KitDataTable<User>({
|
|
174
|
+
rows: [
|
|
175
|
+
{ id: 1, name: 'Ada', role: 'Admin' },
|
|
176
|
+
{ id: 2, name: 'Lin', role: 'Viewer' }
|
|
177
|
+
],
|
|
178
|
+
filters: { name: 'Ad' },
|
|
179
|
+
columns: columns.map(column => column.id === 'name'
|
|
180
|
+
? { ...column, filter: (row, value) => row.name.includes(String(value)) }
|
|
181
|
+
: column)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
app.mount(container)
|
|
185
|
+
expect(container.querySelector('.vobs-data-table__filter')).toBeNull()
|
|
186
|
+
expect(container.querySelectorAll('tbody tr')).toHaveLength(1)
|
|
187
|
+
expect(container.querySelector('tbody')?.textContent).toContain('Ada')
|
|
188
|
+
expect(container.querySelector('tbody')?.textContent).not.toContain('Lin')
|
|
189
|
+
app.destroy()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('默认固定表头,并允许关闭固定表头', () => {
|
|
193
|
+
const container = document.createElement('div')
|
|
194
|
+
const app = createVobs({
|
|
195
|
+
render: () => KitDataTable<User>({
|
|
196
|
+
columns,
|
|
197
|
+
rows: [{ id: 1, name: 'Ada', role: 'Admin' }]
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
app.mount(container)
|
|
201
|
+
expect(container.querySelector('.vobs-data-table')?.classList.contains('vobs-data-table--sticky-header')).toBe(true)
|
|
202
|
+
app.destroy()
|
|
203
|
+
|
|
204
|
+
const disabledContainer = document.createElement('div')
|
|
205
|
+
const disabledApp = createVobs({
|
|
206
|
+
render: () => KitDataTable<User>({
|
|
207
|
+
columns,
|
|
208
|
+
rows: [{ id: 1, name: 'Ada', role: 'Admin' }],
|
|
209
|
+
stickyHeader: false
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
disabledApp.mount(disabledContainer)
|
|
213
|
+
expect(disabledContainer.querySelector('.vobs-data-table')?.classList.contains('vobs-data-table--sticky-header')).toBe(false)
|
|
214
|
+
disabledApp.destroy()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('使用列设置应用列显隐、顺序和宽度', () => {
|
|
218
|
+
const container = document.createElement('div')
|
|
219
|
+
const app = createVobs({
|
|
220
|
+
render: () => KitDataTable<User>({
|
|
221
|
+
columns,
|
|
222
|
+
rows: [{ id: 1, name: 'Ada', role: 'Admin' }],
|
|
223
|
+
columnSettings: {
|
|
224
|
+
visibleColumnIds: ['role', 'name'],
|
|
225
|
+
columnOrder: ['role', 'name'],
|
|
226
|
+
columnWidths: { name: 160 }
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
app.mount(container)
|
|
231
|
+
|
|
232
|
+
expect([...container.querySelectorAll('thead th')].map(cell => cell.getAttribute('data-column-id')))
|
|
233
|
+
.toEqual(['role', 'name'])
|
|
234
|
+
expect(container.querySelector('thead th[data-column-id="name"]')?.getAttribute('style'))
|
|
235
|
+
.toContain('width: 160px')
|
|
236
|
+
expect(container.querySelector('tbody td[data-column-id="name"]')?.getAttribute('style'))
|
|
237
|
+
.toContain('width: 160px')
|
|
238
|
+
app.destroy()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('使用紧凑摘要显示当前显示数、每页数、当前页和总页数', () => {
|
|
242
|
+
const container = document.createElement('div')
|
|
243
|
+
const app = createVobs({
|
|
244
|
+
render: () => KitDataTable<User>({
|
|
245
|
+
columns,
|
|
246
|
+
rows: [
|
|
247
|
+
{ id: 1, name: 'Ada', role: 'Admin' },
|
|
248
|
+
{ id: 2, name: 'Lin', role: 'Viewer' },
|
|
249
|
+
{ id: 3, name: 'Grace', role: 'Reviewer' },
|
|
250
|
+
{ id: 4, name: 'Margaret', role: 'Maintainer' },
|
|
251
|
+
{ id: 5, name: 'Alan', role: 'Contributor' }
|
|
252
|
+
],
|
|
253
|
+
total: 15,
|
|
254
|
+
pageSize: 10
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
app.mount(container)
|
|
258
|
+
|
|
259
|
+
expect(container.querySelector('.vobs-data-table__summary')?.textContent).toBe('5/10 - 1/2')
|
|
260
|
+
app.destroy()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('按 simple、standard、all 逐步增加页码和跳转控件', () => {
|
|
264
|
+
const renderTable = (paginationMode: 'simple' | 'standard' | 'all') => KitDataTable<User>({
|
|
265
|
+
columns,
|
|
266
|
+
rows: Array.from({ length: 8 }, (_, id) => ({ id, name: `User ${id}`, role: 'Viewer' })),
|
|
267
|
+
pageSize: 1,
|
|
268
|
+
paginationMode
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
for (const [mode, expectedPageButtons, expectedNumberButtons, expectsJump] of [
|
|
272
|
+
['simple', 2, 0, false],
|
|
273
|
+
['standard', 2, 3, false],
|
|
274
|
+
['all', 2, 3, true]
|
|
275
|
+
] as const) {
|
|
276
|
+
const container = document.createElement('div')
|
|
277
|
+
const app = createVobs({ render: () => renderTable(mode) })
|
|
278
|
+
app.mount(container)
|
|
279
|
+
|
|
280
|
+
expect(container.querySelectorAll('.vobs-data-table__pagination > .vobs-data-table__page-button')).toHaveLength(expectedPageButtons)
|
|
281
|
+
expect(container.querySelectorAll('.vobs-data-table__pagination-pages .vobs-data-table__page-button')).toHaveLength(expectedNumberButtons)
|
|
282
|
+
expect(container.querySelector('.vobs-data-table__pagination-jump') !== null).toBe(expectsJump)
|
|
283
|
+
expect(container.querySelector('.vobs-data-table__page-size-select')).not.toBeNull()
|
|
284
|
+
app.destroy()
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('切换每页数时回到第 1 页并通过 query 回调通知宿主', () => {
|
|
289
|
+
const container = document.createElement('div')
|
|
290
|
+
const queries: DataTableQuery[] = []
|
|
291
|
+
const app = createVobs({
|
|
292
|
+
render: () => KitDataTable<User>({
|
|
293
|
+
columns,
|
|
294
|
+
rows: Array.from({ length: 6 }, (_, id) => ({ id, name: `User ${id}`, role: 'Viewer' })),
|
|
295
|
+
page: 2,
|
|
296
|
+
pageSize: 1,
|
|
297
|
+
pageSizeOptions: [1, 2, 4],
|
|
298
|
+
onQueryChange: query => queries.push(query)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
app.mount(container)
|
|
302
|
+
|
|
303
|
+
const select = container.querySelector('.vobs-data-table__page-size-select') as HTMLSelectElement
|
|
304
|
+
select.value = '4'
|
|
305
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
306
|
+
expect(queries[0]).toMatchObject({ page: 1, pageSize: 4 })
|
|
307
|
+
app.destroy()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('显式设置分页模式后,即使只有一页也保留每页数控件', () => {
|
|
311
|
+
const container = document.createElement('div')
|
|
312
|
+
const app = createVobs({
|
|
313
|
+
render: () => KitDataTable<User>({
|
|
314
|
+
columns,
|
|
315
|
+
rows: [{ id: 1, name: 'Ada', role: 'Admin' }],
|
|
316
|
+
pageSize: 10,
|
|
317
|
+
paginationMode: 'simple'
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
app.mount(container)
|
|
321
|
+
|
|
322
|
+
expect(container.querySelector('.vobs-data-table__page-size-select')).not.toBeNull()
|
|
323
|
+
app.destroy()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('all 模式可以通过页码输入跳转', () => {
|
|
327
|
+
const container = document.createElement('div')
|
|
328
|
+
const queries: DataTableQuery[] = []
|
|
329
|
+
const app = createVobs({
|
|
330
|
+
render: () => KitDataTable<User>({
|
|
331
|
+
columns,
|
|
332
|
+
rows: Array.from({ length: 4 }, (_, id) => ({ id, name: `User ${id}`, role: 'Viewer' })),
|
|
333
|
+
pageSize: 1,
|
|
334
|
+
paginationMode: 'all',
|
|
335
|
+
onQueryChange: query => queries.push(query)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
app.mount(container)
|
|
339
|
+
|
|
340
|
+
const input = container.querySelector('.vobs-data-table__jump-input') as HTMLInputElement
|
|
341
|
+
expect(input.type).toBe('text')
|
|
342
|
+
expect(input.inputMode).toBe('numeric')
|
|
343
|
+
expect(input.pattern).toBe('[0-9]*')
|
|
344
|
+
input.value = '3'
|
|
345
|
+
container.querySelector('.vobs-data-table__pagination-jump')?.dispatchEvent(
|
|
346
|
+
new Event('submit', { bubbles: true, cancelable: true })
|
|
347
|
+
)
|
|
348
|
+
expect(queries[queries.length - 1]?.page).toBe(3)
|
|
349
|
+
app.destroy()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('pagination 插槽可以完全接管右侧区域并使用分页上下文', () => {
|
|
353
|
+
const container = document.createElement('div')
|
|
354
|
+
const queries: DataTableQuery[] = []
|
|
355
|
+
const app = createVobs({
|
|
356
|
+
render: () => KitDataTable<User>({
|
|
357
|
+
columns,
|
|
358
|
+
rows: Array.from({ length: 4 }, (_, id) => ({ id, name: `User ${id}`, role: 'Viewer' })),
|
|
359
|
+
pageSize: 1,
|
|
360
|
+
pagination: context => {
|
|
361
|
+
const button = createElement('button')
|
|
362
|
+
setAttribute(button, 'type', 'button')
|
|
363
|
+
setAttribute(button, 'data-testid', 'custom-pagination')
|
|
364
|
+
addEventListener(button, 'click', () => context.goToPage(2))
|
|
365
|
+
return button
|
|
366
|
+
},
|
|
367
|
+
onQueryChange: query => queries.push(query)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
app.mount(container)
|
|
371
|
+
|
|
372
|
+
expect(container.querySelector('.vobs-data-table__pagination')).toBeNull()
|
|
373
|
+
;(container.querySelector('[data-testid="custom-pagination"]') as HTMLButtonElement).click()
|
|
374
|
+
expect(queries[queries.length - 1]?.page).toBe(2)
|
|
375
|
+
app.destroy()
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('列设置至少保留一列,并通过回调返回可见列 id', () => {
|
|
379
|
+
const container = document.createElement('div')
|
|
380
|
+
const onChange = vi.fn()
|
|
381
|
+
const app = createVobs({
|
|
382
|
+
render: () => KitColumnSettings({ columns, visibleColumnIds: ['name'], onChange })
|
|
383
|
+
})
|
|
384
|
+
app.mount(container)
|
|
385
|
+
const role = container.querySelectorAll('input')[1] as HTMLInputElement
|
|
386
|
+
role.checked = true
|
|
387
|
+
role.dispatchEvent(new Event('change', { bubbles: true }))
|
|
388
|
+
expect(onChange).toHaveBeenCalledWith(['name', 'role'])
|
|
389
|
+
app.destroy()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('列设置支持恢复、保存顺序和宽度,并可重置', () => {
|
|
393
|
+
const saved: {
|
|
394
|
+
visibleColumnIds: string[]
|
|
395
|
+
columnOrder: string[]
|
|
396
|
+
columnWidths: Record<string, string | number>
|
|
397
|
+
} = {
|
|
398
|
+
visibleColumnIds: ['role', 'name'],
|
|
399
|
+
columnOrder: ['role', 'name'],
|
|
400
|
+
columnWidths: { name: '160px' }
|
|
401
|
+
}
|
|
402
|
+
const persistence: DataTableColumnSettingsPersistence = {
|
|
403
|
+
load: vi.fn(() => saved),
|
|
404
|
+
save: vi.fn((value: DataTableColumnSettings) => {
|
|
405
|
+
saved.visibleColumnIds = [...value.visibleColumnIds]
|
|
406
|
+
saved.columnOrder = [...value.columnOrder]
|
|
407
|
+
saved.columnWidths = { ...value.columnWidths }
|
|
408
|
+
}),
|
|
409
|
+
reset: vi.fn(() => {
|
|
410
|
+
saved.visibleColumnIds = ['name', 'role']
|
|
411
|
+
saved.columnOrder = ['name', 'role']
|
|
412
|
+
saved.columnWidths = {}
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
const settingsChanges: string[][] = []
|
|
416
|
+
const container = document.createElement('div')
|
|
417
|
+
const app = createVobs({
|
|
418
|
+
render: () => KitColumnSettings<User>({
|
|
419
|
+
columns,
|
|
420
|
+
persistence,
|
|
421
|
+
onSettingsChange: value => settingsChanges.push([...value.columnOrder])
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
app.mount(container)
|
|
425
|
+
|
|
426
|
+
expect([...container.querySelectorAll('.vobs-column-settings__layout-label')].map(label => label.textContent))
|
|
427
|
+
.toEqual(['Role', 'Name'])
|
|
428
|
+
expect((container.querySelector(
|
|
429
|
+
'.vobs-column-settings__layout-option[data-column-id="name"] .vobs-column-settings__width'
|
|
430
|
+
) as HTMLInputElement).value).toBe('160px')
|
|
431
|
+
|
|
432
|
+
;(container.querySelector('.vobs-column-settings__move--up:not(:disabled)') as HTMLButtonElement).click()
|
|
433
|
+
expect(persistence.save).toHaveBeenCalled()
|
|
434
|
+
expect(settingsChanges.at(-1)).toEqual(['name', 'role'])
|
|
435
|
+
|
|
436
|
+
const width = container.querySelector(
|
|
437
|
+
'.vobs-column-settings__layout-option[data-column-id="name"] .vobs-column-settings__width'
|
|
438
|
+
) as HTMLInputElement
|
|
439
|
+
width.value = '180px'
|
|
440
|
+
width.dispatchEvent(new Event('change', { bubbles: true }))
|
|
441
|
+
expect(persistence.save).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
442
|
+
columnWidths: { name: '180px' }
|
|
443
|
+
}))
|
|
444
|
+
|
|
445
|
+
;(container.querySelector('.vobs-column-settings__reset') as HTMLButtonElement).click()
|
|
446
|
+
expect(persistence.reset).toHaveBeenCalledOnce()
|
|
447
|
+
expect(settingsChanges.at(-1)).toEqual(['name', 'role'])
|
|
448
|
+
app.destroy()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('受控列设置变化会同步到同页数据表', async () => {
|
|
452
|
+
const settings = state<DataTableColumnSettings>({
|
|
453
|
+
visibleColumnIds: ['name', 'role'],
|
|
454
|
+
columnOrder: ['name', 'role'],
|
|
455
|
+
columnWidths: {}
|
|
456
|
+
})
|
|
457
|
+
const container = document.createElement('div')
|
|
458
|
+
const app = createVobs({
|
|
459
|
+
render: () => {
|
|
460
|
+
const root = createElement('div')
|
|
461
|
+
root.append(
|
|
462
|
+
KitColumnSettings<User>({
|
|
463
|
+
columns,
|
|
464
|
+
get settings() { return settings.value },
|
|
465
|
+
onSettingsChange: value => { settings.value = value }
|
|
466
|
+
}) as Node,
|
|
467
|
+
KitDataTable<User>({
|
|
468
|
+
columns,
|
|
469
|
+
rows: [{ id: 1, name: 'Ada', role: 'Admin' }],
|
|
470
|
+
get columnSettings() { return settings.value }
|
|
471
|
+
}) as Node
|
|
472
|
+
)
|
|
473
|
+
return root
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
app.mount(container)
|
|
477
|
+
|
|
478
|
+
const width = container.querySelector('.vobs-column-settings__width') as HTMLInputElement
|
|
479
|
+
width.value = '180px'
|
|
480
|
+
width.dispatchEvent(new Event('change', { bubbles: true }))
|
|
481
|
+
await Promise.resolve()
|
|
482
|
+
expect(container.querySelector('thead th[data-column-id="name"]')?.getAttribute('style'))
|
|
483
|
+
.toContain('width: 180px')
|
|
484
|
+
app.destroy()
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('列设置持久化工厂支持存取和重置', () => {
|
|
488
|
+
const values = new Map<string, unknown>()
|
|
489
|
+
const get = vi.fn((key: string) => values.get(key) ?? null)
|
|
490
|
+
const set = vi.fn((key: string, value: unknown) => { values.set(key, value) })
|
|
491
|
+
const remove = vi.fn((key: string) => { values.delete(key) })
|
|
492
|
+
const storage: DataTableColumnSettingsStorage = {
|
|
493
|
+
get<T>(key: string): T | null {
|
|
494
|
+
return get(key) as T | null
|
|
495
|
+
},
|
|
496
|
+
set<T>(key: string, value: T): void {
|
|
497
|
+
set(key, value)
|
|
498
|
+
},
|
|
499
|
+
remove
|
|
500
|
+
}
|
|
501
|
+
const persistence = createColumnSettingsPersistence(storage, 'users.columns')
|
|
502
|
+
const settings = {
|
|
503
|
+
visibleColumnIds: ['name'],
|
|
504
|
+
columnOrder: ['name', 'role'],
|
|
505
|
+
columnWidths: { name: 160 }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
persistence.save(settings)
|
|
509
|
+
expect(persistence.load()).toEqual(settings)
|
|
510
|
+
persistence.reset?.()
|
|
511
|
+
expect(persistence.load()).toBeNull()
|
|
512
|
+
expect(set).toHaveBeenCalledWith('users.columns', settings)
|
|
513
|
+
expect(remove).toHaveBeenCalledWith('users.columns')
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('本地持久化在损坏值和无浏览器存储时回退为空设置', () => {
|
|
517
|
+
const values = new Map<string, string>([['users.columns', '{bad json']])
|
|
518
|
+
const storage = {
|
|
519
|
+
getItem: (key: string) => values.get(key) ?? null,
|
|
520
|
+
setItem: (key: string, value: string) => { values.set(key, value) },
|
|
521
|
+
removeItem: (key: string) => { values.delete(key) }
|
|
522
|
+
}
|
|
523
|
+
const persistence = createLocalColumnSettingsPersistence('users.columns', storage)
|
|
524
|
+
expect(persistence.load()).toBeUndefined()
|
|
525
|
+
|
|
526
|
+
const settings = {
|
|
527
|
+
visibleColumnIds: ['name'],
|
|
528
|
+
columnOrder: ['name'],
|
|
529
|
+
columnWidths: {}
|
|
530
|
+
}
|
|
531
|
+
persistence.save(settings)
|
|
532
|
+
expect(persistence.load()).toEqual(settings)
|
|
533
|
+
persistence.reset?.()
|
|
534
|
+
expect(persistence.load()).toBeUndefined()
|
|
535
|
+
})
|
|
536
|
+
})
|