@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 @@
|
|
|
1
|
+
{"version":3,"file":"useDataViewState.d.ts","sourceRoot":"","sources":["../src/useDataViewState.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contember/bindx-dataview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless DataGrid and DataView components for bindx",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc --build",
|
|
12
|
+
"typecheck": "tsc --build"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@contember/bindx": "0.1.0",
|
|
19
|
+
"@contember/bindx-react": "0.1.0",
|
|
20
|
+
"@radix-ui/primitive": "^1.1.3",
|
|
21
|
+
"@radix-ui/react-slot": "^1.2.4"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/contember/bindx.git",
|
|
31
|
+
"directory": "packages/bindx-dataview"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/DataGrid.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataGrid component — headless data grid for top-level entity lists.
|
|
3
|
+
* Uses useEntityList for data loading.
|
|
4
|
+
*
|
|
5
|
+
* For has-many relation grids, use HasManyDataGrid instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { memo, type ReactElement, type ReactNode, useEffect, useMemo, useCallback, useRef, useState } from 'react'
|
|
9
|
+
import type {
|
|
10
|
+
EntityDef,
|
|
11
|
+
EntityAccessor,
|
|
12
|
+
OrderDirection,
|
|
13
|
+
DataViewLayout,
|
|
14
|
+
SelectionValues,
|
|
15
|
+
CommonEntity,
|
|
16
|
+
} from '@contember/bindx'
|
|
17
|
+
import type { StateStorageOrName } from './stateStorage.js'
|
|
18
|
+
import {
|
|
19
|
+
useBindxContext,
|
|
20
|
+
useEntityList,
|
|
21
|
+
} from '@contember/bindx-react'
|
|
22
|
+
import { useDataViewKey } from './DataViewKeyProvider.js'
|
|
23
|
+
import { DataViewProvider, type DataViewContextValue, type DataViewLoaderState } from './DataViewContext.js'
|
|
24
|
+
import { useDataGridSetup, QUERY_FILTER_NAME } from './useDataGridSetup.js'
|
|
25
|
+
|
|
26
|
+
export { QUERY_FILTER_NAME }
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// DataGrid Props
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export interface DataGridProps<TRoleMap extends Record<string, object> = Record<string, object>> {
|
|
33
|
+
/** Entity definition */
|
|
34
|
+
entity: EntityDef<TRoleMap>
|
|
35
|
+
/** Children render function: receives entity proxy `it`, returns column markers + layout */
|
|
36
|
+
children: (it: EntityAccessor<CommonEntity<TRoleMap>>) => ReactNode
|
|
37
|
+
/** Initial sorting (supports multi-column: { title: 'asc', date: 'desc' }) */
|
|
38
|
+
initialSorting?: Partial<Record<string, OrderDirection>>
|
|
39
|
+
/** Items per page. null = show all. Default: 50 */
|
|
40
|
+
itemsPerPage?: number | null
|
|
41
|
+
/** Static filter (combined with dynamic filters) */
|
|
42
|
+
filter?: Record<string, unknown>
|
|
43
|
+
/** Available layouts */
|
|
44
|
+
layouts?: readonly DataViewLayout[]
|
|
45
|
+
/** Initial selection (layout + visibility) */
|
|
46
|
+
initialSelection?: SelectionValues
|
|
47
|
+
/** State storage for filter/sorting/selection state */
|
|
48
|
+
stateStorage?: StateStorageOrName
|
|
49
|
+
/** State storage for current page */
|
|
50
|
+
currentPageStateStorage?: StateStorageOrName
|
|
51
|
+
/** State storage for page size preference */
|
|
52
|
+
pagingSettingsStorage?: StateStorageOrName
|
|
53
|
+
/** Storage key prefix for isolation */
|
|
54
|
+
storageKey?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Implementation
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
function DataGridImpl<TRoleMap extends Record<string, object>>({
|
|
62
|
+
entity,
|
|
63
|
+
children,
|
|
64
|
+
initialSorting,
|
|
65
|
+
itemsPerPage = 50,
|
|
66
|
+
filter: staticFilter,
|
|
67
|
+
layouts,
|
|
68
|
+
initialSelection,
|
|
69
|
+
stateStorage,
|
|
70
|
+
currentPageStateStorage,
|
|
71
|
+
pagingSettingsStorage,
|
|
72
|
+
storageKey,
|
|
73
|
+
}: DataGridProps<TRoleMap>): ReactElement | null {
|
|
74
|
+
const { schema: schemaRegistry } = useBindxContext()
|
|
75
|
+
const entityType = entity.$name
|
|
76
|
+
const contextKey = useDataViewKey()
|
|
77
|
+
const effectiveStorageKey = storageKey ?? contextKey
|
|
78
|
+
|
|
79
|
+
const setup = useDataGridSetup<CommonEntity<TRoleMap>>({
|
|
80
|
+
entityType,
|
|
81
|
+
schemaRegistry,
|
|
82
|
+
children,
|
|
83
|
+
initialSorting,
|
|
84
|
+
itemsPerPage,
|
|
85
|
+
staticFilter,
|
|
86
|
+
layouts,
|
|
87
|
+
initialSelection,
|
|
88
|
+
stateStorage,
|
|
89
|
+
currentPageStateStorage,
|
|
90
|
+
pagingSettingsStorage,
|
|
91
|
+
storageKey: effectiveStorageKey,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ---- Loader state tracking ----
|
|
95
|
+
const hasLoadedOnce = useRef(false)
|
|
96
|
+
const [loaderState, setLoaderState] = useState<DataViewLoaderState>('initial')
|
|
97
|
+
|
|
98
|
+
// ---- Load data via useEntityList ----
|
|
99
|
+
const result = useEntityList(entity, {
|
|
100
|
+
filter: setup.combinedFilter,
|
|
101
|
+
orderBy: setup.sorting.resolvedOrderBy,
|
|
102
|
+
limit: setup.paging.queryLimit,
|
|
103
|
+
offset: setup.paging.queryOffset,
|
|
104
|
+
selection: setup.selection,
|
|
105
|
+
queryKey: setup.queryKey,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const items = result.status === 'ready' ? result.items : []
|
|
109
|
+
const itemCount = items.length
|
|
110
|
+
|
|
111
|
+
// Update loader state
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (result.status === 'ready') {
|
|
114
|
+
hasLoadedOnce.current = true
|
|
115
|
+
setLoaderState('loaded')
|
|
116
|
+
} else if (result.status === 'error') {
|
|
117
|
+
setLoaderState('failed')
|
|
118
|
+
} else if (result.status === 'loading') {
|
|
119
|
+
setLoaderState(hasLoadedOnce.current ? 'refreshing' : 'initial')
|
|
120
|
+
}
|
|
121
|
+
}, [result.status])
|
|
122
|
+
|
|
123
|
+
// Update total count when data is ready
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (result.status === 'ready' && setup.paging.queryLimit !== undefined && setup.paging.queryOffset !== undefined && itemCount < setup.paging.queryLimit) {
|
|
126
|
+
setup.paging.setTotalCount(setup.paging.queryOffset + itemCount)
|
|
127
|
+
}
|
|
128
|
+
}, [result.status, itemCount, setup.paging.queryLimit, setup.paging.queryOffset, setup.paging.setTotalCount])
|
|
129
|
+
|
|
130
|
+
// ---- Reload ----
|
|
131
|
+
const [, setReloadCounter] = useState(0)
|
|
132
|
+
const reload = useCallback((): void => {
|
|
133
|
+
setReloadCounter(c => c + 1)
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
// ---- Row highlighting ----
|
|
137
|
+
const [highlightIndex, setHighlightIndex] = useState<number | null>(null)
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setHighlightIndex(null)
|
|
141
|
+
}, [items])
|
|
142
|
+
|
|
143
|
+
const contextValue = useMemo((): DataViewContextValue => ({
|
|
144
|
+
filtering: setup.filtering,
|
|
145
|
+
sorting: setup.sorting,
|
|
146
|
+
paging: setup.paging,
|
|
147
|
+
selection: setup.selectionState,
|
|
148
|
+
columns: setup.columns,
|
|
149
|
+
entityType,
|
|
150
|
+
items,
|
|
151
|
+
itemCount,
|
|
152
|
+
loaderState,
|
|
153
|
+
reload,
|
|
154
|
+
highlightIndex,
|
|
155
|
+
setHighlightIndex,
|
|
156
|
+
selectionMeta: setup.selection,
|
|
157
|
+
toolbarContent: setup.toolbarContent,
|
|
158
|
+
layoutRenders: setup.layoutRenders,
|
|
159
|
+
layoutElements: setup.layoutElements,
|
|
160
|
+
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, entityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<DataViewProvider value={contextValue}>
|
|
164
|
+
{setup.childrenJsx}
|
|
165
|
+
</DataViewProvider>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Export
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
export const DataGrid = memo(DataGridImpl) as unknown as typeof DataGridImpl
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataView context — provides filtering, sorting, paging, selection state,
|
|
3
|
+
* and utility methods to descendant components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { createContext, useContext } from 'react'
|
|
7
|
+
import type { FilteringState, SortingStateResult, PagingStateResult, SelectionStateResult } from './useDataViewState.js'
|
|
8
|
+
import type { ColumnLeafProps } from './columnLeaf.js'
|
|
9
|
+
import type { EntityAccessor, SelectionMeta } from '@contember/bindx'
|
|
10
|
+
|
|
11
|
+
export type DataViewLoaderState = 'initial' | 'loaded' | 'refreshing' | 'failed'
|
|
12
|
+
|
|
13
|
+
export type DataViewItem = EntityAccessor<object>
|
|
14
|
+
|
|
15
|
+
export interface DataViewElementData {
|
|
16
|
+
readonly name: string
|
|
17
|
+
readonly label?: React.ReactNode
|
|
18
|
+
readonly fallback?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DataViewContextValue {
|
|
22
|
+
readonly filtering: FilteringState
|
|
23
|
+
readonly sorting: SortingStateResult
|
|
24
|
+
readonly paging: PagingStateResult
|
|
25
|
+
readonly selection: SelectionStateResult
|
|
26
|
+
readonly columns: readonly ColumnLeafProps[]
|
|
27
|
+
readonly entityType: string
|
|
28
|
+
readonly items: readonly DataViewItem[]
|
|
29
|
+
readonly itemCount: number
|
|
30
|
+
readonly loaderState: DataViewLoaderState
|
|
31
|
+
readonly reload: () => void
|
|
32
|
+
readonly highlightIndex: number | null
|
|
33
|
+
readonly setHighlightIndex: (index: number | null) => void
|
|
34
|
+
readonly selectionMeta: SelectionMeta
|
|
35
|
+
readonly toolbarContent?: React.ReactNode
|
|
36
|
+
/** Named layout render callbacks — analyzed during collection, called per item at runtime */
|
|
37
|
+
readonly layoutRenders: ReadonlyMap<string, (item: DataViewItem) => React.ReactNode>
|
|
38
|
+
/** Elements per layout — extracted from layout render callbacks during collection */
|
|
39
|
+
readonly layoutElements: ReadonlyMap<string, readonly DataViewElementData[]>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DataViewContext = createContext<DataViewContextValue | null>(null)
|
|
43
|
+
|
|
44
|
+
export function useDataViewContext(): DataViewContextValue {
|
|
45
|
+
const ctx = useContext(DataViewContext)
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
throw new Error('useDataViewContext must be used within a DataViewProvider')
|
|
48
|
+
}
|
|
49
|
+
return ctx
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useOptionalDataViewContext(): DataViewContextValue | null {
|
|
53
|
+
return useContext(DataViewContext)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DataViewProvider = DataViewContext.Provider
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
const DataViewKeyContext = createContext<string | undefined>(undefined)
|
|
4
|
+
|
|
5
|
+
export interface DataViewKeyProviderProps {
|
|
6
|
+
readonly value: string
|
|
7
|
+
readonly children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DataViewKeyProvider({ value, children }: DataViewKeyProviderProps): ReactNode {
|
|
11
|
+
return <DataViewKeyContext value={value}>{children}</DataViewKeyContext>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useDataViewKey(fallback = 'dataview'): string {
|
|
15
|
+
return useContext(DataViewKeyContext) ?? fallback
|
|
16
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HasManyDataGrid — headless data grid for has-many relation fields.
|
|
3
|
+
* Fetches relation data from the parent entity with dynamic filter/sort/page params.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <Entity entity={schema.Author} by={{ id }}>
|
|
8
|
+
* {(author) => (
|
|
9
|
+
* <HasManyDataGrid field={author.articles}>
|
|
10
|
+
* {(it) => (
|
|
11
|
+
* <>
|
|
12
|
+
* <TextColumn field={it.title} />
|
|
13
|
+
* <BooleanColumn field={it.active} />
|
|
14
|
+
* </>
|
|
15
|
+
* )}
|
|
16
|
+
* </HasManyDataGrid>
|
|
17
|
+
* )}
|
|
18
|
+
* </Entity>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { memo, type ReactElement, type ReactNode, useEffect, useMemo, useCallback, useRef, useState } from 'react'
|
|
23
|
+
import type {
|
|
24
|
+
EntityAccessor,
|
|
25
|
+
HasManyRef,
|
|
26
|
+
OrderDirection,
|
|
27
|
+
DataViewLayout,
|
|
28
|
+
SelectionValues,
|
|
29
|
+
} from '@contember/bindx'
|
|
30
|
+
import { FIELD_REF_META } from '@contember/bindx'
|
|
31
|
+
import {
|
|
32
|
+
buildQueryFromSelection,
|
|
33
|
+
generateHasManyAlias,
|
|
34
|
+
EntityHandle,
|
|
35
|
+
setEntityData,
|
|
36
|
+
} from '@contember/bindx'
|
|
37
|
+
import type { SchemaRegistry } from '@contember/bindx'
|
|
38
|
+
import type { StateStorageOrName } from './stateStorage.js'
|
|
39
|
+
import {
|
|
40
|
+
useBindxContext,
|
|
41
|
+
} from '@contember/bindx-react'
|
|
42
|
+
import { useDataViewKey } from './DataViewKeyProvider.js'
|
|
43
|
+
import { DataViewProvider, type DataViewContextValue, type DataViewLoaderState } from './DataViewContext.js'
|
|
44
|
+
import { useDataGridSetup } from './useDataGridSetup.js'
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Props
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export interface HasManyDataGridProps<TEntity extends object = object> {
|
|
51
|
+
/** Has-many relation field from parent entity */
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
field: HasManyRef<TEntity, any>
|
|
54
|
+
/** Children render function: receives entity proxy `it`, returns column markers + layout */
|
|
55
|
+
children: (it: EntityAccessor<TEntity>) => ReactNode
|
|
56
|
+
/** Initial sorting */
|
|
57
|
+
initialSorting?: Partial<Record<string, OrderDirection>>
|
|
58
|
+
/** Items per page. null = show all. Default: 50 */
|
|
59
|
+
itemsPerPage?: number | null
|
|
60
|
+
/** Static filter (combined with dynamic filters) */
|
|
61
|
+
filter?: Record<string, unknown>
|
|
62
|
+
/** Available layouts */
|
|
63
|
+
layouts?: readonly DataViewLayout[]
|
|
64
|
+
/** Initial selection (layout + visibility) */
|
|
65
|
+
initialSelection?: SelectionValues
|
|
66
|
+
/** State storage for filter/sorting/selection state */
|
|
67
|
+
stateStorage?: StateStorageOrName
|
|
68
|
+
/** State storage for current page */
|
|
69
|
+
currentPageStateStorage?: StateStorageOrName
|
|
70
|
+
/** State storage for page size preference */
|
|
71
|
+
pagingSettingsStorage?: StateStorageOrName
|
|
72
|
+
/** Storage key prefix for isolation */
|
|
73
|
+
storageKey?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// List state
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
interface ListState {
|
|
81
|
+
status: 'loading' | 'error' | 'ready'
|
|
82
|
+
items: Array<{ id: string; data: object }>
|
|
83
|
+
totalCount?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const INITIAL_LIST_STATE: ListState = { status: 'loading', items: [] }
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Implementation
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
function HasManyDataGridImpl<TEntity extends object>({
|
|
93
|
+
field,
|
|
94
|
+
children,
|
|
95
|
+
initialSorting,
|
|
96
|
+
itemsPerPage = 50,
|
|
97
|
+
filter: staticFilter,
|
|
98
|
+
layouts,
|
|
99
|
+
initialSelection,
|
|
100
|
+
stateStorage,
|
|
101
|
+
currentPageStateStorage,
|
|
102
|
+
pagingSettingsStorage,
|
|
103
|
+
storageKey,
|
|
104
|
+
}: HasManyDataGridProps<TEntity>): ReactElement | null {
|
|
105
|
+
const { schema: schemaRegistry, store, dispatcher, batcher } = useBindxContext()
|
|
106
|
+
|
|
107
|
+
// Extract relation metadata from field prop
|
|
108
|
+
const fieldMeta = field[FIELD_REF_META]
|
|
109
|
+
const parentEntityType = fieldMeta.entityType
|
|
110
|
+
const parentEntityId = fieldMeta.entityId
|
|
111
|
+
const fieldName = fieldMeta.fieldName
|
|
112
|
+
const targetEntityType = field.__entityName
|
|
113
|
+
|
|
114
|
+
const contextKey = useDataViewKey()
|
|
115
|
+
const effectiveStorageKey = storageKey ?? `${parentEntityType}:${fieldName}`
|
|
116
|
+
|
|
117
|
+
const setup = useDataGridSetup<TEntity>({
|
|
118
|
+
entityType: targetEntityType,
|
|
119
|
+
schemaRegistry,
|
|
120
|
+
children,
|
|
121
|
+
initialSorting,
|
|
122
|
+
itemsPerPage,
|
|
123
|
+
staticFilter,
|
|
124
|
+
layouts,
|
|
125
|
+
initialSelection,
|
|
126
|
+
stateStorage,
|
|
127
|
+
currentPageStateStorage,
|
|
128
|
+
pagingSettingsStorage,
|
|
129
|
+
storageKey: effectiveStorageKey,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// ---- State ----
|
|
133
|
+
const hasLoadedOnce = useRef(false)
|
|
134
|
+
const [loaderState, setLoaderState] = useState<DataViewLoaderState>('initial')
|
|
135
|
+
const [listState, setListState] = useState<ListState>(INITIAL_LIST_STATE)
|
|
136
|
+
|
|
137
|
+
// ---- Build relation query spec ----
|
|
138
|
+
const alias = useMemo(
|
|
139
|
+
() => generateHasManyAlias(fieldName, {
|
|
140
|
+
filter: setup.combinedFilter,
|
|
141
|
+
orderBy: setup.sorting.resolvedOrderBy as unknown[],
|
|
142
|
+
limit: setup.paging.queryLimit,
|
|
143
|
+
offset: setup.paging.queryOffset,
|
|
144
|
+
}),
|
|
145
|
+
[fieldName, setup.combinedFilter, setup.sorting.resolvedOrderBy, setup.paging.queryLimit, setup.paging.queryOffset],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const optionsKey = useMemo(
|
|
149
|
+
() => JSON.stringify({
|
|
150
|
+
filter: setup.combinedFilter ?? {},
|
|
151
|
+
orderBy: setup.sorting.resolvedOrderBy ?? [],
|
|
152
|
+
limit: setup.paging.queryLimit,
|
|
153
|
+
offset: setup.paging.queryOffset,
|
|
154
|
+
}),
|
|
155
|
+
[setup.combinedFilter, setup.sorting.resolvedOrderBy, setup.paging.queryLimit, setup.paging.queryOffset],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// ---- Data loading via GetQuery on parent ----
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const abortController = new AbortController()
|
|
161
|
+
setListState(INITIAL_LIST_STATE)
|
|
162
|
+
|
|
163
|
+
const fetchData = async (): Promise<void> => {
|
|
164
|
+
try {
|
|
165
|
+
const targetSpec = buildQueryFromSelection(setup.selection)
|
|
166
|
+
const currentOptions = JSON.parse(optionsKey) as {
|
|
167
|
+
filter: Record<string, unknown>
|
|
168
|
+
orderBy: readonly Record<string, unknown>[]
|
|
169
|
+
limit?: number
|
|
170
|
+
offset?: number
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parentSpec = {
|
|
174
|
+
fields: [
|
|
175
|
+
{ name: 'id', sourcePath: ['id'] },
|
|
176
|
+
{
|
|
177
|
+
name: alias,
|
|
178
|
+
sourcePath: [fieldName],
|
|
179
|
+
isArray: true as const,
|
|
180
|
+
totalCount: true,
|
|
181
|
+
filter: Object.keys(currentOptions.filter).length > 0 ? currentOptions.filter : undefined,
|
|
182
|
+
orderBy: currentOptions.orderBy.length > 0 ? currentOptions.orderBy : undefined,
|
|
183
|
+
limit: currentOptions.limit,
|
|
184
|
+
offset: currentOptions.offset,
|
|
185
|
+
nested: targetSpec,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await batcher.enqueue(
|
|
191
|
+
{
|
|
192
|
+
type: 'get',
|
|
193
|
+
entityType: parentEntityType,
|
|
194
|
+
by: { id: parentEntityId },
|
|
195
|
+
spec: parentSpec,
|
|
196
|
+
},
|
|
197
|
+
{ signal: abortController.signal },
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if (abortController.signal.aborted) return
|
|
201
|
+
|
|
202
|
+
if (result.type !== 'get' || !result.data) {
|
|
203
|
+
setListState({ status: 'error', items: [] })
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const relationData = (result.data[alias] ?? result.data[fieldName]) as Array<Record<string, unknown>> | undefined
|
|
208
|
+
const totalCount = Array.isArray(relationData) && 'totalCount' in relationData
|
|
209
|
+
? (relationData as unknown as { totalCount: number }).totalCount
|
|
210
|
+
: undefined
|
|
211
|
+
|
|
212
|
+
if (!Array.isArray(relationData)) {
|
|
213
|
+
setListState({ status: 'ready', items: [], totalCount })
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const items = relationData.map((data: Record<string, unknown>) => {
|
|
218
|
+
const id = data['id'] as string
|
|
219
|
+
dispatcher.dispatch(
|
|
220
|
+
setEntityData(targetEntityType, id, data, true),
|
|
221
|
+
)
|
|
222
|
+
return { id, data: data as object }
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
setListState({ status: 'ready', items, totalCount })
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (abortController.signal.aborted) return
|
|
228
|
+
setListState({ status: 'error', items: [] })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fetchData()
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
abortController.abort()
|
|
236
|
+
}
|
|
237
|
+
}, [parentEntityType, parentEntityId, fieldName, targetEntityType, alias, optionsKey, setup.selection, batcher, dispatcher, store])
|
|
238
|
+
|
|
239
|
+
// ---- Build items from state ----
|
|
240
|
+
const items = useMemo((): EntityAccessor<TEntity>[] => {
|
|
241
|
+
if (listState.status !== 'ready') return []
|
|
242
|
+
return listState.items.map((item) =>
|
|
243
|
+
EntityHandle.create<TEntity>(
|
|
244
|
+
item.id,
|
|
245
|
+
targetEntityType,
|
|
246
|
+
store,
|
|
247
|
+
dispatcher,
|
|
248
|
+
schemaRegistry as SchemaRegistry<Record<string, object>>,
|
|
249
|
+
) as unknown as EntityAccessor<TEntity>,
|
|
250
|
+
)
|
|
251
|
+
}, [listState, targetEntityType, store, dispatcher, schemaRegistry])
|
|
252
|
+
|
|
253
|
+
const itemCount = items.length
|
|
254
|
+
|
|
255
|
+
// Update loader state
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (listState.status === 'ready') {
|
|
258
|
+
hasLoadedOnce.current = true
|
|
259
|
+
setLoaderState('loaded')
|
|
260
|
+
} else if (listState.status === 'error') {
|
|
261
|
+
setLoaderState('failed')
|
|
262
|
+
} else if (listState.status === 'loading') {
|
|
263
|
+
setLoaderState(hasLoadedOnce.current ? 'refreshing' : 'initial')
|
|
264
|
+
}
|
|
265
|
+
}, [listState.status])
|
|
266
|
+
|
|
267
|
+
// Update total count from paginateRelation
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (listState.totalCount !== undefined) {
|
|
270
|
+
setup.paging.setTotalCount(listState.totalCount)
|
|
271
|
+
} else if (listState.status === 'ready' && setup.paging.queryLimit !== undefined && setup.paging.queryOffset !== undefined && itemCount < setup.paging.queryLimit) {
|
|
272
|
+
setup.paging.setTotalCount(setup.paging.queryOffset + itemCount)
|
|
273
|
+
}
|
|
274
|
+
}, [listState.status, listState.totalCount, itemCount, setup.paging.queryLimit, setup.paging.queryOffset, setup.paging.setTotalCount])
|
|
275
|
+
|
|
276
|
+
// ---- Reload ----
|
|
277
|
+
const [, setReloadCounter] = useState(0)
|
|
278
|
+
const reload = useCallback((): void => {
|
|
279
|
+
setReloadCounter(c => c + 1)
|
|
280
|
+
}, [])
|
|
281
|
+
|
|
282
|
+
// ---- Row highlighting ----
|
|
283
|
+
const [highlightIndex, setHighlightIndex] = useState<number | null>(null)
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
setHighlightIndex(null)
|
|
287
|
+
}, [items])
|
|
288
|
+
|
|
289
|
+
const contextValue = useMemo((): DataViewContextValue => ({
|
|
290
|
+
filtering: setup.filtering,
|
|
291
|
+
sorting: setup.sorting,
|
|
292
|
+
paging: setup.paging,
|
|
293
|
+
selection: setup.selectionState,
|
|
294
|
+
columns: setup.columns,
|
|
295
|
+
entityType: targetEntityType,
|
|
296
|
+
items,
|
|
297
|
+
itemCount,
|
|
298
|
+
loaderState,
|
|
299
|
+
reload,
|
|
300
|
+
highlightIndex,
|
|
301
|
+
setHighlightIndex,
|
|
302
|
+
selectionMeta: setup.selection,
|
|
303
|
+
toolbarContent: setup.toolbarContent,
|
|
304
|
+
layoutRenders: setup.layoutRenders,
|
|
305
|
+
layoutElements: setup.layoutElements,
|
|
306
|
+
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, targetEntityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<DataViewProvider value={contextValue}>
|
|
310
|
+
{setup.childrenJsx}
|
|
311
|
+
</DataViewProvider>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Export
|
|
317
|
+
// ============================================================================
|
|
318
|
+
|
|
319
|
+
export const HasManyDataGrid = memo(HasManyDataGridImpl) as unknown as typeof HasManyDataGridImpl
|