@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,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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Converts a boolean to a data attribute value.
3
+ * Returns '' (present) for true, undefined (absent) for false.
4
+ */
5
+ export function dataAttribute(value: boolean): '' | undefined {
6
+ return value ? '' : undefined
7
+ }