@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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColumnLeaf — the data carrier for extracted column metadata.
|
|
3
|
+
*
|
|
4
|
+
* `ColumnLeaf` is a marker React component (returns null) whose props carry
|
|
5
|
+
* all column metadata. `analyzeChildren()` walks a JSX element tree,
|
|
6
|
+
* resolving components with `staticRender` and collecting props from registered
|
|
7
|
+
* marker types — a lightweight equivalent of contember-oss's ChildrenAnalyzer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import type { FieldRefBase, FilterHandler, FilterArtifact, EntityAccessor } from '@contember/bindx'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// ColumnLeaf Props — the new ColumnMeta
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Column Leaf Props
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** All known column type literals for type narrowing */
|
|
22
|
+
export type ColumnType = 'text' | 'number' | 'date' | 'dateTime' | 'boolean' | 'uuid' | 'isDefined' | 'enum' | 'enumList' | 'hasOne' | 'hasMany' | 'action' | (string & {})
|
|
23
|
+
|
|
24
|
+
export interface ColumnLeafProps {
|
|
25
|
+
// ── Core ──
|
|
26
|
+
readonly name: string
|
|
27
|
+
readonly fieldName: string | null
|
|
28
|
+
readonly fieldRef: FieldRefBase<unknown> | null
|
|
29
|
+
readonly sortingField: string | null
|
|
30
|
+
readonly filterName: string | null
|
|
31
|
+
readonly filterHandler: FilterHandler<FilterArtifact> | undefined
|
|
32
|
+
readonly isTextSearchable: boolean
|
|
33
|
+
readonly columnType?: ColumnType
|
|
34
|
+
readonly collectSelection?: (collectorProxy: unknown) => void
|
|
35
|
+
|
|
36
|
+
// ── Enum ──
|
|
37
|
+
/** Enum type name for resolving options from context (e.g. 'DeploymentStatus') */
|
|
38
|
+
readonly enumName?: string
|
|
39
|
+
/** Explicit enum options — value → label (overrides context-resolved options) */
|
|
40
|
+
readonly enumOptions?: Readonly<Record<string, React.ReactNode>>
|
|
41
|
+
|
|
42
|
+
// ── Relation ──
|
|
43
|
+
readonly relatedEntityName?: string
|
|
44
|
+
readonly relatedSelection?: import('@contember/bindx').SelectionMeta
|
|
45
|
+
readonly renderFilterItem?: (accessor: EntityAccessor<object>) => React.ReactNode
|
|
46
|
+
|
|
47
|
+
// ── UI ──
|
|
48
|
+
readonly header?: React.ReactNode
|
|
49
|
+
readonly renderCell: (accessor: EntityAccessor<object>) => React.ReactNode
|
|
50
|
+
readonly renderFilter?: (props: { artifact: FilterArtifact; setArtifact: (artifact: FilterArtifact) => void }) => React.ReactNode
|
|
51
|
+
readonly renderCellWrapper?: (content: React.ReactNode, item: EntityAccessor<object>) => React.ReactNode
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Type guard for relation columns */
|
|
55
|
+
export function isRelationColumn(col: ColumnLeafProps): col is ColumnLeafProps & { columnType: 'hasOne' | 'hasMany'; relatedEntityName: string; renderFilterItem: (accessor: EntityAccessor<object>) => React.ReactNode } {
|
|
56
|
+
return (col.columnType === 'hasOne' || col.columnType === 'hasMany') && !!col.relatedEntityName && !!col.renderFilterItem
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Marker component — returns null at runtime.
|
|
61
|
+
* Its props are the column metadata extracted by `analyzeChildren()`.
|
|
62
|
+
*/
|
|
63
|
+
export function ColumnLeaf(_props: ColumnLeafProps): null {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Generalized Children Analyzer
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
interface ComponentWithStaticRender {
|
|
72
|
+
staticRender: (props: Record<string, unknown>) => React.ReactNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasStaticRender(type: unknown): type is ComponentWithStaticRender {
|
|
76
|
+
return typeof type === 'function' && 'staticRender' in type
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Result of analyzing a JSX element tree for marker components.
|
|
81
|
+
*/
|
|
82
|
+
export interface ChildrenAnalysisResult {
|
|
83
|
+
/** Get all collected props for a given marker type */
|
|
84
|
+
getAll<TProps>(marker: React.ComponentType<TProps>): TProps[]
|
|
85
|
+
/** Get the first collected props for a given marker type, or undefined */
|
|
86
|
+
getFirst<TProps>(marker: React.ComponentType<TProps>): TProps | undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk a JSX element tree and collect props from registered marker types.
|
|
91
|
+
*
|
|
92
|
+
* For each element:
|
|
93
|
+
* 1. If its type is in `markerTypes` → collect its props
|
|
94
|
+
* 2. If it's a `React.Fragment` → recurse into children
|
|
95
|
+
* 3. If it has a `staticRender` static method → call it and recurse into result
|
|
96
|
+
* 4. Otherwise → skip (renders normally at runtime)
|
|
97
|
+
*/
|
|
98
|
+
export function analyzeChildren(
|
|
99
|
+
elements: React.ReactNode,
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
markerTypes: ReadonlySet<React.ComponentType<any>>,
|
|
102
|
+
): ChildrenAnalysisResult {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const collected = new Map<React.ComponentType<any>, unknown[]>()
|
|
105
|
+
for (const type of markerTypes) {
|
|
106
|
+
collected.set(type, [])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
walkTree(elements, markerTypes, collected)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
getAll<TProps>(marker: React.ComponentType<TProps>): TProps[] {
|
|
113
|
+
return (collected.get(marker) ?? []) as TProps[]
|
|
114
|
+
},
|
|
115
|
+
getFirst<TProps>(marker: React.ComponentType<TProps>): TProps | undefined {
|
|
116
|
+
const items = collected.get(marker)
|
|
117
|
+
return items?.[0] as TProps | undefined
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function walkTree(
|
|
123
|
+
elements: React.ReactNode,
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
markerTypes: ReadonlySet<React.ComponentType<any>>,
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
collected: Map<React.ComponentType<any>, unknown[]>,
|
|
128
|
+
): void {
|
|
129
|
+
React.Children.forEach(elements, (child) => {
|
|
130
|
+
if (!React.isValidElement(child)) return
|
|
131
|
+
|
|
132
|
+
if (markerTypes.has(child.type as React.ComponentType)) {
|
|
133
|
+
collected.get(child.type as React.ComponentType)!.push(child.props)
|
|
134
|
+
} else if (child.type === React.Fragment) {
|
|
135
|
+
walkTree((child.props as { children?: React.ReactNode }).children, markerTypes, collected)
|
|
136
|
+
} else if (hasStaticRender(child.type)) {
|
|
137
|
+
const rendered = child.type.staticRender(child.props as Record<string, unknown>)
|
|
138
|
+
walkTree(rendered, markerTypes, collected)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convenience wrapper: extract `ColumnLeafProps` from a JSX element tree.
|
|
145
|
+
*/
|
|
146
|
+
export function extractColumnLeaves(elements: React.ReactNode): ColumnLeafProps[] {
|
|
147
|
+
return analyzeChildren(elements, new Set([ColumnLeaf])).getAll(ColumnLeaf)
|
|
148
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column type definitions — headless behavior layer for DataGrid columns.
|
|
3
|
+
*
|
|
4
|
+
* Each column type defines how to extract values, create filter handlers,
|
|
5
|
+
* and whether sorting/text search is supported by default.
|
|
6
|
+
*
|
|
7
|
+
* These are framework-agnostic — no React dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FilterHandler, FilterArtifact, EntityAccessor } from '@contember/bindx'
|
|
11
|
+
import type {
|
|
12
|
+
TextFilterArtifact,
|
|
13
|
+
NumberRangeFilterArtifact,
|
|
14
|
+
DateFilterArtifact,
|
|
15
|
+
BooleanFilterArtifact,
|
|
16
|
+
EnumFilterArtifact,
|
|
17
|
+
EnumListFilterArtifact,
|
|
18
|
+
IsDefinedFilterArtifact,
|
|
19
|
+
RelationFilterArtifact,
|
|
20
|
+
} from '@contember/bindx'
|
|
21
|
+
import {
|
|
22
|
+
createTextFilterHandler,
|
|
23
|
+
createNumberRangeFilterHandler,
|
|
24
|
+
createDateFilterHandler,
|
|
25
|
+
createBooleanFilterHandler,
|
|
26
|
+
createEnumFilterHandler,
|
|
27
|
+
createEnumListFilterHandler,
|
|
28
|
+
createIsDefinedFilterHandler,
|
|
29
|
+
createRelationFilterHandler,
|
|
30
|
+
} from '@contember/bindx'
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Column Type Definition Interface
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export interface ColumnTypeDef<TValue = unknown, TFilterArtifact extends FilterArtifact = FilterArtifact> {
|
|
37
|
+
readonly name: string
|
|
38
|
+
readonly defaultSortable: boolean
|
|
39
|
+
readonly isTextSearchable: boolean
|
|
40
|
+
readonly createFilterHandler: (fieldName: string) => FilterHandler<TFilterArtifact>
|
|
41
|
+
readonly extractValue: (accessor: EntityAccessor<object>, fieldName: string) => TValue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defineColumnType<TValue, TFilterArtifact extends FilterArtifact>(
|
|
45
|
+
def: ColumnTypeDef<TValue, TFilterArtifact>,
|
|
46
|
+
): ColumnTypeDef<TValue, TFilterArtifact> {
|
|
47
|
+
return def
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Value Extractors
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Access a field on an EntityAccessor by name.
|
|
56
|
+
* EntityAccessor is a Proxy — bracket notation triggers the proxy get trap.
|
|
57
|
+
*/
|
|
58
|
+
export function accessField(accessor: EntityAccessor<object>, fieldName: string): unknown {
|
|
59
|
+
// EntityAccessor is a Proxy — bracket access goes through the get trap
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
return (accessor as any)[fieldName]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractScalarValue<T>(accessor: EntityAccessor<object>, fieldName: string): T | null {
|
|
65
|
+
const fieldRef = accessField(accessor, fieldName) as { value?: unknown } | null
|
|
66
|
+
if (!fieldRef || typeof fieldRef !== 'object') return null
|
|
67
|
+
return (fieldRef.value ?? null) as T | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Built-in Column Type Definitions
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export const textColumnDef: ColumnTypeDef<string | null, TextFilterArtifact> = defineColumnType({
|
|
75
|
+
name: 'text',
|
|
76
|
+
defaultSortable: true,
|
|
77
|
+
isTextSearchable: true,
|
|
78
|
+
createFilterHandler: createTextFilterHandler,
|
|
79
|
+
extractValue: (accessor, fieldName) => extractScalarValue<string>(accessor, fieldName),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const numberColumnDef: ColumnTypeDef<number | null, NumberRangeFilterArtifact> = defineColumnType({
|
|
83
|
+
name: 'number',
|
|
84
|
+
defaultSortable: true,
|
|
85
|
+
isTextSearchable: false,
|
|
86
|
+
createFilterHandler: createNumberRangeFilterHandler,
|
|
87
|
+
extractValue: (accessor, fieldName) => extractScalarValue<number>(accessor, fieldName),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const dateColumnDef: ColumnTypeDef<string | null, DateFilterArtifact> = defineColumnType({
|
|
91
|
+
name: 'date',
|
|
92
|
+
defaultSortable: true,
|
|
93
|
+
isTextSearchable: false,
|
|
94
|
+
createFilterHandler: createDateFilterHandler,
|
|
95
|
+
extractValue: (accessor, fieldName) => extractScalarValue<string>(accessor, fieldName),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const dateTimeColumnDef: ColumnTypeDef<string | null, DateFilterArtifact> = defineColumnType({
|
|
99
|
+
name: 'dateTime',
|
|
100
|
+
defaultSortable: true,
|
|
101
|
+
isTextSearchable: false,
|
|
102
|
+
createFilterHandler: createDateFilterHandler,
|
|
103
|
+
extractValue: (accessor, fieldName) => extractScalarValue<string>(accessor, fieldName),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export const booleanColumnDef: ColumnTypeDef<boolean | null, BooleanFilterArtifact> = defineColumnType({
|
|
107
|
+
name: 'boolean',
|
|
108
|
+
defaultSortable: false,
|
|
109
|
+
isTextSearchable: false,
|
|
110
|
+
createFilterHandler: createBooleanFilterHandler,
|
|
111
|
+
extractValue: (accessor, fieldName) => extractScalarValue<boolean>(accessor, fieldName),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export const enumColumnDef: ColumnTypeDef<string | null, EnumFilterArtifact> = defineColumnType({
|
|
115
|
+
name: 'enum',
|
|
116
|
+
defaultSortable: true,
|
|
117
|
+
isTextSearchable: false,
|
|
118
|
+
createFilterHandler: createEnumFilterHandler,
|
|
119
|
+
extractValue: (accessor, fieldName) => extractScalarValue<string>(accessor, fieldName),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const enumListColumnDef: ColumnTypeDef<readonly string[] | null, EnumListFilterArtifact> = defineColumnType({
|
|
123
|
+
name: 'enumList',
|
|
124
|
+
defaultSortable: false,
|
|
125
|
+
isTextSearchable: false,
|
|
126
|
+
createFilterHandler: createEnumListFilterHandler,
|
|
127
|
+
extractValue: (accessor, fieldName) => extractScalarValue<readonly string[]>(accessor, fieldName),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
export const uuidColumnDef: ColumnTypeDef<string | null, TextFilterArtifact> = defineColumnType({
|
|
131
|
+
name: 'uuid',
|
|
132
|
+
defaultSortable: false,
|
|
133
|
+
isTextSearchable: false,
|
|
134
|
+
createFilterHandler: createTextFilterHandler,
|
|
135
|
+
extractValue: (accessor, fieldName) => extractScalarValue<string>(accessor, fieldName),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
export const isDefinedColumnDef: ColumnTypeDef<unknown, IsDefinedFilterArtifact> = defineColumnType({
|
|
139
|
+
name: 'isDefined',
|
|
140
|
+
defaultSortable: false,
|
|
141
|
+
isTextSearchable: false,
|
|
142
|
+
createFilterHandler: createIsDefinedFilterHandler,
|
|
143
|
+
extractValue: (accessor, fieldName) => extractScalarValue<unknown>(accessor, fieldName),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
export const hasOneColumnDef: ColumnTypeDef<unknown, RelationFilterArtifact> = defineColumnType({
|
|
147
|
+
name: 'hasOne',
|
|
148
|
+
defaultSortable: false,
|
|
149
|
+
isTextSearchable: false,
|
|
150
|
+
createFilterHandler: createRelationFilterHandler,
|
|
151
|
+
extractValue: (accessor, fieldName) => accessField(accessor, fieldName) ?? null,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
export const hasManyColumnDef: ColumnTypeDef<unknown, RelationFilterArtifact> = defineColumnType({
|
|
155
|
+
name: 'hasMany',
|
|
156
|
+
defaultSortable: false,
|
|
157
|
+
isTextSearchable: false,
|
|
158
|
+
createFilterHandler: createRelationFilterHandler,
|
|
159
|
+
extractValue: (accessor, fieldName) => accessField(accessor, fieldName) ?? null,
|
|
160
|
+
})
|
package/src/columns.tsx
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataGrid column components.
|
|
3
|
+
*
|
|
4
|
+
* These are "metadata" components — they are not rendered directly.
|
|
5
|
+
* Instead, they carry `staticRender` methods that produce `ColumnLeaf`
|
|
6
|
+
* elements, which the DataGrid extracts for column metadata.
|
|
7
|
+
*
|
|
8
|
+
* Scalar columns are built via `createColumn()` + column type defs.
|
|
9
|
+
* Relation/action/generic columns use manual `staticRender`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { ReactNode } from 'react'
|
|
13
|
+
import type { FieldRefBase, HasOneRef, HasManyRef, FilterHandler, FilterArtifact, EntityAccessor, EnumFilterArtifact, EnumListFilterArtifact, SelectionMeta } from '@contember/bindx'
|
|
14
|
+
import { SelectionScope } from '@contember/bindx'
|
|
15
|
+
import { FIELD_REF_META, createCollectorProxy } from '@contember/bindx-react'
|
|
16
|
+
import { createColumn, type ColumnRenderProps } from './createColumn.js'
|
|
17
|
+
import { accessField } from './columnTypes.js'
|
|
18
|
+
import { createRelationColumn, hasOneCellConfig, hasManyCellConfig, type RelationColumnProps } from './createRelationColumn.jsx'
|
|
19
|
+
import {
|
|
20
|
+
textColumnDef,
|
|
21
|
+
numberColumnDef,
|
|
22
|
+
dateColumnDef,
|
|
23
|
+
dateTimeColumnDef,
|
|
24
|
+
booleanColumnDef,
|
|
25
|
+
enumColumnDef,
|
|
26
|
+
enumListColumnDef,
|
|
27
|
+
uuidColumnDef,
|
|
28
|
+
isDefinedColumnDef,
|
|
29
|
+
hasOneColumnDef,
|
|
30
|
+
hasManyColumnDef,
|
|
31
|
+
} from './columnTypes.js'
|
|
32
|
+
import { ColumnLeaf, type ColumnLeafProps } from './columnLeaf.js'
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Re-export ColumnLeafProps as ColumnMeta for compatibility
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export type ColumnMeta = ColumnLeafProps
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Extraction Helpers
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
interface FieldRefMetaCarrier {
|
|
45
|
+
readonly [FIELD_REF_META]: {
|
|
46
|
+
readonly entityType: string
|
|
47
|
+
readonly fieldName: string
|
|
48
|
+
readonly isArray: boolean
|
|
49
|
+
readonly isRelation: boolean
|
|
50
|
+
readonly enumName?: string
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Type guard: checks if a value carries FIELD_REF_META symbol. */
|
|
55
|
+
export function hasFieldRefMeta(ref: unknown): ref is FieldRefMetaCarrier {
|
|
56
|
+
return ref != null && typeof ref === 'object' && FIELD_REF_META in ref
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Extract field name from a field ref (works in both collector and runtime proxies). */
|
|
60
|
+
export function extractFieldName(ref: unknown): string | null {
|
|
61
|
+
return hasFieldRefMeta(ref) ? ref[FIELD_REF_META].fieldName : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extract enum name from a field ref (if field is an enum). */
|
|
65
|
+
export function extractEnumName(ref: unknown): string | undefined {
|
|
66
|
+
return hasFieldRefMeta(ref) ? ref[FIELD_REF_META].enumName : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Extract related entity type name from a relation field ref. */
|
|
70
|
+
export function extractRelatedEntityName(ref: unknown): string | null {
|
|
71
|
+
if (!hasFieldRefMeta(ref)) return null
|
|
72
|
+
const meta = ref[FIELD_REF_META]
|
|
73
|
+
return meta.entityType || null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Access a related entity accessor from a parent row accessor by field name.
|
|
78
|
+
* EntityAccessor is a Proxy — bracket notation triggers the get trap.
|
|
79
|
+
*/
|
|
80
|
+
export function getRelatedAccessor(item: EntityAccessor<object>, fieldName: string): EntityAccessor<object> | null {
|
|
81
|
+
return accessField(item, fieldName) as EntityAccessor<object> | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Default Cell Renderers
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
function renderScalarDefault({ value }: ColumnRenderProps<unknown>): React.ReactNode {
|
|
89
|
+
return value != null ? String(value) : ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderBooleanDefault({ value }: ColumnRenderProps<boolean | null>): React.ReactNode {
|
|
93
|
+
return value != null ? String(value) : ''
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderIsDefinedDefault({ value }: ColumnRenderProps<unknown>): React.ReactNode {
|
|
97
|
+
return value != null ? '✓' : '✗'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderDateTimeDefault({ value }: ColumnRenderProps<string | null>): React.ReactNode {
|
|
101
|
+
if (typeof value !== 'string') return ''
|
|
102
|
+
const date = new Date(value)
|
|
103
|
+
if (isNaN(date.getTime())) return value
|
|
104
|
+
return date.toLocaleString()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderEnumListDefault({ value }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
|
|
108
|
+
if (!Array.isArray(value)) return ''
|
|
109
|
+
return value.join(', ')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Scalar Column Props
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
interface DataGridScalarColumnPropsBase<T> {
|
|
117
|
+
field: FieldRefBase<T> | FieldRefBase<T | undefined>
|
|
118
|
+
header?: React.ReactNode
|
|
119
|
+
sortable?: boolean
|
|
120
|
+
filter?: boolean
|
|
121
|
+
children?: (value: T | null) => React.ReactNode
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface DataGridTextColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
125
|
+
export interface DataGridNumberColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
126
|
+
export interface DataGridDateColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
127
|
+
export interface DataGridDateTimeColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
128
|
+
export interface DataGridBooleanColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
129
|
+
export interface DataGridUuidColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
130
|
+
export interface DataGridIsDefinedColumnProps<T> extends DataGridScalarColumnPropsBase<T> {}
|
|
131
|
+
|
|
132
|
+
interface EnumExtraProps<T extends string> {
|
|
133
|
+
options: readonly T[] | Record<T, ReactNode>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DataGridEnumColumnProps<T extends string> extends DataGridScalarColumnPropsBase<T>, EnumExtraProps<NoInfer<T>> {}
|
|
137
|
+
|
|
138
|
+
export interface DataGridEnumListColumnProps<T extends string> extends DataGridScalarColumnPropsBase<T>, EnumExtraProps<NoInfer<T>> {}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Scalar Columns via createColumn()
|
|
142
|
+
//
|
|
143
|
+
// createColumn handles `children` (custom cell renderer) and `options` (enum)
|
|
144
|
+
// natively — no monkey-patching needed.
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
export const DataGridTextColumn = createColumn(textColumnDef, {
|
|
148
|
+
renderCell: renderScalarDefault,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
export const DataGridNumberColumn = createColumn(numberColumnDef, {
|
|
152
|
+
renderCell: renderScalarDefault,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
export const DataGridDateColumn = createColumn(dateColumnDef, {
|
|
156
|
+
renderCell: renderScalarDefault,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export const DataGridDateTimeColumn = createColumn(dateTimeColumnDef, {
|
|
160
|
+
renderCell: renderDateTimeDefault,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
export const DataGridBooleanColumn = createColumn(booleanColumnDef, {
|
|
164
|
+
renderCell: renderBooleanDefault,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
export const DataGridEnumColumn = createColumn<string | null, EnumFilterArtifact, EnumExtraProps<string>>(enumColumnDef, {
|
|
168
|
+
renderCell: renderScalarDefault,
|
|
169
|
+
}) as <TValue extends string>(props: DataGridEnumColumnProps<TValue>) => ReactNode
|
|
170
|
+
|
|
171
|
+
export const DataGridEnumListColumn = createColumn<readonly string[] | null, EnumListFilterArtifact, EnumExtraProps<string>>(enumListColumnDef, {
|
|
172
|
+
renderCell: renderEnumListDefault,
|
|
173
|
+
}) as <TValue extends string>(props: DataGridEnumListColumnProps<TValue>) => ReactNode
|
|
174
|
+
|
|
175
|
+
export const DataGridUuidColumn = createColumn(uuidColumnDef, {
|
|
176
|
+
renderCell: renderScalarDefault,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
export const DataGridIsDefinedColumn = createColumn(isDefinedColumnDef, {
|
|
180
|
+
renderCell: renderIsDefinedDefault,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Relation Column Props & Components
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
export interface DataGridHasOneColumnProps<TEntity, TSelected> extends RelationColumnProps<TEntity, TSelected> {
|
|
188
|
+
field: HasOneRef<TEntity, TSelected>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Headless HasOne column — no filter UI or cell tooltip. Use @contember/bindx-ui for styled version. */
|
|
192
|
+
const _DataGridHasOneColumn = createRelationColumn(hasOneColumnDef, hasOneCellConfig)
|
|
193
|
+
export const DataGridHasOneColumn: {
|
|
194
|
+
<TEntity, TSelected>(props: DataGridHasOneColumnProps<TEntity, TSelected>): null
|
|
195
|
+
staticRender: typeof _DataGridHasOneColumn.staticRender
|
|
196
|
+
buildLeaf: typeof _DataGridHasOneColumn.buildLeaf
|
|
197
|
+
} = _DataGridHasOneColumn
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// HasMany Column
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
export interface DataGridHasManyColumnProps<TEntity, TSelected> extends RelationColumnProps<TEntity, TSelected> {
|
|
204
|
+
field: HasManyRef<TEntity, TSelected>
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Headless HasMany column — no filter UI or cell tooltip. Use @contember/bindx-ui for styled version. */
|
|
208
|
+
const _DataGridHasManyColumn = createRelationColumn(hasManyColumnDef, hasManyCellConfig)
|
|
209
|
+
export const DataGridHasManyColumn: {
|
|
210
|
+
<TEntity, TSelected>(props: DataGridHasManyColumnProps<TEntity, TSelected>): null
|
|
211
|
+
staticRender: typeof _DataGridHasManyColumn.staticRender
|
|
212
|
+
buildLeaf: typeof _DataGridHasManyColumn.buildLeaf
|
|
213
|
+
} = _DataGridHasManyColumn
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Action Column
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
export interface DataGridActionColumnProps {
|
|
220
|
+
children: React.ReactNode | ((entity: EntityAccessor<object>) => React.ReactNode)
|
|
221
|
+
header?: React.ReactNode
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const DataGridActionColumn = Object.assign(
|
|
225
|
+
function DataGridActionColumn(_props: DataGridActionColumnProps): null {
|
|
226
|
+
return null
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
staticRender: (props: Record<string, unknown>): React.ReactNode => {
|
|
230
|
+
const header = (props['header'] as React.ReactNode) ?? ''
|
|
231
|
+
const children = props['children']
|
|
232
|
+
const cellRenderer = typeof children === 'function'
|
|
233
|
+
? children as (value: unknown) => React.ReactNode
|
|
234
|
+
: () => children as React.ReactNode
|
|
235
|
+
|
|
236
|
+
const leafProps: ColumnLeafProps = {
|
|
237
|
+
columnType: 'action',
|
|
238
|
+
name: `action-${Math.random().toString(36).slice(2, 8)}`,
|
|
239
|
+
fieldName: null,
|
|
240
|
+
fieldRef: null,
|
|
241
|
+
sortingField: null,
|
|
242
|
+
filterName: null,
|
|
243
|
+
filterHandler: undefined,
|
|
244
|
+
isTextSearchable: false,
|
|
245
|
+
header,
|
|
246
|
+
renderCell: (accessor: EntityAccessor<object>) => cellRenderer(accessor),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return React.createElement(ColumnLeaf, leafProps as ColumnLeafProps)
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Generic Column
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
export interface DataGridColumnProps<T> {
|
|
259
|
+
field?: FieldRefBase<T>
|
|
260
|
+
header?: React.ReactNode
|
|
261
|
+
sortable?: boolean
|
|
262
|
+
filter?: boolean
|
|
263
|
+
filterHandler?: FilterHandler<FilterArtifact>
|
|
264
|
+
children?: (value: T | null, accessor: EntityAccessor<object>) => React.ReactNode
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const DataGridColumn = Object.assign(
|
|
268
|
+
function DataGridColumn<T>(_props: DataGridColumnProps<T>): null {
|
|
269
|
+
return null
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
staticRender: (props: Record<string, unknown>): React.ReactNode => {
|
|
273
|
+
const fieldRef = props['field'] as FieldRefBase<unknown> | undefined
|
|
274
|
+
const fieldName = fieldRef ? extractFieldName(fieldRef) : null
|
|
275
|
+
const header = props['header'] as React.ReactNode | undefined
|
|
276
|
+
const sortable = (props['sortable'] as boolean | undefined) ?? false
|
|
277
|
+
const filterEnabled = (props['filter'] as boolean | undefined) ?? true
|
|
278
|
+
const customHandler = props['filterHandler'] as FilterHandler<FilterArtifact> | undefined
|
|
279
|
+
const children = props['children'] as ((value: unknown, accessor: EntityAccessor<object>) => React.ReactNode) | undefined
|
|
280
|
+
|
|
281
|
+
const leafProps: ColumnLeafProps = {
|
|
282
|
+
name: fieldName ?? `col-${Math.random().toString(36).slice(2, 8)}`,
|
|
283
|
+
fieldName,
|
|
284
|
+
fieldRef: fieldRef ?? null,
|
|
285
|
+
sortingField: sortable && fieldName ? fieldName : null,
|
|
286
|
+
filterName: filterEnabled && fieldName ? fieldName : null,
|
|
287
|
+
filterHandler: filterEnabled && fieldName
|
|
288
|
+
? (customHandler ?? textColumnDef.createFilterHandler(fieldName) as FilterHandler<FilterArtifact>)
|
|
289
|
+
: undefined,
|
|
290
|
+
isTextSearchable: false,
|
|
291
|
+
header,
|
|
292
|
+
renderCell: (accessor: EntityAccessor<object>) => {
|
|
293
|
+
if (!fieldName) return null
|
|
294
|
+
const ref = (accessor as unknown as Record<string, unknown>)[fieldName]
|
|
295
|
+
const value = ref && typeof ref === 'object' && 'value' in ref
|
|
296
|
+
? (ref as { value: unknown }).value ?? null
|
|
297
|
+
: null
|
|
298
|
+
if (children) return children(value, accessor)
|
|
299
|
+
return value != null ? String(value) : ''
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return React.createElement(ColumnLeaf, leafProps as ColumnLeafProps)
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
)
|