@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,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
|
+
}
|