@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.
Files changed (114) hide show
  1. package/dist/DataGrid.d.ts +38 -0
  2. package/dist/DataGrid.d.ts.map +1 -0
  3. package/dist/DataViewContext.d.ts +39 -0
  4. package/dist/DataViewContext.d.ts.map +1 -0
  5. package/dist/DataViewKeyProvider.d.ts +8 -0
  6. package/dist/DataViewKeyProvider.d.ts.map +1 -0
  7. package/dist/HasManyDataGrid.d.ts +51 -0
  8. package/dist/HasManyDataGrid.d.ts.map +1 -0
  9. package/dist/columnLeaf.d.ts +72 -0
  10. package/dist/columnLeaf.d.ts.map +1 -0
  11. package/dist/columnTypes.d.ts +35 -0
  12. package/dist/columnTypes.d.ts.map +1 -0
  13. package/dist/columns.d.ts +115 -0
  14. package/dist/columns.d.ts.map +1 -0
  15. package/dist/contextHooks.d.ts +45 -0
  16. package/dist/contextHooks.d.ts.map +1 -0
  17. package/dist/createColumn.d.ts +37 -0
  18. package/dist/createColumn.d.ts.map +1 -0
  19. package/dist/createRelationColumn.d.ts +57 -0
  20. package/dist/createRelationColumn.d.ts.map +1 -0
  21. package/dist/dataAttribute.d.ts +6 -0
  22. package/dist/dataAttribute.d.ts.map +1 -0
  23. package/dist/export.d.ts +46 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/filterComponents.d.ts +127 -0
  26. package/dist/filterComponents.d.ts.map +1 -0
  27. package/dist/filterContext.d.ts +10 -0
  28. package/dist/filterContext.d.ts.map +1 -0
  29. package/dist/filterHooks.d.ts +74 -0
  30. package/dist/filterHooks.d.ts.map +1 -0
  31. package/dist/filterScope.d.ts +54 -0
  32. package/dist/filterScope.d.ts.map +1 -0
  33. package/dist/filterWrappers.d.ts +66 -0
  34. package/dist/filterWrappers.d.ts.map +1 -0
  35. package/dist/index.d.ts +28 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/infiniteLoad.d.ts +32 -0
  38. package/dist/infiniteLoad.d.ts.map +1 -0
  39. package/dist/loaderComponents.d.ts +75 -0
  40. package/dist/loaderComponents.d.ts.map +1 -0
  41. package/dist/markers.d.ts +36 -0
  42. package/dist/markers.d.ts.map +1 -0
  43. package/dist/pagingComponents.d.ts +50 -0
  44. package/dist/pagingComponents.d.ts.map +1 -0
  45. package/dist/renderCellValue.d.ts +9 -0
  46. package/dist/renderCellValue.d.ts.map +1 -0
  47. package/dist/select/MultiSelect.d.ts +27 -0
  48. package/dist/select/MultiSelect.d.ts.map +1 -0
  49. package/dist/select/Select.d.ts +27 -0
  50. package/dist/select/Select.d.ts.map +1 -0
  51. package/dist/select/SelectDataView.d.ts +46 -0
  52. package/dist/select/SelectDataView.d.ts.map +1 -0
  53. package/dist/select/SelectEachValue.d.ts +20 -0
  54. package/dist/select/SelectEachValue.d.ts.map +1 -0
  55. package/dist/select/SelectItemTrigger.d.ts +25 -0
  56. package/dist/select/SelectItemTrigger.d.ts.map +1 -0
  57. package/dist/select/SelectOption.d.ts +23 -0
  58. package/dist/select/SelectOption.d.ts.map +1 -0
  59. package/dist/select/SelectPlaceholder.d.ts +16 -0
  60. package/dist/select/SelectPlaceholder.d.ts.map +1 -0
  61. package/dist/select/index.d.ts +9 -0
  62. package/dist/select/index.d.ts.map +1 -0
  63. package/dist/select/selectContext.d.ts +20 -0
  64. package/dist/select/selectContext.d.ts.map +1 -0
  65. package/dist/selectionComponents.d.ts +62 -0
  66. package/dist/selectionComponents.d.ts.map +1 -0
  67. package/dist/sortingComponents.d.ts +39 -0
  68. package/dist/sortingComponents.d.ts.map +1 -0
  69. package/dist/stateStorage.d.ts +20 -0
  70. package/dist/stateStorage.d.ts.map +1 -0
  71. package/dist/useDataGridSetup.d.ts +53 -0
  72. package/dist/useDataGridSetup.d.ts.map +1 -0
  73. package/dist/useDataView.d.ts +57 -0
  74. package/dist/useDataView.d.ts.map +1 -0
  75. package/dist/useDataViewState.d.ts +89 -0
  76. package/dist/useDataViewState.d.ts.map +1 -0
  77. package/package.json +33 -0
  78. package/src/DataGrid.tsx +173 -0
  79. package/src/DataViewContext.tsx +56 -0
  80. package/src/DataViewKeyProvider.tsx +16 -0
  81. package/src/HasManyDataGrid.tsx +319 -0
  82. package/src/columnLeaf.ts +148 -0
  83. package/src/columnTypes.ts +160 -0
  84. package/src/columns.tsx +306 -0
  85. package/src/contextHooks.ts +157 -0
  86. package/src/createColumn.ts +128 -0
  87. package/src/createRelationColumn.tsx +203 -0
  88. package/src/dataAttribute.ts +7 -0
  89. package/src/export.tsx +219 -0
  90. package/src/filterComponents.tsx +617 -0
  91. package/src/filterContext.ts +27 -0
  92. package/src/filterHooks.ts +386 -0
  93. package/src/filterScope.tsx +89 -0
  94. package/src/filterWrappers.tsx +187 -0
  95. package/src/index.ts +356 -0
  96. package/src/infiniteLoad.tsx +188 -0
  97. package/src/loaderComponents.tsx +239 -0
  98. package/src/markers.ts +54 -0
  99. package/src/pagingComponents.tsx +149 -0
  100. package/src/select/MultiSelect.tsx +88 -0
  101. package/src/select/Select.tsx +88 -0
  102. package/src/select/SelectDataView.tsx +231 -0
  103. package/src/select/SelectEachValue.tsx +34 -0
  104. package/src/select/SelectItemTrigger.tsx +51 -0
  105. package/src/select/SelectOption.tsx +40 -0
  106. package/src/select/SelectPlaceholder.tsx +22 -0
  107. package/src/select/index.ts +17 -0
  108. package/src/select/selectContext.ts +82 -0
  109. package/src/selectionComponents.tsx +160 -0
  110. package/src/sortingComponents.tsx +104 -0
  111. package/src/stateStorage.ts +188 -0
  112. package/src/useDataGridSetup.ts +230 -0
  113. package/src/useDataView.ts +148 -0
  114. 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
+ }