Skip to content

Commit e8ed9b1

Browse files
authored
Merge pull request #24 from contember/fix/datagrid-enum-column-cell-labels
fix: render enum column cell labels from options prop
2 parents 7bd0d63 + de05907 commit e8ed9b1

9 files changed

Lines changed: 527 additions & 52 deletions

File tree

packages/bindx-dataview/src/columns.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import React, { ReactNode } from 'react'
1313
import type { FieldRef, HasOneRef, HasManyRef, FilterHandler, FilterArtifact, EntityAccessor, EnumFilterArtifact, EnumListFilterArtifact, SelectionMeta } from '@contember/bindx'
1414
import { SelectionScope } from '@contember/bindx'
1515
import { FIELD_REF_META, createCollectorProxy } from '@contember/bindx-react'
16-
import { createColumn, type ColumnRenderProps } from './createColumn.js'
16+
import { createColumn, createColumnStaticRender, type ColumnRenderProps } from './createColumn.js'
1717
import { accessField } from './columnTypes.js'
1818
import { createRelationColumn, hasOneCellConfig, hasManyCellConfig, type RelationColumnProps } from './createRelationColumn.jsx'
1919
import {
@@ -104,9 +104,19 @@ function renderDateTimeDefault({ value }: ColumnRenderProps<string | null>): Rea
104104
return date.toLocaleString()
105105
}
106106

