@contember/bindx-dataview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/DataGrid.d.ts +38 -0
  2. package/dist/DataGrid.d.ts.map +1 -0
  3. package/dist/DataViewContext.d.ts +39 -0
  4. package/dist/DataViewContext.d.ts.map +1 -0
  5. package/dist/DataViewKeyProvider.d.ts +8 -0
  6. package/dist/DataViewKeyProvider.d.ts.map +1 -0
  7. package/dist/HasManyDataGrid.d.ts +51 -0
  8. package/dist/HasManyDataGrid.d.ts.map +1 -0
  9. package/dist/columnLeaf.d.ts +72 -0
  10. package/dist/columnLeaf.d.ts.map +1 -0
  11. package/dist/columnTypes.d.ts +35 -0
  12. package/dist/columnTypes.d.ts.map +1 -0
  13. package/dist/columns.d.ts +115 -0
  14. package/dist/columns.d.ts.map +1 -0
  15. package/dist/contextHooks.d.ts +45 -0
  16. package/dist/contextHooks.d.ts.map +1 -0
  17. package/dist/createColumn.d.ts +37 -0
  18. package/dist/createColumn.d.ts.map +1 -0
  19. package/dist/createRelationColumn.d.ts +57 -0
  20. package/dist/createRelationColumn.d.ts.map +1 -0
  21. package/dist/dataAttribute.d.ts +6 -0
  22. package/dist/dataAttribute.d.ts.map +1 -0
  23. package/dist/export.d.ts +46 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/filterComponents.d.ts +127 -0
  26. package/dist/filterComponents.d.ts.map +1 -0
  27. package/dist/filterContext.d.ts +10 -0
  28. package/dist/filterContext.d.ts.map +1 -0
  29. package/dist/filterHooks.d.ts +74 -0
  30. package/dist/filterHooks.d.ts.map +1 -0
  31. package/dist/filterScope.d.ts +54 -0
  32. package/dist/filterScope.d.ts.map +1 -0
  33. package/dist/filterWrappers.d.ts +66 -0
  34. package/dist/filterWrappers.d.ts.map +1 -0
  35. package/dist/index.d.ts +28 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/infiniteLoad.d.ts +32 -0
  38. package/dist/infiniteLoad.d.ts.map +1 -0
  39. package/dist/loaderComponents.d.ts +75 -0
  40. package/dist/loaderComponents.d.ts.map +1 -0
  41. package/dist/markers.d.ts +36 -0
  42. package/dist/markers.d.ts.map +1 -0
  43. package/dist/pagingComponents.d.ts +50 -0
  44. package/dist/pagingComponents.d.ts.map +1 -0
  45. package/dist/renderCellValue.d.ts +9 -0
  46. package/dist/renderCellValue.d.ts.map +1 -0
  47. package/dist/select/MultiSelect.d.ts +27 -0
  48. package/dist/select/MultiSelect.d.ts.map +1 -0
  49. package/dist/select/Select.d.ts +27 -0
  50. package/dist/select/Select.d.ts.map +1 -0
  51. package/dist/select/SelectDataView.d.ts +46 -0
  52. package/dist/select/SelectDataView.d.ts.map +1 -0
  53. package/dist/select/SelectEachValue.d.ts +20 -0
  54. package/dist/select/SelectEachValue.d.ts.map +1 -0
  55. package/dist/select/SelectItemTrigger.d.ts +25 -0
  56. package/dist/select/SelectItemTrigger.d.ts.map +1 -0
  57. package/dist/select/SelectOption.d.ts +23 -0
  58. package/dist/select/SelectOption.d.ts.map +1 -0
  59. package/dist/select/SelectPlaceholder.d.ts +16 -0
  60. package/dist/select/SelectPlaceholder.d.ts.map +1 -0
  61. package/dist/select/index.d.ts +9 -0
  62. package/dist/select/index.d.ts.map +1 -0
  63. package/dist/select/selectContext.d.ts +20 -0
  64. package/dist/select/selectContext.d.ts.map +1 -0
  65. package/dist/selectionComponents.d.ts +62 -0
  66. package/dist/selectionComponents.d.ts.map +1 -0
  67. package/dist/sortingComponents.d.ts +39 -0
  68. package/dist/sortingComponents.d.ts.map +1 -0
  69. package/dist/stateStorage.d.ts +20 -0
  70. package/dist/stateStorage.d.ts.map +1 -0
  71. package/dist/useDataGridSetup.d.ts +53 -0
  72. package/dist/useDataGridSetup.d.ts.map +1 -0
  73. package/dist/useDataView.d.ts +57 -0
  74. package/dist/useDataView.d.ts.map +1 -0
  75. package/dist/useDataViewState.d.ts +89 -0
  76. package/dist/useDataViewState.d.ts.map +1 -0
  77. package/package.json +33 -0
  78. package/src/DataGrid.tsx +173 -0
  79. package/src/DataViewContext.tsx +56 -0
  80. package/src/DataViewKeyProvider.tsx +16 -0
  81. package/src/HasManyDataGrid.tsx +319 -0
  82. package/src/columnLeaf.ts +148 -0
  83. package/src/columnTypes.ts +160 -0
  84. package/src/columns.tsx +306 -0
  85. package/src/contextHooks.ts +157 -0
  86. package/src/createColumn.ts +128 -0
  87. package/src/createRelationColumn.tsx +203 -0
  88. package/src/dataAttribute.ts +7 -0
  89. package/src/export.tsx +219 -0
  90. package/src/filterComponents.tsx +617 -0
  91. package/src/filterContext.ts +27 -0
  92. package/src/filterHooks.ts +386 -0
  93. package/src/filterScope.tsx +89 -0
  94. package/src/filterWrappers.tsx +187 -0
  95. package/src/index.ts +356 -0
  96. package/src/infiniteLoad.tsx +188 -0
  97. package/src/loaderComponents.tsx +239 -0
  98. package/src/markers.ts +54 -0
  99. package/src/pagingComponents.tsx +149 -0
  100. package/src/select/MultiSelect.tsx +88 -0
  101. package/src/select/Select.tsx +88 -0
  102. package/src/select/SelectDataView.tsx +231 -0
  103. package/src/select/SelectEachValue.tsx +34 -0
  104. package/src/select/SelectItemTrigger.tsx +51 -0
  105. package/src/select/SelectOption.tsx +40 -0
  106. package/src/select/SelectPlaceholder.tsx +22 -0
  107. package/src/select/index.ts +17 -0
  108. package/src/select/selectContext.ts +82 -0
  109. package/src/selectionComponents.tsx +160 -0
  110. package/src/sortingComponents.tsx +104 -0
  111. package/src/stateStorage.ts +188 -0
  112. package/src/useDataGridSetup.ts +230 -0
  113. package/src/useDataView.ts +148 -0
  114. package/src/useDataViewState.ts +404 -0
@@ -0,0 +1,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
+ })
@@ -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
+ )