@contember/bindx-dataview 0.1.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/dist/DataGrid.d.ts +38 -0
- package/dist/DataGrid.d.ts.map +1 -0
- package/dist/DataViewContext.d.ts +39 -0
- package/dist/DataViewContext.d.ts.map +1 -0
- package/dist/DataViewKeyProvider.d.ts +8 -0
- package/dist/DataViewKeyProvider.d.ts.map +1 -0
- package/dist/HasManyDataGrid.d.ts +51 -0
- package/dist/HasManyDataGrid.d.ts.map +1 -0
- package/dist/columnLeaf.d.ts +72 -0
- package/dist/columnLeaf.d.ts.map +1 -0
- package/dist/columnTypes.d.ts +35 -0
- package/dist/columnTypes.d.ts.map +1 -0
- package/dist/columns.d.ts +115 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/contextHooks.d.ts +45 -0
- package/dist/contextHooks.d.ts.map +1 -0
- package/dist/createColumn.d.ts +37 -0
- package/dist/createColumn.d.ts.map +1 -0
- package/dist/createRelationColumn.d.ts +57 -0
- package/dist/createRelationColumn.d.ts.map +1 -0
- package/dist/dataAttribute.d.ts +6 -0
- package/dist/dataAttribute.d.ts.map +1 -0
- package/dist/export.d.ts +46 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/filterComponents.d.ts +127 -0
- package/dist/filterComponents.d.ts.map +1 -0
- package/dist/filterContext.d.ts +10 -0
- package/dist/filterContext.d.ts.map +1 -0
- package/dist/filterHooks.d.ts +74 -0
- package/dist/filterHooks.d.ts.map +1 -0
- package/dist/filterScope.d.ts +54 -0
- package/dist/filterScope.d.ts.map +1 -0
- package/dist/filterWrappers.d.ts +66 -0
- package/dist/filterWrappers.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/infiniteLoad.d.ts +32 -0
- package/dist/infiniteLoad.d.ts.map +1 -0
- package/dist/loaderComponents.d.ts +75 -0
- package/dist/loaderComponents.d.ts.map +1 -0
- package/dist/markers.d.ts +36 -0
- package/dist/markers.d.ts.map +1 -0
- package/dist/pagingComponents.d.ts +50 -0
- package/dist/pagingComponents.d.ts.map +1 -0
- package/dist/renderCellValue.d.ts +9 -0
- package/dist/renderCellValue.d.ts.map +1 -0
- package/dist/select/MultiSelect.d.ts +27 -0
- package/dist/select/MultiSelect.d.ts.map +1 -0
- package/dist/select/Select.d.ts +27 -0
- package/dist/select/Select.d.ts.map +1 -0
- package/dist/select/SelectDataView.d.ts +46 -0
- package/dist/select/SelectDataView.d.ts.map +1 -0
- package/dist/select/SelectEachValue.d.ts +20 -0
- package/dist/select/SelectEachValue.d.ts.map +1 -0
- package/dist/select/SelectItemTrigger.d.ts +25 -0
- package/dist/select/SelectItemTrigger.d.ts.map +1 -0
- package/dist/select/SelectOption.d.ts +23 -0
- package/dist/select/SelectOption.d.ts.map +1 -0
- package/dist/select/SelectPlaceholder.d.ts +16 -0
- package/dist/select/SelectPlaceholder.d.ts.map +1 -0
- package/dist/select/index.d.ts +9 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/selectContext.d.ts +20 -0
- package/dist/select/selectContext.d.ts.map +1 -0
- package/dist/selectionComponents.d.ts +62 -0
- package/dist/selectionComponents.d.ts.map +1 -0
- package/dist/sortingComponents.d.ts +39 -0
- package/dist/sortingComponents.d.ts.map +1 -0
- package/dist/stateStorage.d.ts +20 -0
- package/dist/stateStorage.d.ts.map +1 -0
- package/dist/useDataGridSetup.d.ts +53 -0
- package/dist/useDataGridSetup.d.ts.map +1 -0
- package/dist/useDataView.d.ts +57 -0
- package/dist/useDataView.d.ts.map +1 -0
- package/dist/useDataViewState.d.ts +89 -0
- package/dist/useDataViewState.d.ts.map +1 -0
- package/package.json +33 -0
- package/src/DataGrid.tsx +173 -0
- package/src/DataViewContext.tsx +56 -0
- package/src/DataViewKeyProvider.tsx +16 -0
- package/src/HasManyDataGrid.tsx +319 -0
- package/src/columnLeaf.ts +148 -0
- package/src/columnTypes.ts +160 -0
- package/src/columns.tsx +306 -0
- package/src/contextHooks.ts +157 -0
- package/src/createColumn.ts +128 -0
- package/src/createRelationColumn.tsx +203 -0
- package/src/dataAttribute.ts +7 -0
- package/src/export.tsx +219 -0
- package/src/filterComponents.tsx +617 -0
- package/src/filterContext.ts +27 -0
- package/src/filterHooks.ts +386 -0
- package/src/filterScope.tsx +89 -0
- package/src/filterWrappers.tsx +187 -0
- package/src/index.ts +356 -0
- package/src/infiniteLoad.tsx +188 -0
- package/src/loaderComponents.tsx +239 -0
- package/src/markers.ts +54 -0
- package/src/pagingComponents.tsx +149 -0
- package/src/select/MultiSelect.tsx +88 -0
- package/src/select/Select.tsx +88 -0
- package/src/select/SelectDataView.tsx +231 -0
- package/src/select/SelectEachValue.tsx +34 -0
- package/src/select/SelectItemTrigger.tsx +51 -0
- package/src/select/SelectOption.tsx +40 -0
- package/src/select/SelectPlaceholder.tsx +22 -0
- package/src/select/index.ts +17 -0
- package/src/select/selectContext.ts +82 -0
- package/src/selectionComponents.tsx +160 -0
- package/src/sortingComponents.tsx +104 -0
- package/src/stateStorage.ts +188 -0
- package/src/useDataGridSetup.ts +230 -0
- package/src/useDataView.ts +148 -0
- package/src/useDataViewState.ts +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State management hooks for DataView filtering, sorting, paging, and selection.
|
|
3
|
+
*
|
|
4
|
+
* All hooks support optional state storage (URL, session, local, custom)
|
|
5
|
+
* for persisting state across navigations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useMemo } from 'react'
|
|
9
|
+
import type {
|
|
10
|
+
FilterHandler,
|
|
11
|
+
FilterArtifact,
|
|
12
|
+
SortingState,
|
|
13
|
+
SortingDirections,
|
|
14
|
+
SortingDirectionAction,
|
|
15
|
+
PagingState,
|
|
16
|
+
PagingInfo,
|
|
17
|
+
OrderDirection,
|
|
18
|
+
DataViewLayout,
|
|
19
|
+
SelectionValues,
|
|
20
|
+
SelectionState,
|
|
21
|
+
FieldRefBase,
|
|
22
|
+
} from '@contember/bindx'
|
|
23
|
+
import { FIELD_REF_META } from '@contember/bindx'
|
|
24
|
+
import { useStoredState, type StateStorageOrName } from './stateStorage.js'
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Filter State
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export interface RegisteredFilter {
|
|
31
|
+
readonly name: string
|
|
32
|
+
readonly handler: FilterHandler<FilterArtifact>
|
|
33
|
+
artifact: FilterArtifact
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FilteringState {
|
|
37
|
+
readonly filters: ReadonlyMap<string, RegisteredFilter>
|
|
38
|
+
getArtifact(name: string): FilterArtifact | undefined
|
|
39
|
+
setArtifact(name: string, artifact: FilterArtifact): void
|
|
40
|
+
resetFilter(name: string): void
|
|
41
|
+
resetAll(): void
|
|
42
|
+
readonly hasActiveFilters: boolean
|
|
43
|
+
readonly resolvedWhere: Record<string, unknown> | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UseFilteringOptions {
|
|
47
|
+
filters: ReadonlyMap<string, { handler: FilterHandler<FilterArtifact>; initialArtifact?: FilterArtifact }>
|
|
48
|
+
/** State storage backend. Default: 'null' (no persistence) */
|
|
49
|
+
stateStorage?: StateStorageOrName
|
|
50
|
+
/** Storage key prefix */
|
|
51
|
+
storageKey?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useFilteringState(options: UseFilteringOptions): FilteringState {
|
|
55
|
+
const { filters: filterDefs, stateStorage = 'null', storageKey = 'dataview' } = options
|
|
56
|
+
|
|
57
|
+
const defaultArtifacts = useMemo((): Record<string, FilterArtifact> => {
|
|
58
|
+
const result: Record<string, FilterArtifact> = {}
|
|
59
|
+
for (const [name, def] of filterDefs) {
|
|
60
|
+
result[name] = def.initialArtifact ?? def.handler.defaultArtifact()
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}, [filterDefs])
|
|
64
|
+
|
|
65
|
+
const [artifacts, setArtifacts] = useStoredState<Record<string, FilterArtifact>>(
|
|
66
|
+
stateStorage,
|
|
67
|
+
[storageKey, 'filters'],
|
|
68
|
+
(stored) => stored ?? defaultArtifacts,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const getArtifact = useCallback(
|
|
72
|
+
(name: string): FilterArtifact | undefined => artifacts[name],
|
|
73
|
+
[artifacts],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const setArtifact = useCallback(
|
|
77
|
+
(name: string, artifact: FilterArtifact): void => {
|
|
78
|
+
setArtifacts({ ...artifacts, [name]: artifact })
|
|
79
|
+
},
|
|
80
|
+
[artifacts, setArtifacts],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const resetFilter = useCallback(
|
|
84
|
+
(name: string): void => {
|
|
85
|
+
const def = filterDefs.get(name)
|
|
86
|
+
if (!def) return
|
|
87
|
+
setArtifacts({ ...artifacts, [name]: def.handler.defaultArtifact() })
|
|
88
|
+
},
|
|
89
|
+
[filterDefs, artifacts, setArtifacts],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const resetAll = useCallback((): void => {
|
|
93
|
+
setArtifacts(defaultArtifacts)
|
|
94
|
+
}, [defaultArtifacts, setArtifacts])
|
|
95
|
+
|
|
96
|
+
const hasActiveFilters = useMemo((): boolean => {
|
|
97
|
+
for (const [name, def] of filterDefs) {
|
|
98
|
+
const artifact = artifacts[name]
|
|
99
|
+
if (artifact && def.handler.isActive(artifact)) {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}, [filterDefs, artifacts])
|
|
105
|
+
|
|
106
|
+
const resolvedWhere = useMemo((): Record<string, unknown> | undefined => {
|
|
107
|
+
const conditions: Record<string, unknown>[] = []
|
|
108
|
+
for (const [name, def] of filterDefs) {
|
|
109
|
+
const artifact = artifacts[name]
|
|
110
|
+
if (!artifact) continue
|
|
111
|
+
const where = def.handler.toWhere(artifact)
|
|
112
|
+
if (where) conditions.push(where)
|
|
113
|
+
}
|
|
114
|
+
if (conditions.length === 0) return undefined
|
|
115
|
+
if (conditions.length === 1) return conditions[0]
|
|
116
|
+
return { and: conditions }
|
|
117
|
+
}, [filterDefs, artifacts])
|
|
118
|
+
|
|
119
|
+
const filters = useMemo((): ReadonlyMap<string, RegisteredFilter> => {
|
|
120
|
+
const map = new Map<string, RegisteredFilter>()
|
|
121
|
+
for (const [name, def] of filterDefs) {
|
|
122
|
+
map.set(name, {
|
|
123
|
+
name,
|
|
124
|
+
handler: def.handler,
|
|
125
|
+
artifact: artifacts[name] ?? def.handler.defaultArtifact(),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
return map
|
|
129
|
+
}, [filterDefs, artifacts])
|
|
130
|
+
|
|
131
|
+
return { filters, getArtifact, setArtifact, resetFilter, resetAll, hasActiveFilters, resolvedWhere }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Sorting State (Multi-Column)
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
export interface UseSortingOptions {
|
|
139
|
+
sortableFields: ReadonlySet<string>
|
|
140
|
+
initialSorting?: SortingDirections
|
|
141
|
+
stateStorage?: StateStorageOrName
|
|
142
|
+
storageKey?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface SortingStateResult {
|
|
146
|
+
readonly state: SortingState
|
|
147
|
+
setOrderBy<T>(field: FieldRefBase<T>, action: SortingDirectionAction, append?: boolean): void
|
|
148
|
+
clear(): void
|
|
149
|
+
directionOf<T>(field: FieldRefBase<T>): OrderDirection | null
|
|
150
|
+
readonly resolvedOrderBy: readonly Record<string, unknown>[] | undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveSortAction(
|
|
154
|
+
currentDirection: OrderDirection | null,
|
|
155
|
+
action: SortingDirectionAction,
|
|
156
|
+
): OrderDirection | null {
|
|
157
|
+
switch (action) {
|
|
158
|
+
case 'next':
|
|
159
|
+
if (currentDirection === null) return 'asc'
|
|
160
|
+
if (currentDirection === 'asc') return 'desc'
|
|
161
|
+
return null
|
|
162
|
+
case 'toggleAsc':
|
|
163
|
+
return currentDirection === 'asc' ? null : 'asc'
|
|
164
|
+
case 'toggleDesc':
|
|
165
|
+
return currentDirection === 'desc' ? null : 'desc'
|
|
166
|
+
case 'clear':
|
|
167
|
+
return null
|
|
168
|
+
default:
|
|
169
|
+
return action
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useSortingState(options: UseSortingOptions): SortingStateResult {
|
|
174
|
+
const { sortableFields, initialSorting, stateStorage = 'null', storageKey = 'dataview' } = options
|
|
175
|
+
|
|
176
|
+
const [directions, setDirections] = useStoredState<SortingDirections>(
|
|
177
|
+
stateStorage,
|
|
178
|
+
[storageKey, 'sorting'],
|
|
179
|
+
(stored) => stored ?? initialSorting ?? {},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const state = useMemo((): SortingState => ({ directions }), [directions])
|
|
183
|
+
|
|
184
|
+
const setOrderBy = useCallback(
|
|
185
|
+
<T>(field: FieldRefBase<T>, action: SortingDirectionAction, append?: boolean): void => {
|
|
186
|
+
const fieldName = field[FIELD_REF_META].fieldName
|
|
187
|
+
if (!sortableFields.has(fieldName)) return
|
|
188
|
+
const currentDir = directions[fieldName] ?? null
|
|
189
|
+
const newDir = resolveSortAction(currentDir, action)
|
|
190
|
+
|
|
191
|
+
if (append) {
|
|
192
|
+
const next = { ...directions }
|
|
193
|
+
if (newDir) {
|
|
194
|
+
next[fieldName] = newDir
|
|
195
|
+
} else {
|
|
196
|
+
delete next[fieldName]
|
|
197
|
+
}
|
|
198
|
+
setDirections(next)
|
|
199
|
+
} else if (newDir) {
|
|
200
|
+
setDirections({ [fieldName]: newDir })
|
|
201
|
+
} else {
|
|
202
|
+
setDirections({})
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
[sortableFields, directions, setDirections],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const clear = useCallback((): void => {
|
|
209
|
+
setDirections({})
|
|
210
|
+
}, [setDirections])
|
|
211
|
+
|
|
212
|
+
const directionOf = useCallback(
|
|
213
|
+
<T>(field: FieldRefBase<T>): OrderDirection | null => directions[field[FIELD_REF_META].fieldName] ?? null,
|
|
214
|
+
[directions],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const resolvedOrderBy = useMemo((): readonly Record<string, unknown>[] | undefined => {
|
|
218
|
+
const entries = Object.entries(directions)
|
|
219
|
+
if (entries.length === 0) return undefined
|
|
220
|
+
return entries.map(([field, dir]) => buildNestedOrderBy(field, dir))
|
|
221
|
+
}, [directions])
|
|
222
|
+
|
|
223
|
+
return { state, setOrderBy, clear, directionOf, resolvedOrderBy }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildNestedOrderBy(fieldPath: string, direction: OrderDirection): Record<string, unknown> {
|
|
227
|
+
const parts = fieldPath.split('.')
|
|
228
|
+
let result: Record<string, unknown> = { [parts[parts.length - 1]!]: direction }
|
|
229
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
230
|
+
result = { [parts[i]!]: result }
|
|
231
|
+
}
|
|
232
|
+
return result
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Paging State
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
export interface UsePagingOptions {
|
|
240
|
+
/** Initial items per page. null = show all. Default: 50 */
|
|
241
|
+
initialItemsPerPage?: number | null
|
|
242
|
+
/** State storage for current page */
|
|
243
|
+
currentPageStateStorage?: StateStorageOrName
|
|
244
|
+
/** State storage for page size preference */
|
|
245
|
+
pagingSettingsStorage?: StateStorageOrName
|
|
246
|
+
/** Storage key prefix */
|
|
247
|
+
storageKey?: string
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface PagingStateResult {
|
|
251
|
+
readonly state: PagingState
|
|
252
|
+
readonly info: PagingInfo
|
|
253
|
+
readonly hasPrevious: boolean
|
|
254
|
+
readonly hasNext: boolean
|
|
255
|
+
goTo(page: number): void
|
|
256
|
+
next(): void
|
|
257
|
+
previous(): void
|
|
258
|
+
first(): void
|
|
259
|
+
last(): void
|
|
260
|
+
setItemsPerPage(count: number | null): void
|
|
261
|
+
setTotalCount(count: number): void
|
|
262
|
+
refreshTotalCount(): void
|
|
263
|
+
readonly queryLimit: number | undefined
|
|
264
|
+
readonly queryOffset: number | undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function usePagingState(options: UsePagingOptions = {}): PagingStateResult {
|
|
268
|
+
const {
|
|
269
|
+
initialItemsPerPage = 50,
|
|
270
|
+
currentPageStateStorage = 'null',
|
|
271
|
+
pagingSettingsStorage = 'null',
|
|
272
|
+
storageKey = 'dataview',
|
|
273
|
+
} = options
|
|
274
|
+
|
|
275
|
+
const [pageIndex, setPageIndex] = useStoredState<number>(
|
|
276
|
+
currentPageStateStorage,
|
|
277
|
+
[storageKey, 'pageIndex'],
|
|
278
|
+
(stored) => stored ?? 0,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const [itemsPerPage, setItemsPerPageRaw] = useStoredState<number | null>(
|
|
282
|
+
pagingSettingsStorage,
|
|
283
|
+
[storageKey, 'itemsPerPage'],
|
|
284
|
+
(stored) => stored ?? initialItemsPerPage,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
const state = useMemo((): PagingState => ({
|
|
288
|
+
pageIndex,
|
|
289
|
+
itemsPerPage: itemsPerPage ?? 0,
|
|
290
|
+
}), [pageIndex, itemsPerPage])
|
|
291
|
+
|
|
292
|
+
const [totalCount, setTotalCount] = useState<number | null>(null)
|
|
293
|
+
const [refreshCounter, setRefreshCounter] = useState(0)
|
|
294
|
+
|
|
295
|
+
const totalPages = useMemo((): number | null => {
|
|
296
|
+
if (totalCount === null || itemsPerPage === null) return null
|
|
297
|
+
return Math.max(1, Math.ceil(totalCount / itemsPerPage))
|
|
298
|
+
}, [totalCount, itemsPerPage])
|
|
299
|
+
|
|
300
|
+
const info: PagingInfo = useMemo(() => ({
|
|
301
|
+
totalCount,
|
|
302
|
+
totalPages,
|
|
303
|
+
}), [totalCount, totalPages])
|
|
304
|
+
|
|
305
|
+
const hasPrevious = pageIndex > 0
|
|
306
|
+
const hasNext = itemsPerPage === null ? false : (totalPages !== null ? pageIndex < totalPages - 1 : true)
|
|
307
|
+
|
|
308
|
+
const goTo = useCallback(
|
|
309
|
+
(page: number): void => setPageIndex(Math.max(0, page)),
|
|
310
|
+
[setPageIndex],
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const next = useCallback((): void => setPageIndex(pageIndex + 1), [pageIndex, setPageIndex])
|
|
314
|
+
const previous = useCallback((): void => setPageIndex(Math.max(0, pageIndex - 1)), [pageIndex, setPageIndex])
|
|
315
|
+
const first = useCallback((): void => setPageIndex(0), [setPageIndex])
|
|
316
|
+
|
|
317
|
+
const last = useCallback((): void => {
|
|
318
|
+
if (totalPages === null) return
|
|
319
|
+
setPageIndex(Math.max(0, totalPages - 1))
|
|
320
|
+
}, [totalPages, setPageIndex])
|
|
321
|
+
|
|
322
|
+
const setItemsPerPage = useCallback(
|
|
323
|
+
(count: number | null): void => {
|
|
324
|
+
setItemsPerPageRaw(count)
|
|
325
|
+
setPageIndex(0)
|
|
326
|
+
},
|
|
327
|
+
[setItemsPerPageRaw, setPageIndex],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const refreshTotalCount = useCallback((): void => {
|
|
331
|
+
setRefreshCounter(c => c + 1)
|
|
332
|
+
setTotalCount(null)
|
|
333
|
+
}, [])
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
state,
|
|
337
|
+
info,
|
|
338
|
+
hasPrevious,
|
|
339
|
+
hasNext,
|
|
340
|
+
goTo,
|
|
341
|
+
next,
|
|
342
|
+
previous,
|
|
343
|
+
first,
|
|
344
|
+
last,
|
|
345
|
+
setItemsPerPage,
|
|
346
|
+
setTotalCount,
|
|
347
|
+
refreshTotalCount,
|
|
348
|
+
queryLimit: itemsPerPage ?? undefined,
|
|
349
|
+
queryOffset: itemsPerPage === null ? undefined : pageIndex * itemsPerPage,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ============================================================================
|
|
354
|
+
// Selection State (Column Visibility + Layout)
|
|
355
|
+
// ============================================================================
|
|
356
|
+
|
|
357
|
+
export interface UseSelectionOptions {
|
|
358
|
+
layouts?: readonly DataViewLayout[]
|
|
359
|
+
initialSelection?: SelectionValues
|
|
360
|
+
stateStorage?: StateStorageOrName
|
|
361
|
+
storageKey?: string
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface SelectionStateResult {
|
|
365
|
+
readonly state: SelectionState
|
|
366
|
+
setLayout(layout: string | undefined): void
|
|
367
|
+
setVisibility(name: string, visible: boolean | undefined): void
|
|
368
|
+
isVisible(name: string, fallback?: boolean): boolean
|
|
369
|
+
readonly currentLayout: string | undefined
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const DEFAULT_SELECTION: SelectionValues = { visibility: {} }
|
|
373
|
+
|
|
374
|
+
export function useSelectionState(options: UseSelectionOptions = {}): SelectionStateResult {
|
|
375
|
+
const { layouts = [], initialSelection, stateStorage = 'null', storageKey = 'dataview' } = options
|
|
376
|
+
|
|
377
|
+
const [values, setValues] = useStoredState<SelectionValues>(
|
|
378
|
+
stateStorage,
|
|
379
|
+
[storageKey, 'selection'],
|
|
380
|
+
(stored) => stored ?? initialSelection ?? DEFAULT_SELECTION,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
const state = useMemo((): SelectionState => ({ values, layouts }), [values, layouts])
|
|
384
|
+
|
|
385
|
+
const setLayout = useCallback((layout: string | undefined): void => {
|
|
386
|
+
setValues({ ...values, layout })
|
|
387
|
+
}, [values, setValues])
|
|
388
|
+
|
|
389
|
+
const setVisibility = useCallback((name: string, visible: boolean | undefined): void => {
|
|
390
|
+
const next = { ...values.visibility }
|
|
391
|
+
if (visible === undefined) {
|
|
392
|
+
delete next[name]
|
|
393
|
+
} else {
|
|
394
|
+
next[name] = visible
|
|
395
|
+
}
|
|
396
|
+
setValues({ ...values, visibility: next })
|
|
397
|
+
}, [values, setValues])
|
|
398
|
+
|
|
399
|
+
const isVisible = useCallback((name: string, fallback = true): boolean => {
|
|
400
|
+
return values.visibility[name] ?? fallback
|
|
401
|
+
}, [values.visibility])
|
|
402
|
+
|
|
403
|
+
return { state, setLayout, setVisibility, isVisible, currentLayout: values.layout }
|
|
404
|
+
}
|