@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fine-grained context hooks matching Contember's react-dataview API.
|
|
3
|
+
*
|
|
4
|
+
* These hooks provide convenient access to specific parts of the DataView context,
|
|
5
|
+
* matching Contember's pattern of separate state/methods hooks per concern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from 'react'
|
|
9
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
10
|
+
import type { DataViewElementData } from './DataViewContext.js'
|
|
11
|
+
import type { FilteringState, SortingStateResult, PagingStateResult, SelectionStateResult } from './useDataViewState.js'
|
|
12
|
+
import type {
|
|
13
|
+
SortingState,
|
|
14
|
+
SortingDirectionAction,
|
|
15
|
+
PagingState,
|
|
16
|
+
PagingInfo,
|
|
17
|
+
OrderDirection,
|
|
18
|
+
FilterHandler,
|
|
19
|
+
FilterArtifact,
|
|
20
|
+
DataViewLayout,
|
|
21
|
+
FieldRefBase,
|
|
22
|
+
} from '@contember/bindx'
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Sorting
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface DataViewSortingMethods {
|
|
29
|
+
setOrderBy<T>(field: FieldRefBase<T>, action: SortingDirectionAction, append?: boolean): void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useDataViewSortingState(): SortingState {
|
|
33
|
+
return useDataViewContext().sorting.state
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useDataViewSortingMethods(): DataViewSortingMethods {
|
|
37
|
+
const { sorting } = useDataViewContext()
|
|
38
|
+
return useMemo(() => ({ setOrderBy: sorting.setOrderBy }), [sorting.setOrderBy])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useDataViewSortingDirection<T>(field: FieldRefBase<T>): OrderDirection | null {
|
|
42
|
+
return useDataViewContext().sorting.directionOf(field)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Paging
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export interface DataViewPagingMethods {
|
|
50
|
+
goToPage(page: number | 'first' | 'next' | 'previous' | 'last'): void
|
|
51
|
+
setItemsPerPage(count: number | null): void
|
|
52
|
+
refreshTotalCount(): void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useDataViewPagingState(): PagingState {
|
|
56
|
+
return useDataViewContext().paging.state
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useDataViewPagingInfo(): PagingInfo {
|
|
60
|
+
return useDataViewContext().paging.info
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useDataViewPagingMethods(): DataViewPagingMethods {
|
|
64
|
+
const { paging } = useDataViewContext()
|
|
65
|
+
return useMemo((): DataViewPagingMethods => ({
|
|
66
|
+
goToPage: (page: number | 'first' | 'next' | 'previous' | 'last'): void => {
|
|
67
|
+
if (page === 'first') paging.first()
|
|
68
|
+
else if (page === 'last') paging.last()
|
|
69
|
+
else if (page === 'next') paging.next()
|
|
70
|
+
else if (page === 'previous') paging.previous()
|
|
71
|
+
else paging.goTo(page)
|
|
72
|
+
},
|
|
73
|
+
setItemsPerPage: paging.setItemsPerPage,
|
|
74
|
+
refreshTotalCount: paging.refreshTotalCount,
|
|
75
|
+
}), [paging])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Filtering
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
export interface DataViewFilteringMethods {
|
|
83
|
+
setFilter(name: string, artifact: FilterArtifact): void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useDataViewFilteringState(): FilteringState {
|
|
87
|
+
return useDataViewContext().filtering
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useDataViewFilteringMethods(): DataViewFilteringMethods {
|
|
91
|
+
const { filtering } = useDataViewContext()
|
|
92
|
+
return useMemo(() => ({ setFilter: filtering.setArtifact }), [filtering.setArtifact])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function useDataViewFilterHandlerRegistry(): ReadonlyMap<string, { handler: FilterHandler<FilterArtifact> }> {
|
|
96
|
+
return useDataViewContext().filtering.filters
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Selection
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export interface DataViewSelectionState {
|
|
104
|
+
readonly layout: string | undefined
|
|
105
|
+
readonly visibility: Record<string, boolean | undefined>
|
|
106
|
+
readonly layouts: readonly DataViewLayout[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DataViewSelectionMethods {
|
|
110
|
+
setLayout(layout: string | undefined): void
|
|
111
|
+
setVisibility(name: string, visible: boolean): void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function useDataViewSelectionState(): DataViewSelectionState {
|
|
115
|
+
const { selection } = useDataViewContext()
|
|
116
|
+
return useMemo((): DataViewSelectionState => ({
|
|
117
|
+
layout: selection.currentLayout,
|
|
118
|
+
visibility: selection.state.values.visibility ?? {},
|
|
119
|
+
layouts: selection.state.layouts,
|
|
120
|
+
}), [selection.currentLayout, selection.state.values.visibility, selection.state.layouts])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function useDataViewSelectionMethods(): DataViewSelectionMethods {
|
|
124
|
+
const { selection } = useDataViewContext()
|
|
125
|
+
return useMemo(() => ({
|
|
126
|
+
setLayout: selection.setLayout,
|
|
127
|
+
setVisibility: selection.setVisibility,
|
|
128
|
+
}), [selection.setLayout, selection.setVisibility])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Elements (visibility-togglable items)
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
export type { DataViewElementData } from './DataViewContext.js'
|
|
136
|
+
|
|
137
|
+
export function useDataViewElements(): readonly DataViewElementData[] {
|
|
138
|
+
const { columns, selection, layoutElements } = useDataViewContext()
|
|
139
|
+
const currentLayout = selection.currentLayout
|
|
140
|
+
|
|
141
|
+
return useMemo((): readonly DataViewElementData[] => {
|
|
142
|
+
// If the active layout has explicitly declared elements, use those
|
|
143
|
+
if (currentLayout) {
|
|
144
|
+
const elements = layoutElements.get(currentLayout)
|
|
145
|
+
if (elements) return elements
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: derive elements from columns (table layout or no layout markers)
|
|
149
|
+
return columns
|
|
150
|
+
.filter(c => c.fieldName !== null)
|
|
151
|
+
.map(c => ({
|
|
152
|
+
name: c.name,
|
|
153
|
+
label: c.header,
|
|
154
|
+
fallback: true,
|
|
155
|
+
}))
|
|
156
|
+
}, [columns, currentLayout, layoutElements])
|
|
157
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createColumn()` — factory for building column components from a column type definition.
|
|
3
|
+
*
|
|
4
|
+
* Layer 2 in the column architecture: wraps a headless `ColumnTypeDef` with
|
|
5
|
+
* rendering configuration (cell renderer, optional filter renderer) to produce
|
|
6
|
+
* a React component usable inside a DataGrid children render function.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import type { FieldRefBase, FilterArtifact, FilterHandler, EntityAccessor } from '@contember/bindx'
|
|
11
|
+
import type { ColumnTypeDef } from './columnTypes.js'
|
|
12
|
+
import { ColumnLeaf, type ColumnLeafProps } from './columnLeaf.js'
|
|
13
|
+
import { extractFieldName, extractEnumName } from './columns.js'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Render Props
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface ColumnRenderProps<TValue> {
|
|
20
|
+
readonly value: TValue
|
|
21
|
+
readonly accessor: EntityAccessor<object>
|
|
22
|
+
readonly fieldRef: FieldRefBase<unknown> | null
|
|
23
|
+
readonly fieldName: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FilterRenderProps<TFilterArtifact> {
|
|
27
|
+
readonly artifact: TFilterArtifact
|
|
28
|
+
readonly setArtifact: (artifact: TFilterArtifact) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Config
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface CreateColumnConfig<TValue, TFilterArtifact extends FilterArtifact> {
|
|
36
|
+
readonly renderCell: (props: ColumnRenderProps<TValue>) => React.ReactNode
|
|
37
|
+
readonly renderFilter?: (props: FilterRenderProps<TFilterArtifact>) => React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Column Component Props
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export interface ColumnComponentProps<TValue = unknown> {
|
|
45
|
+
field: FieldRefBase<TValue>
|
|
46
|
+
header?: React.ReactNode
|
|
47
|
+
sortable?: boolean
|
|
48
|
+
filter?: boolean
|
|
49
|
+
children?: (value: TValue | null, accessor: EntityAccessor<object>) => React.ReactNode
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Factory
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
export interface ColumnComponent<TExtraProps = object> {
|
|
57
|
+
<T>(props: ColumnComponentProps<T> & TExtraProps): null
|
|
58
|
+
staticRender: (props: Record<string, unknown>) => React.ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TExtraProps = object>(
|
|
62
|
+
columnType: ColumnTypeDef<TValue, TFilterArtifact>,
|
|
63
|
+
config: CreateColumnConfig<TValue, TFilterArtifact>,
|
|
64
|
+
): ColumnComponent<TExtraProps> {
|
|
65
|
+
function Column(_props: ColumnComponentProps<unknown> & TExtraProps): null {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Column.staticRender = (props: Record<string, unknown>): React.ReactNode => {
|
|
70
|
+
const fieldRef = props['field'] as FieldRefBase<unknown> | undefined
|
|
71
|
+
const fieldName = fieldRef ? extractFieldName(fieldRef) : null
|
|
72
|
+
const header = props['header'] as React.ReactNode | undefined
|
|
73
|
+
const sortable = (props['sortable'] as boolean | undefined) ?? columnType.defaultSortable
|
|
74
|
+
const filterEnabled = (props['filter'] as boolean | undefined) ?? false
|
|
75
|
+
const children = props['children'] as ((value: TValue | null, accessor: EntityAccessor<object>) => React.ReactNode) | undefined
|
|
76
|
+
const rawOptions = props['options'] as readonly string[] | Readonly<Record<string, React.ReactNode>> | undefined
|
|
77
|
+
const enumOptions = Array.isArray(rawOptions)
|
|
78
|
+
? Object.fromEntries(rawOptions.map(v => [v, v])) as Readonly<Record<string, React.ReactNode>>
|
|
79
|
+
: rawOptions
|
|
80
|
+
const enumName = extractEnumName(fieldRef)
|
|
81
|
+
|
|
82
|
+
const renderCell = children
|
|
83
|
+
? (accessor: EntityAccessor<object>): React.ReactNode => {
|
|
84
|
+
const value = fieldName
|
|
85
|
+
? columnType.extractValue(accessor, fieldName)
|
|
86
|
+
: null as TValue
|
|
87
|
+
return children(value, accessor)
|
|
88
|
+
}
|
|
89
|
+
: (accessor: EntityAccessor<object>): React.ReactNode => {
|
|
90
|
+
const value = fieldName
|
|
91
|
+
? columnType.extractValue(accessor, fieldName)
|
|
92
|
+
: null as TValue
|
|
93
|
+
return config.renderCell({
|
|
94
|
+
value,
|
|
95
|
+
accessor,
|
|
96
|
+
fieldRef: fieldRef ?? null,
|
|
97
|
+
fieldName,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const leafProps = {
|
|
102
|
+
name: fieldName ?? `col-${Math.random().toString(36).slice(2, 8)}`,
|
|
103
|
+
fieldName,
|
|
104
|
+
fieldRef: fieldRef ?? null,
|
|
105
|
+
sortingField: sortable && fieldName ? fieldName : null,
|
|
106
|
+
filterName: filterEnabled && fieldName ? fieldName : null,
|
|
107
|
+
filterHandler: filterEnabled && fieldName
|
|
108
|
+
? columnType.createFilterHandler(fieldName) as FilterHandler<FilterArtifact>
|
|
109
|
+
: undefined,
|
|
110
|
+
isTextSearchable: columnType.isTextSearchable,
|
|
111
|
+
columnType: columnType.name as ColumnLeafProps['columnType'],
|
|
112
|
+
enumName,
|
|
113
|
+
enumOptions,
|
|
114
|
+
header,
|
|
115
|
+
renderCell,
|
|
116
|
+
renderFilter: config.renderFilter
|
|
117
|
+
? ({ artifact, setArtifact }: { artifact: unknown; setArtifact: (artifact: FilterArtifact) => void }) => config.renderFilter!({
|
|
118
|
+
artifact: artifact as TFilterArtifact,
|
|
119
|
+
setArtifact: setArtifact as (artifact: TFilterArtifact) => void,
|
|
120
|
+
})
|
|
121
|
+
: undefined,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return React.createElement(ColumnLeaf, leafProps as ColumnLeafProps)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Column as ColumnComponent<TExtraProps>
|
|
128
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRelationColumn()` — factory for building relation column components (hasOne, hasMany).
|
|
3
|
+
*
|
|
4
|
+
* Parallel to `createColumn()` for scalar types. Handles:
|
|
5
|
+
* - Related entity metadata extraction (entityName, selection)
|
|
6
|
+
* - Collection phase (collectSelection)
|
|
7
|
+
* - Filter item rendering (renderFilterItem)
|
|
8
|
+
* - Cell rendering with children render function
|
|
9
|
+
*
|
|
10
|
+
* UI layers provide renderFilter and renderCellWrapper via config.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react'
|
|
14
|
+
import type { FieldRefBase, FilterArtifact, FilterHandler, EntityAccessor, SelectionMeta } from '@contember/bindx'
|
|
15
|
+
import { SelectionScope } from '@contember/bindx'
|
|
16
|
+
import { createCollectorProxy } from '@contember/bindx-react'
|
|
17
|
+
import type { ColumnTypeDef } from './columnTypes.js'
|
|
18
|
+
import { accessField } from './columnTypes.js'
|
|
19
|
+
|
|
20
|
+
/** If a render result is a FieldRef-like object with `.value`, extract the string value. */
|
|
21
|
+
function unwrapRenderResult(result: React.ReactNode): React.ReactNode {
|
|
22
|
+
if (result && typeof result === 'object' && 'value' in result) {
|
|
23
|
+
const val = (result as { value: unknown }).value
|
|
24
|
+
return val != null ? String(val) : ''
|
|
25
|
+
}
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
import { ColumnLeaf, type ColumnLeafProps } from './columnLeaf.js'
|
|
29
|
+
import { extractFieldName, extractRelatedEntityName, getRelatedAccessor } from './columns.js'
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Config
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface RelationColumnConfig {
|
|
36
|
+
/** Renders the filter UI in column header popover. Receives relation context. */
|
|
37
|
+
readonly renderFilter?: (ctx: RelationFilterContext) => React.ReactNode
|
|
38
|
+
/** Wraps cell content (e.g. tooltip with filter actions). */
|
|
39
|
+
readonly renderCellWrapper?: (ctx: RelationCellWrapperContext) => React.ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RelationFilterContext {
|
|
43
|
+
readonly filterName: string
|
|
44
|
+
readonly entityName: string
|
|
45
|
+
readonly selection: SelectionMeta
|
|
46
|
+
readonly renderItem: (accessor: EntityAccessor<object>) => React.ReactNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RelationCellWrapperContext {
|
|
50
|
+
readonly content: React.ReactNode
|
|
51
|
+
readonly item: EntityAccessor<object>
|
|
52
|
+
readonly fieldName: string
|
|
53
|
+
readonly filterName: string
|
|
54
|
+
readonly fieldRef: FieldRefBase<unknown>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Props
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export interface RelationColumnProps<TEntity, TSelected> {
|
|
62
|
+
field: object
|
|
63
|
+
header?: React.ReactNode
|
|
64
|
+
filter?: boolean
|
|
65
|
+
renderCellWrapper?: (content: React.ReactNode, item: EntityAccessor<object>) => React.ReactNode
|
|
66
|
+
children: (entity: EntityAccessor<TEntity, TSelected>) => React.ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Factory
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
interface RelationCellConfig {
|
|
74
|
+
/** How to collect selection for the relation (differs between hasOne and hasMany) */
|
|
75
|
+
collectSelection: (renderer: (ref: unknown) => React.ReactNode, fieldRef: FieldRefBase<unknown>) => void
|
|
76
|
+
/** How to render the cell content (differs between hasOne and hasMany) */
|
|
77
|
+
renderCell: (accessor: EntityAccessor<object>, fieldName: string, renderer: (ref: unknown) => React.ReactNode) => React.ReactNode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createRelationColumn<TFilterArtifact extends FilterArtifact>(
|
|
81
|
+
columnType: ColumnTypeDef<unknown, TFilterArtifact>,
|
|
82
|
+
cellConfig: RelationCellConfig,
|
|
83
|
+
uiConfig: RelationColumnConfig = {},
|
|
84
|
+
) {
|
|
85
|
+
function buildLeaf(props: Record<string, unknown>): ColumnLeafProps {
|
|
86
|
+
const fieldRef = props['field'] as FieldRefBase<unknown> | undefined
|
|
87
|
+
const fieldName = fieldRef ? extractFieldName(fieldRef) : null
|
|
88
|
+
const renderer = props['children'] as ((ref: unknown) => React.ReactNode) | undefined
|
|
89
|
+
const header = props['header'] as React.ReactNode | undefined
|
|
90
|
+
const filterEnabled = (props['filter'] as boolean | undefined) ?? true
|
|
91
|
+
const customCellWrapper = props['renderCellWrapper'] as ((content: React.ReactNode, item: EntityAccessor<object>) => React.ReactNode) | undefined
|
|
92
|
+
|
|
93
|
+
const relatedEntityName = extractRelatedEntityName(fieldRef) ?? ''
|
|
94
|
+
|
|
95
|
+
// Collect selection metadata for the related entity (for filter fetching)
|
|
96
|
+
let relatedSelection: SelectionMeta = { fields: new Map() }
|
|
97
|
+
if (relatedEntityName && renderer) {
|
|
98
|
+
const scope = new SelectionScope()
|
|
99
|
+
const proxy = createCollectorProxy(scope, relatedEntityName)
|
|
100
|
+
renderer(proxy)
|
|
101
|
+
relatedSelection = scope.toSelectionMeta()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const renderFilterItem = renderer
|
|
105
|
+
? (accessor: EntityAccessor<object>): React.ReactNode => unwrapRenderResult(renderer(accessor))
|
|
106
|
+
: (() => null)
|
|
107
|
+
|
|
108
|
+
const filterName = filterEnabled && fieldName ? fieldName : null
|
|
109
|
+
|
|
110
|
+
// Resolve renderFilter — use UI config, or leave undefined (headless)
|
|
111
|
+
// Note: relation filter UI ignores artifact/setArtifact (uses hooks internally)
|
|
112
|
+
const renderFilter = filterName && relatedEntityName && uiConfig.renderFilter
|
|
113
|
+
? (_filterProps: { artifact: FilterArtifact; setArtifact: (artifact: FilterArtifact) => void }) =>
|
|
114
|
+
uiConfig.renderFilter!({ filterName, entityName: relatedEntityName, selection: relatedSelection, renderItem: renderFilterItem })
|
|
115
|
+
: undefined
|
|
116
|
+
|
|
117
|
+
// Resolve renderCellWrapper — user prop overrides UI config default
|
|
118
|
+
const resolvedCellWrapper = customCellWrapper
|
|
119
|
+
?? (filterName && fieldName && fieldRef && uiConfig.renderCellWrapper
|
|
120
|
+
? (content: React.ReactNode, item: EntityAccessor<object>) =>
|
|
121
|
+
uiConfig.renderCellWrapper!({ content, item, fieldName, filterName, fieldRef })
|
|
122
|
+
: undefined)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: fieldName ?? `col-${Math.random().toString(36).slice(2, 8)}`,
|
|
126
|
+
fieldName,
|
|
127
|
+
fieldRef: fieldRef ?? null,
|
|
128
|
+
sortingField: null,
|
|
129
|
+
filterName,
|
|
130
|
+
filterHandler: filterName
|
|
131
|
+
? columnType.createFilterHandler(fieldName!) as FilterHandler<FilterArtifact>
|
|
132
|
+
: undefined,
|
|
133
|
+
isTextSearchable: false,
|
|
134
|
+
columnType: columnType.name as ColumnLeafProps['columnType'],
|
|
135
|
+
relatedEntityName,
|
|
136
|
+
relatedSelection,
|
|
137
|
+
renderFilterItem,
|
|
138
|
+
renderFilter,
|
|
139
|
+
renderCellWrapper: resolvedCellWrapper,
|
|
140
|
+
header,
|
|
141
|
+
collectSelection: () => {
|
|
142
|
+
if (renderer && fieldRef) {
|
|
143
|
+
cellConfig.collectSelection(renderer, fieldRef)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
renderCell: (accessor: EntityAccessor<object>) => {
|
|
147
|
+
if (!fieldName || !renderer) return null
|
|
148
|
+
return cellConfig.renderCell(accessor, fieldName, renderer)
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const Component = Object.assign(
|
|
154
|
+
function RelationColumn<TEntity, TSelected>(_props: RelationColumnProps<TEntity, TSelected>): null {
|
|
155
|
+
return null
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
staticRender: (props: Record<string, unknown>): React.ReactNode => {
|
|
159
|
+
return React.createElement(ColumnLeaf, buildLeaf(props) as ColumnLeafProps)
|
|
160
|
+
},
|
|
161
|
+
buildLeaf,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return Component as RelationColumnComponent
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface RelationColumnComponent {
|
|
169
|
+
<TEntity, TSelected>(props: RelationColumnProps<TEntity, TSelected>): null
|
|
170
|
+
staticRender: (props: Record<string, unknown>) => React.ReactNode
|
|
171
|
+
buildLeaf: (props: Record<string, unknown>) => ColumnLeafProps
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Cell configs for hasOne and hasMany
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
export const hasOneCellConfig: RelationCellConfig = {
|
|
179
|
+
collectSelection: (renderer, fieldRef) => {
|
|
180
|
+
renderer(fieldRef)
|
|
181
|
+
},
|
|
182
|
+
renderCell: (accessor, fieldName, renderer) => {
|
|
183
|
+
const related = getRelatedAccessor(accessor, fieldName)
|
|
184
|
+
if (!related) return null
|
|
185
|
+
return unwrapRenderResult(renderer(related))
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const hasManyCellConfig: RelationCellConfig = {
|
|
190
|
+
collectSelection: (renderer, fieldRef) => {
|
|
191
|
+
const ref = fieldRef as { map?: (fn: (item: unknown, index: number) => unknown) => unknown[] }
|
|
192
|
+
ref.map?.((item) => { renderer(item); return null })
|
|
193
|
+
},
|
|
194
|
+
renderCell: (accessor, fieldName, renderer) => {
|
|
195
|
+
const ref = accessField(accessor, fieldName) as { items?: EntityAccessor<object>[] } | null
|
|
196
|
+
const items = ref?.items
|
|
197
|
+
if (!Array.isArray(items) || items.length === 0) return ''
|
|
198
|
+
|
|
199
|
+
return items.map((item, i) => (
|
|
200
|
+
<React.Fragment key={i}>{i > 0 ? ', ' : ''}{unwrapRenderResult(renderer(item))}</React.Fragment>
|
|
201
|
+
))
|
|
202
|
+
},
|
|
203
|
+
}
|