107-
function renderEnumListDefault({ value }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
107+
function renderEnumDefault({ value, enumOptions }: ColumnRenderProps<string | null>): React.ReactNode {
108+
if (value == null) return ''
109+
return enumOptions?.[value] ?? value
110+
}
111+
112+
function renderEnumListDefault({ value, enumOptions }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
108113
if (!Array.isArray(value)) return ''
109-
return value.join(', ')
114+
return value.map((v, i) => (
115+
<React.Fragment key={i}>
116+
{i > 0 ? ', ' : null}
117+
{enumOptions?.[v] ?? v}
118+
</React.Fragment>
119+
))
110120
}
111121

112122
// ============================================================================
@@ -164,13 +174,15 @@ export const DataGridBooleanColumn = createColumn(booleanColumnDef, {
164174
renderCell: renderBooleanDefault,
165175
})
166176

167-
export const DataGridEnumColumn = createColumn<string | null, EnumFilterArtifact, EnumExtraProps<string>>(enumColumnDef, {
168-
renderCell: renderScalarDefault,
169-
}) as <TValue extends string>(props: DataGridEnumColumnProps<TValue>) => ReactNode
177+
export const DataGridEnumColumn = Object.assign(
178+
<TValue extends string>(_props: DataGridEnumColumnProps<TValue>): null => null,
179+
{ staticRender: createColumnStaticRender(enumColumnDef, { renderCell: renderEnumDefault }) },
180+
)
170181

171-
export const DataGridEnumListColumn = createColumn<readonly string[] | null, EnumListFilterArtifact, EnumExtraProps<string>>(enumListColumnDef, {
172-
renderCell: renderEnumListDefault,
173-
}) as <TValue extends string>(props: DataGridEnumListColumnProps<TValue>) => ReactNode
182+
export const DataGridEnumListColumn = Object.assign(
183+
<TValue extends string>(_props: DataGridEnumListColumnProps<TValue>): null => null,
184+
{ staticRender: createColumnStaticRender(enumListColumnDef, { renderCell: renderEnumListDefault }) },
185+
)
174186

175187
export const DataGridUuidColumn = createColumn(uuidColumnDef, {
176188
renderCell: renderScalarDefault,

packages/bindx-dataview/src/createColumn.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface ColumnRenderProps<TValue> {
2121
readonly accessor: EntityAccessor<object>
2222
readonly fieldRef: FieldRef<unknown> | null
2323
readonly fieldName: string | null
24+
readonly enumOptions: Readonly<Record<string, React.ReactNode>> | undefined
25+
readonly enumName: string | undefined
2426
}
2527

2628
export interface FilterRenderProps<TFilterArtifact> {
@@ -58,25 +60,28 @@ export interface ColumnComponent<TExtraProps = object> {
5860
staticRender: (props: Record<string, unknown>) => React.ReactNode
5961
}
6062

61-
export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TExtraProps = object>(
63+
/**
64+
* Build the static-render function for a column type definition. Used both by
65+
* {@link createColumn} and by column components that need a narrower generic
66+
* signature than `ColumnComponent` can express (e.g. enum columns constrained
67+
* to `T extends string`) — those define their own component and attach this
68+
* static render via `Object.assign`.
69+
*/
70+
export function createColumnStaticRender<TValue, TFilterArtifact extends FilterArtifact>(
6271
columnType: ColumnTypeDef<TValue, TFilterArtifact>,
6372
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 => {
73+
): (props: Record<string, unknown>) => React.ReactNode {
74+
return (props: Record<string, unknown>): React.ReactNode => {
7075
const fieldRef = props['field'] as FieldRef<unknown> | undefined
7176
const fieldName = fieldRef ? extractFieldName(fieldRef) : null
7277
const header = props['header'] as React.ReactNode | undefined
7378
const sortable = (props['sortable'] as boolean | undefined) ?? columnType.defaultSortable
7479
const filterEnabled = (props['filter'] as boolean | undefined) ?? false
7580
const children = props['children'] as ((value: TValue | null, accessor: EntityAccessor<object>) => React.ReactNode) | undefined
7681
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
82+
const enumOptions: Readonly<Record<string, React.ReactNode>> | undefined = Array.isArray(rawOptions)
83+
? Object.fromEntries((rawOptions as readonly string[]).map(v => [v, v]))
84+
: rawOptions as Readonly<Record<string, React.ReactNode>> | undefined
8085
const enumName = extractEnumName(fieldRef)
8186

8287
const renderCell = children
@@ -95,6 +100,8 @@ export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TEx
95100
accessor,
96101
fieldRef: fieldRef ?? null,
97102
fieldName,
103+
enumOptions,
104+
enumName,
98105
})
99106
}
100107

@@ -123,6 +130,15 @@ export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TEx
123130

124131
return React.createElement(ColumnLeaf, leafProps as ColumnLeafProps)
125132
}
133+
}
126134

135+
export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TExtraProps = object>(
136+
columnType: ColumnTypeDef<TValue, TFilterArtifact>,
137+
config: CreateColumnConfig<TValue, TFilterArtifact>,
138+
): ColumnComponent<TExtraProps> {
139+
function Column(_props: ColumnComponentProps<unknown> & TExtraProps): null {
140+
return null
141+
}
142+
Column.staticRender = createColumnStaticRender(columnType, config)
127143
return Column as ColumnComponent<TExtraProps>
128144
}

packages/bindx-dataview/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
// createColumn factory (Layer 2: UI wrapping)
4141
export {
4242
createColumn,
43+
createColumnStaticRender,
4344
type ColumnRenderProps,
4445
type FilterRenderProps,
4546
type CreateColumnConfig,

packages/bindx-react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export type {
140140
export {
141141
// Schema utilities
142142
scalar,
143+
enumScalar,
143144
hasOne,
144145
hasMany,
145146
defineSchema,

packages/bindx-ui/src/datagrid/columns.tsx

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import React, { type ReactElement, type ReactNode } from 'react'
1212
import type { EntityAccessor, EntityDef, EnumFilterArtifact, EnumListFilterArtifact, FieldRef } from '@contember/bindx'
1313
import {
1414
createColumn,
15+
createColumnStaticRender,
1516
createRelationColumn,
1617
hasOneCellConfig,
1718
hasManyCellConfig,
@@ -99,9 +100,33 @@ function renderDateTimeDefault({ value }: ColumnRenderProps<string | null>): Rea
99100
return date.toLocaleString()
100101
}
101102

102-
function renderEnumListDefault({ value }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
103+
function EnumCellLabel({ value, enumOptions, enumName }: {
104+
value: string
105+
enumOptions: Readonly<Record<string, ReactNode>> | undefined
106+
enumName: string | undefined
107+
}): ReactNode {
108+
const formatter = useEnumOptionsFormatter()
109+
if (enumOptions?.[value] != null) return enumOptions[value]
110+
if (enumName) {
111+
const resolved = formatter(enumName)
112+
if (resolved[value] != null) return resolved[value]
113+
}
114+
return value
115+
}
116+
117+
function renderEnumDefault({ value, enumOptions, enumName }: ColumnRenderProps<string | null>): ReactNode {
118+
if (value == null) return ''
119+
return <EnumCellLabel value={value} enumOptions={enumOptions} enumName={enumName} />
120+
}
121+
122+
function renderEnumListDefault({ value, enumOptions, enumName }: ColumnRenderProps<readonly string[] | null>): ReactNode {
103123
if (!Array.isArray(value)) return ''
104-
return value.join(', ')
124+
return value.map((v, i) => (
125+
<React.Fragment key={i}>
126+
{i > 0 ? ', ' : null}
127+
<EnumCellLabel value={v} enumOptions={enumOptions} enumName={enumName} />
128+
</React.Fragment>
129+
))
105130
}
106131

107132
function ColumnEnumFilterControls(): ReactElement {
@@ -152,41 +177,45 @@ export const DataGridBooleanColumn = createColumn(booleanColumnDef, {
152177
renderFilter: () => <DataGridBooleanFilterControls />,
153178
})
154179

155-
const _DataGridEnumColumn = createColumn(enumColumnDef, {
156-
renderCell: renderScalarDefault,
157-
renderFilter: () => <ColumnEnumFilterControls />,
158-
})
159-
160180
type ExtractEnum<F> = F extends FieldRef<infer T> ? Exclude<T, null | undefined> & string : string
161181

162-
export const DataGridEnumColumn = <F extends FieldRef<any>>(props: {
163-
field: F
164-
header?: ReactNode
165-
sortable?: boolean
166-
filter?: boolean
167-
children?: (value: ExtractEnum<F> | null, accessor: EntityAccessor<object>) => ReactNode
168-
options?: { [K in ExtractEnum<F>]?: ReactNode }
169-
}): ReactNode => null
170-
DataGridEnumColumn.staticRender = _DataGridEnumColumn.staticRender
171-
172-
const _DataGridEnumListColumn = createColumn(enumListColumnDef, {
173-
renderCell: renderEnumListDefault,
174-
renderFilter: () => <ColumnEnumFilterControls />,
175-
})
182+
export const DataGridEnumColumn = Object.assign(
183+
<F extends FieldRef<any>>(_props: {
184+
field: F
185+
header?: ReactNode
186+
sortable?: boolean
187+
filter?: boolean
188+
children?: (value: ExtractEnum<F> | null, accessor: EntityAccessor<object>) => ReactNode
189+
options?: { [K in ExtractEnum<F>]?: ReactNode }
190+
}): ReactNode => null,
191+
{
192+
staticRender: createColumnStaticRender(enumColumnDef, {
193+
renderCell: renderEnumDefault,
194+
renderFilter: () => <ColumnEnumFilterControls />,
195+
}),
196+
},
197+
)
176198

177199
type ExtractEnumList<F> = F extends FieldRef<infer T>
178200
? T extends readonly (infer U)[] | null ? U & string : string
179201
: string
180202

181-
export const DataGridEnumListColumn = <F extends FieldRef<any>>(props: {
182-
field: F
183-
header?: ReactNode
184-
sortable?: boolean
185-
filter?: boolean
186-
children?: (value: ExtractEnumList<F>[] | null, accessor: EntityAccessor<object>) => ReactNode
187-
options?: { [K in ExtractEnumList<F>]?: ReactNode }
188-
}): ReactNode => null
189-
DataGridEnumListColumn.staticRender = _DataGridEnumListColumn.staticRender
203+
export const DataGridEnumListColumn = Object.assign(
204+
<F extends FieldRef<any>>(_props: {
205+
field: F
206+
header?: ReactNode
207+
sortable?: boolean
208+
filter?: boolean
209+
children?: (value: ExtractEnumList<F>[] | null, accessor: EntityAccessor<object>) => ReactNode
210+
options?: { [K in ExtractEnumList<F>]?: ReactNode }
211+
}): ReactNode => null,
212+
{
213+
staticRender: createColumnStaticRender(enumListColumnDef, {
214+
renderCell: renderEnumListDefault,
215+
renderFilter: () => <ColumnEnumFilterControls />,
216+
}),
217+
},
218+
)
190219

191220
export const DataGridUuidColumn = createColumn(uuidColumnDef, {
192221
renderCell: renderScalarDefault,

packages/bindx/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export type {
126126
// ============================================================================
127127

128128
// Schema utilities
129-
export { scalar, hasOne, hasMany, defineSchema, entityDef, roleEntityDef, SchemaRegistry, ContemberSchema, SchemaLoader } from './schema/index.js'
129+
export { scalar, enumScalar, hasOne, hasMany, defineSchema, entityDef, roleEntityDef, SchemaRegistry, ContemberSchema, SchemaLoader } from './schema/index.js'
130130

131131
// Role types
132132
export type {

0 commit comments

Comments
 (0)