@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,104 @@
1
+ /**
2
+ * Composable sorting UI components using Radix Slot pattern.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * <DataViewSortingTrigger field={it.title}>
7
+ * <button>Sort by Title</button>
8
+ * </DataViewSortingTrigger>
9
+ *
10
+ * <DataViewSortingSwitch field={it.title}
11
+ * asc={<span>↑</span>}
12
+ * desc={<span>↓</span>}
13
+ * none={<span>↕</span>}
14
+ * />
15
+ * ```
16
+ */
17
+
18
+ import React, { forwardRef, type ReactElement, useCallback } from 'react'
19
+ import { Slot } from '@radix-ui/react-slot'
20
+ import { composeEventHandlers } from '@radix-ui/primitive'
21
+ import { useDataViewContext } from './DataViewContext.js'
22
+ import { dataAttribute } from './dataAttribute.js'
23
+ import type { SortingDirectionAction, OrderDirection, FieldRefBase } from '@contember/bindx'
24
+
25
+ // ============================================================================
26
+ // DataViewSortingTrigger
27
+ // ============================================================================
28
+
29
+ export interface DataViewSortingTriggerProps<T> {
30
+ /** Field to sort by */
31
+ field: FieldRefBase<T>
32
+ /** Sorting action. Default: 'next' (cycles asc → desc → none) */
33
+ action?: SortingDirectionAction
34
+ /** Button element to render */
35
+ children: ReactElement
36
+ }
37
+
38
+ const actionToState: Record<Exclude<SortingDirectionAction, OrderDirection>, OrderDirection | null> = {
39
+ next: null,
40
+ toggleAsc: 'asc',
41
+ toggleDesc: 'desc',
42
+ clear: null,
43
+ }
44
+
45
+ export const DataViewSortingTrigger = forwardRef<HTMLButtonElement, DataViewSortingTriggerProps<unknown>>(
46
+ ({ action = 'next', field, ...props }, ref) => {
47
+ const { sorting } = useDataViewContext()
48
+ const orderDirection = sorting.directionOf(field)
49
+
50
+ const changeOrder = useCallback(
51
+ (e: React.MouseEvent): void => {
52
+ sorting.setOrderBy(field, action, e.ctrlKey || e.metaKey)
53
+ },
54
+ [field, action, sorting],
55
+ )
56
+
57
+ const active = !!action
58
+ && action !== 'next'
59
+ && action !== 'clear'
60
+ && orderDirection === (action === 'toggleAsc' ? 'asc' : action === 'toggleDesc' ? 'desc' : action)
61
+
62
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
63
+
64
+ return (
65
+ <Slot
66
+ ref={ref}
67
+ onClick={composeEventHandlers(onClick, changeOrder)}
68
+ data-active={dataAttribute(active)}
69
+ data-current={orderDirection ?? 'none'}
70
+ {...otherProps}
71
+ />
72
+ )
73
+ },
74
+ ) as <T>(props: DataViewSortingTriggerProps<T> & React.RefAttributes<HTMLButtonElement>) => ReactElement | null
75
+ ;(DataViewSortingTrigger as React.FC).displayName = 'DataViewSortingTrigger'
76
+
77
+ // ============================================================================
78
+ // DataViewSortingSwitch
79
+ // ============================================================================
80
+
81
+ export interface DataViewSortingSwitchProps<T> {
82
+ /** Field to check sorting direction for */
83
+ field: FieldRefBase<T>
84
+ /** Render when sorted ascending */
85
+ asc?: React.ReactNode
86
+ /** Render when sorted descending */
87
+ desc?: React.ReactNode
88
+ /** Render when not sorted */
89
+ none?: React.ReactNode
90
+ }
91
+
92
+ export function DataViewSortingSwitch<T>({
93
+ field,
94
+ asc,
95
+ desc,
96
+ none,
97
+ }: DataViewSortingSwitchProps<T>): ReactElement {
98
+ const { sorting } = useDataViewContext()
99
+ const direction = sorting.directionOf(field)
100
+
101
+ if (direction === 'asc') return <>{asc ?? null}</>
102
+ if (direction === 'desc') return <>{desc ?? null}</>
103
+ return <>{none ?? null}</>
104
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * State storage abstraction for DataView state persistence.
3
+ *
4
+ * Supports URL params, sessionStorage, localStorage, or custom backends.
5
+ *
6
+ * Usage:
7
+ * ```tsx
8
+ * const [value, setValue] = useStoredState('url', ['myGrid', 'sorting'], initialValue)
9
+ * ```
10
+ */
11
+
12
+ import { useState, useCallback, useMemo } from 'react'
13
+
14
+ // ============================================================================
15
+ // Storage Interface
16
+ // ============================================================================
17
+
18
+ export interface StateStorage {
19
+ get<T>(key: string): T | undefined
20
+ set<T>(key: string, value: T): void
21
+ remove(key: string): void
22
+ }
23
+
24
+ export type StateStorageOrName = 'url' | 'session' | 'local' | 'null' | StateStorage
25
+
26
+ // ============================================================================
27
+ // Built-in Storage Backends
28
+ // ============================================================================
29
+
30
+ const nullStorage: StateStorage = {
31
+ get: () => undefined,
32
+ set: () => {},
33
+ remove: () => {},
34
+ }
35
+
36
+ function createWebStorage(storage: Storage): StateStorage {
37
+ return {
38
+ get<T>(key: string): T | undefined {
39
+ try {
40
+ const raw = storage.getItem(key)
41
+ return raw ? JSON.parse(raw) : undefined
42
+ } catch {
43
+ return undefined
44
+ }
45
+ },
46
+ set<T>(key: string, value: T): void {
47
+ try {
48
+ storage.setItem(key, JSON.stringify(value))
49
+ } catch {
50
+ // Storage full or unavailable
51
+ }
52
+ },
53
+ remove(key: string): void {
54
+ try {
55
+ storage.removeItem(key)
56
+ } catch {
57
+ // Ignore
58
+ }
59
+ },
60
+ }
61
+ }
62
+
63
+ function createUrlStorage(): StateStorage {
64
+ return {
65
+ get<T>(key: string): T | undefined {
66
+ if (typeof window === 'undefined') return undefined
67
+ try {
68
+ const params = new URLSearchParams(window.location.search)
69
+ const raw = params.get(key)
70
+ return raw ? JSON.parse(raw) : undefined
71
+ } catch {
72
+ return undefined
73
+ }
74
+ },
75
+ set<T>(key: string, value: T): void {
76
+ if (typeof window === 'undefined') return
77
+ try {
78
+ const params = new URLSearchParams(window.location.search)
79
+ const serialized = JSON.stringify(value)
80
+ params.set(key, serialized)
81
+ const url = `${window.location.pathname}?${params.toString()}`
82
+ window.history.replaceState(null, '', url)
83
+ } catch {
84
+ // Ignore
85
+ }
86
+ },
87
+ remove(key: string): void {
88
+ if (typeof window === 'undefined') return
89
+ try {
90
+ const params = new URLSearchParams(window.location.search)
91
+ params.delete(key)
92
+ const search = params.toString()
93
+ const url = search ? `${window.location.pathname}?${search}` : window.location.pathname
94
+ window.history.replaceState(null, '', url)
95
+ } catch {
96
+ // Ignore
97
+ }
98
+ },
99
+ }
100
+ }
101
+
102
+ function resolveStorage(storageOrName: StateStorageOrName): StateStorage {
103
+ if (typeof storageOrName === 'object') return storageOrName
104
+
105
+ switch (storageOrName) {
106
+ case 'null':
107
+ return nullStorage
108
+ case 'url':
109
+ return createUrlStorage()
110
+ case 'session':
111
+ if (typeof sessionStorage !== 'undefined') {
112
+ return createWebStorage(sessionStorage)
113
+ }
114
+ return nullStorage
115
+ case 'local':
116
+ if (typeof localStorage !== 'undefined') {
117
+ return createWebStorage(localStorage)
118
+ }
119
+ return nullStorage
120
+ }
121
+ }
122
+
123
+ function createFallbackStorage(storages: StateStorage[]): StateStorage {
124
+ return {
125
+ get<T>(key: string): T | undefined {
126
+ for (const storage of storages) {
127
+ const value = storage.get<T>(key)
128
+ if (value !== undefined) return value
129
+ }
130
+ return undefined
131
+ },
132
+ set<T>(key: string, value: T): void {
133
+ for (const storage of storages) {
134
+ storage.set(key, value)
135
+ }
136
+ },
137
+ remove(key: string): void {
138
+ for (const storage of storages) {
139
+ storage.remove(key)
140
+ }
141
+ },
142
+ }
143
+ }
144
+
145
+ function resolveStorageOrFallback(storageOrName: StateStorageOrName | StateStorageOrName[]): StateStorage {
146
+ if (Array.isArray(storageOrName)) {
147
+ return createFallbackStorage(storageOrName.map(resolveStorage))
148
+ }
149
+ return resolveStorage(storageOrName)
150
+ }
151
+
152
+ // ============================================================================
153
+ // useStoredState Hook
154
+ // ============================================================================
155
+
156
+ export type SetStoredState<T> = (value: T | ((current: T) => T)) => void
157
+
158
+ export function useStoredState<T>(
159
+ storageOrName: StateStorageOrName | StateStorageOrName[],
160
+ key: readonly string[],
161
+ initializer: (storedValue: T | undefined) => T,
162
+ ): [T, SetStoredState<T>] {
163
+ const storage = useMemo(() => resolveStorageOrFallback(storageOrName), [storageOrName])
164
+ const storageKey = key.join(':')
165
+
166
+ const [value, setValueInternal] = useState<T>(() => {
167
+ const stored = storage.get<T>(storageKey)
168
+ return initializer(stored)
169
+ })
170
+
171
+ const setValue = useCallback((update: T | ((current: T) => T)): void => {
172
+ setValueInternal(current => {
173
+ const newValue = typeof update === 'function' ? (update as (current: T) => T)(current) : update
174
+ storage.set(storageKey, newValue)
175
+ return newValue
176
+ })
177
+ }, [storage, storageKey])
178
+
179
+ return [value, setValue]
180
+ }
181
+
182
+ // ============================================================================
183
+ // DataView Key
184
+ // ============================================================================
185
+
186
+ export function buildDataViewKey(parts: readonly string[]): string {
187
+ return parts.join(':')
188
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Shared setup logic for DataGrid and HasManyDataGrid.
3
+ * Handles collection phase (JSX analysis), state management, and filter building.
4
+ */
5
+
6
+ import React, { type ReactNode, useMemo } from 'react'
7
+ import type {
8
+ EntityAccessor,
9
+ OrderDirection,
10
+ FilterHandler,
11
+ FilterArtifact,
12
+ SortingDirections,
13
+ DataViewLayout,
14
+ SelectionMeta,
15
+ SelectionValues,
16
+ SchemaRegistry,
17
+ } from '@contember/bindx'
18
+ import { createFullTextFilterHandler, SelectionScope, buildQueryFromSelection } from '@contember/bindx'
19
+ import {
20
+ createCollectorProxy,
21
+ mergeSelections,
22
+ collectSelection,
23
+ } from '@contember/bindx-react'
24
+ import { ColumnLeaf, type ColumnLeafProps, analyzeChildren } from './columnLeaf.js'
25
+ import { DataGridToolbarContent, type DataGridToolbarContentProps } from './markers.js'
26
+ import { DataGridLayout, type DataGridLayoutProps } from './markers.js'
27
+ import { useFilteringState, useSortingState, usePagingState, useSelectionState } from './useDataViewState.js'
28
+ import type { StateStorageOrName } from './stateStorage.js'
29
+ import type { DataViewContextValue, DataViewElementData } from './DataViewContext.js'
30
+ import { DataViewElement, type DataViewElementProps } from './selectionComponents.js'
31
+
32
+ export const QUERY_FILTER_NAME = '__query'
33
+
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const MARKER_TYPES: ReadonlySet<React.ComponentType<any>> = new Set([
36
+ ColumnLeaf,
37
+ DataGridToolbarContent,
38
+ DataGridLayout,
39
+ ])
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ const ELEMENT_MARKER_TYPES: ReadonlySet<React.ComponentType<any>> = new Set([
43
+ DataViewElement,
44
+ ])
45
+
46
+ export interface DataGridCommonProps<TEntity extends object> {
47
+ children: (it: EntityAccessor<TEntity>) => ReactNode
48
+ initialSorting?: Partial<Record<string, OrderDirection>>
49
+ itemsPerPage?: number | null
50
+ filter?: Record<string, unknown>
51
+ layouts?: readonly DataViewLayout[]
52
+ initialSelection?: SelectionValues
53
+ stateStorage?: StateStorageOrName
54
+ currentPageStateStorage?: StateStorageOrName
55
+ pagingSettingsStorage?: StateStorageOrName
56
+ storageKey?: string
57
+ }
58
+
59
+ export interface DataGridSetupResult {
60
+ childrenJsx: ReactNode
61
+ columns: ColumnLeafProps[]
62
+ selection: SelectionMeta
63
+ queryKey: string
64
+ toolbarContent: ReactNode | undefined
65
+ layoutRenders: Map<string, (item: DataViewContextValue['items'][number]) => ReactNode>
66
+ layoutElements: Map<string, readonly DataViewElementData[]>
67
+ effectiveLayouts: readonly DataViewLayout[]
68
+ filtering: ReturnType<typeof useFilteringState>
69
+ sorting: ReturnType<typeof useSortingState>
70
+ paging: ReturnType<typeof usePagingState>
71
+ selectionState: ReturnType<typeof useSelectionState>
72
+ combinedFilter: Record<string, unknown> | undefined
73
+ }
74
+
75
+ export function useDataGridSetup<TEntity extends object>({
76
+ entityType,
77
+ schemaRegistry,
78
+ children,
79
+ initialSorting,
80
+ itemsPerPage = 50,
81
+ staticFilter,
82
+ layouts,
83
+ initialSelection,
84
+ stateStorage,
85
+ currentPageStateStorage,
86
+ pagingSettingsStorage,
87
+ storageKey,
88
+ }: {
89
+ entityType: string
90
+ schemaRegistry: SchemaRegistry | null
91
+ children: (it: EntityAccessor<TEntity>) => ReactNode
92
+ initialSorting?: Partial<Record<string, OrderDirection>>
93
+ itemsPerPage?: number | null
94
+ staticFilter?: Record<string, unknown>
95
+ layouts?: readonly DataViewLayout[]
96
+ initialSelection?: SelectionValues
97
+ stateStorage?: StateStorageOrName
98
+ currentPageStateStorage?: StateStorageOrName
99
+ pagingSettingsStorage?: StateStorageOrName
100
+ storageKey?: string
101
+ }): DataGridSetupResult {
102
+ // ---- Phase 1: Collection ----
103
+ const { childrenJsx, columns, selection, queryKey, toolbarContent, layoutRenders, layoutElements, effectiveLayouts } = useMemo(() => {
104
+ const scope = new SelectionScope()
105
+ const collector = createCollectorProxy<TEntity>(scope, entityType, schemaRegistry ?? undefined) as unknown as EntityAccessor<TEntity>
106
+
107
+ const jsx = children(collector)
108
+ const analysis = analyzeChildren(jsx, MARKER_TYPES)
109
+
110
+ const cols = analysis.getAll(ColumnLeaf) as unknown as ColumnLeafProps[]
111
+ const toolbarMarker = analysis.getFirst(DataGridToolbarContent) as DataGridToolbarContentProps | undefined
112
+ const layoutMarkers = analysis.getAll(DataGridLayout) as unknown as DataGridLayoutProps[]
113
+
114
+ for (const col of cols) {
115
+ col.collectSelection?.(collector)
116
+ }
117
+
118
+ const renders = new Map<string, (item: DataViewContextValue['items'][number]) => ReactNode>()
119
+ const elements = new Map<string, readonly DataViewElementData[]>()
120
+ for (const marker of layoutMarkers) {
121
+ const layoutCallback = marker.children as (item: EntityAccessor<object>) => ReactNode
122
+ const layoutJsx = layoutCallback(collector as unknown as EntityAccessor<object>)
123
+ const layoutSel = collectSelection(layoutJsx)
124
+ mergeSelections(scope.toSelectionMeta(), layoutSel)
125
+ renders.set(marker.name, layoutCallback)
126
+
127
+ const elementAnalysis = analyzeChildren(layoutJsx, ELEMENT_MARKER_TYPES)
128
+ const elementProps = elementAnalysis.getAll(DataViewElement) as unknown as DataViewElementProps[]
129
+ if (elementProps.length > 0) {
130
+ elements.set(marker.name, elementProps.map(it => ({ name: it.name, label: it.label, fallback: it.fallback })))
131
+ }
132
+ }
133
+
134
+ const jsxSel = collectSelection(jsx)
135
+ const sel = scope.toSelectionMeta()
136
+ mergeSelections(sel, jsxSel)
137
+
138
+ const query = buildQueryFromSelection(sel)
139
+ const key = JSON.stringify({ entityType, query })
140
+
141
+ const autoLayouts = layouts ?? [
142
+ { name: 'table', label: 'Table' },
143
+ ...layoutMarkers.map(m => ({ name: m.name, label: m.label })),
144
+ ]
145
+
146
+ return {
147
+ childrenJsx: jsx,
148
+ columns: cols,
149
+ selection: sel,
150
+ queryKey: key,
151
+ toolbarContent: toolbarMarker?.children,
152
+ layoutRenders: renders,
153
+ layoutElements: elements,
154
+ effectiveLayouts: autoLayouts,
155
+ }
156
+ }, [entityType, schemaRegistry, children, layouts])
157
+
158
+ // ---- Phase 2: State management ----
159
+ const filterDefs = useMemo((): ReadonlyMap<string, { handler: FilterHandler<FilterArtifact> }> => {
160
+ const map = new Map<string, { handler: FilterHandler<FilterArtifact> }>()
161
+ const textFieldPaths: string[] = []
162
+ for (const col of columns) {
163
+ if (col.filterName && col.filterHandler) {
164
+ map.set(col.filterName, { handler: col.filterHandler })
165
+ }
166
+ if (col.isTextSearchable && col.fieldName) {
167
+ textFieldPaths.push(col.fieldName)
168
+ }
169
+ }
170
+ if (textFieldPaths.length > 0) {
171
+ map.set(QUERY_FILTER_NAME, { handler: createFullTextFilterHandler(textFieldPaths) })
172
+ }
173
+ return map
174
+ }, [columns])
175
+
176
+ const sortableFields = useMemo((): ReadonlySet<string> => {
177
+ const set = new Set<string>()
178
+ for (const col of columns) {
179
+ if (col.sortingField) {
180
+ set.add(col.sortingField)
181
+ }
182
+ }
183
+ return set
184
+ }, [columns])
185
+
186
+ const initialSortingDirs = useMemo((): SortingDirections | undefined => {
187
+ if (!initialSorting) return undefined
188
+ const dirs: Record<string, OrderDirection> = {}
189
+ for (const [field, dir] of Object.entries(initialSorting)) {
190
+ if (dir) dirs[field] = dir
191
+ }
192
+ return Object.keys(dirs).length > 0 ? dirs : undefined
193
+ }, [initialSorting])
194
+
195
+ const filtering = useFilteringState({ filters: filterDefs, stateStorage, storageKey })
196
+ const sorting = useSortingState({ sortableFields, initialSorting: initialSortingDirs, stateStorage, storageKey })
197
+ const paging = usePagingState({
198
+ initialItemsPerPage: itemsPerPage,
199
+ currentPageStateStorage: currentPageStateStorage ?? stateStorage,
200
+ pagingSettingsStorage,
201
+ storageKey,
202
+ })
203
+ const selectionState = useSelectionState({ layouts: effectiveLayouts, initialSelection, stateStorage, storageKey })
204
+
205
+ // ---- Phase 3: Build combined filter ----
206
+ const combinedFilter = useMemo((): Record<string, unknown> | undefined => {
207
+ const parts: Record<string, unknown>[] = []
208
+ if (staticFilter) parts.push(staticFilter as Record<string, unknown>)
209
+ if (filtering.resolvedWhere) parts.push(filtering.resolvedWhere)
210
+ if (parts.length === 0) return undefined
211
+ if (parts.length === 1) return parts[0]
212
+ return { and: parts }
213
+ }, [staticFilter, filtering.resolvedWhere])
214
+
215
+ return {
216
+ childrenJsx,
217
+ columns,
218
+ selection,
219
+ queryKey,
220
+ toolbarContent,
221
+ layoutRenders,
222
+ layoutElements,
223
+ effectiveLayouts,
224
+ filtering,
225
+ sorting,
226
+ paging,
227
+ selectionState,
228
+ combinedFilter,
229
+ }
230
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * useDataView — low-level hook for building custom data views.
3
+ *
4
+ * Manages filtering, sorting, paging, and selection state, and loads data
5
+ * from the backend adapter. This is the hook API that DataGrid
6
+ * uses internally, exposed for building custom grid/list UIs.
7
+ */
8
+
9
+ import { useMemo } from 'react'
10
+ import type {
11
+ EntityDef,
12
+ EntityAccessor,
13
+ FilterHandler,
14
+ FilterArtifact,
15
+ SelectionMeta,
16
+ FieldError,
17
+ SortingDirections,
18
+ DataViewLayout,
19
+ SelectionValues,
20
+ } from '@contember/bindx'
21
+ import { useEntityList } from '@contember/bindx-react'
22
+ import {
23
+ useFilteringState,
24
+ useSortingState,
25
+ usePagingState,
26
+ useSelectionState,
27
+ type FilteringState,
28
+ type SortingStateResult,
29
+ type PagingStateResult,
30
+ type SelectionStateResult,
31
+ } from './useDataViewState.js'
32
+ import type { StateStorageOrName } from './stateStorage.js'
33
+
34
+ // ============================================================================
35
+ // Types
36
+ // ============================================================================
37
+
38
+ export interface UseDataViewOptions {
39
+ /** Filter definitions: name → { handler, initialArtifact? } */
40
+ filters?: ReadonlyMap<string, { handler: FilterHandler<FilterArtifact>; initialArtifact?: FilterArtifact }>
41
+ /** Fields that can be sorted */
42
+ sortableFields?: ReadonlySet<string>
43
+ /** Initial sorting directions (supports multi-column) */
44
+ initialSorting?: SortingDirections
45
+ /** Items per page. null = show all. Default: 50 */
46
+ itemsPerPage?: number | null
47
+ /** Static filter (always applied) */
48
+ filter?: Record<string, unknown>
49
+ /** Selection metadata for building the query */
50
+ selection: SelectionMeta
51
+ /** Available layouts */
52
+ layouts?: readonly DataViewLayout[]
53
+ /** Initial selection (layout + visibility) */
54
+ initialSelection?: SelectionValues
55
+ /** State storage for filter/sorting/selection state */
56
+ stateStorage?: StateStorageOrName
57
+ /** State storage for current page (typically 'url') */
58
+ currentPageStateStorage?: StateStorageOrName
59
+ /** State storage for page size preference (typically 'session' or 'local') */
60
+ pagingSettingsStorage?: StateStorageOrName
61
+ /** Storage key prefix for isolation */
62
+ storageKey?: string
63
+ }
64
+
65
+ export interface DataViewResult {
66
+ /** Current load status */
67
+ readonly status: 'loading' | 'error' | 'ready'
68
+ /** Error if status is 'error' */
69
+ readonly error?: FieldError
70
+ /** Loaded items as entity accessors */
71
+ readonly items: readonly EntityAccessor<object>[]
72
+ /** Filtering state and controls */
73
+ readonly filtering: FilteringState
74
+ /** Sorting state and controls */
75
+ readonly sorting: SortingStateResult
76
+ /** Paging state and controls */
77
+ readonly paging: PagingStateResult
78
+ /** Selection state (visibility + layout) */
79
+ readonly selection: SelectionStateResult
80
+ }
81
+
82
+ // ============================================================================
83
+ // Hook
84
+ // ============================================================================
85
+
86
+ const EMPTY_FILTERS = new Map<string, { handler: FilterHandler<FilterArtifact> }>()
87
+ const EMPTY_SORTABLE = new Set<string>()
88
+
89
+ export function useDataView(
90
+ entity: EntityDef,
91
+ options: UseDataViewOptions,
92
+ ): DataViewResult {
93
+ const {
94
+ filters: filterDefs = EMPTY_FILTERS,
95
+ sortableFields = EMPTY_SORTABLE,
96
+ initialSorting,
97
+ itemsPerPage = 50,
98
+ filter: staticFilter,
99
+ selection,
100
+ layouts,
101
+ initialSelection,
102
+ stateStorage,
103
+ currentPageStateStorage,
104
+ pagingSettingsStorage,
105
+ storageKey,
106
+ } = options
107
+
108
+ const filtering = useFilteringState({ filters: filterDefs, stateStorage, storageKey })
109
+ const sorting = useSortingState({ sortableFields, initialSorting, stateStorage, storageKey })
110
+ const paging = usePagingState({
111
+ initialItemsPerPage: itemsPerPage,
112
+ currentPageStateStorage: currentPageStateStorage ?? stateStorage,
113
+ pagingSettingsStorage,
114
+ storageKey,
115
+ })
116
+ const selectionState = useSelectionState({ layouts, initialSelection, stateStorage, storageKey })
117
+
118
+ // Combine static + dynamic filters
119
+ const combinedFilter = useMemo((): Record<string, unknown> | undefined => {
120
+ const parts: Record<string, unknown>[] = []
121
+ if (staticFilter) parts.push(staticFilter)
122
+ if (filtering.resolvedWhere) parts.push(filtering.resolvedWhere)
123
+ if (parts.length === 0) return undefined
124
+ if (parts.length === 1) return parts[0]
125
+ return { and: parts }
126
+ }, [staticFilter, filtering.resolvedWhere])
127
+
128
+ // Load data
129
+ const result = useEntityList(entity, {
130
+ filter: combinedFilter,
131
+ orderBy: sorting.resolvedOrderBy,
132
+ limit: paging.queryLimit,
133
+ offset: paging.queryOffset,
134
+ selection,
135
+ })
136
+
137
+ const items = result.status === 'ready' ? result.items : []
138
+
139
+ return {
140
+ status: result.status,
141
+ error: result.status === 'error' ? result.error : undefined,
142
+ items,
143
+ filtering,
144
+ sorting,
145
+ paging,
146
+ selection: selectionState,
147
+ }
148
+ }