Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions packages/bindx-dataview/src/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import React, { ReactNode } from 'react'
import type { FieldRef, HasOneRef, HasManyRef, FilterHandler, FilterArtifact, EntityAccessor, EnumFilterArtifact, EnumListFilterArtifact, SelectionMeta } from '@contember/bindx'
import { SelectionScope } from '@contember/bindx'
import { FIELD_REF_META, createCollectorProxy } from '@contember/bindx-react'
import { createColumn, type ColumnRenderProps } from './createColumn.js'
import { createColumn, createColumnStaticRender, type ColumnRenderProps } from './createColumn.js'
import { accessField } from './columnTypes.js'
import { createRelationColumn, hasOneCellConfig, hasManyCellConfig, type RelationColumnProps } from './createRelationColumn.jsx'
import {
Expand Down Expand Up @@ -104,9 +104,19 @@ function renderDateTimeDefault({ value }: ColumnRenderProps<string | null>): Rea
return date.toLocaleString()
}

function renderEnumListDefault({ value }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
function renderEnumDefault({ value, enumOptions }: ColumnRenderProps<string | null>): React.ReactNode {
if (value == null) return ''
return enumOptions?.[value] ?? value
}

function renderEnumListDefault({ value, enumOptions }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
if (!Array.isArray(value)) return ''
return value.join(', ')
return value.map((v, i) => (
<React.Fragment key={i}>
{i > 0 ? ', ' : null}
{enumOptions?.[v] ?? v}
</React.Fragment>
))
}

// ============================================================================
Expand Down Expand Up @@ -164,13 +174,15 @@ export const DataGridBooleanColumn = createColumn(booleanColumnDef, {
renderCell: renderBooleanDefault,
})

export const DataGridEnumColumn = createColumn<string | null, EnumFilterArtifact, EnumExtraProps<string>>(enumColumnDef, {
renderCell: renderScalarDefault,
}) as <TValue extends string>(props: DataGridEnumColumnProps<TValue>) => ReactNode
export const DataGridEnumColumn = Object.assign(
<TValue extends string>(_props: DataGridEnumColumnProps<TValue>): null => null,
{ staticRender: createColumnStaticRender(enumColumnDef, { renderCell: renderEnumDefault }) },
)

export const DataGridEnumListColumn = createColumn<readonly string[] | null, EnumListFilterArtifact, EnumExtraProps<string>>(enumListColumnDef, {
renderCell: renderEnumListDefault,
}) as <TValue extends string>(props: DataGridEnumListColumnProps<TValue>) => ReactNode
export const DataGridEnumListColumn = Object.assign(
<TValue extends string>(_props: DataGridEnumListColumnProps<TValue>): null => null,
{ staticRender: createColumnStaticRender(enumListColumnDef, { renderCell: renderEnumListDefault }) },
)

export const DataGridUuidColumn = createColumn(uuidColumnDef, {
renderCell: renderScalarDefault,
Expand Down
36 changes: 26 additions & 10 deletions packages/bindx-dataview/src/createColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface ColumnRenderProps<TValue> {
readonly accessor: EntityAccessor<object>
readonly fieldRef: FieldRef<unknown> | null
readonly fieldName: string | null
readonly enumOptions: Readonly<Record<string, React.ReactNode>> | undefined
readonly enumName: string | undefined
}

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

export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TExtraProps = object>(
/**
* Build the static-render function for a column type definition. Used both by
* {@link createColumn} and by column components that need a narrower generic
* signature than `ColumnComponent` can express (e.g. enum columns constrained
* to `T extends string`) — those define their own component and attach this
* static render via `Object.assign`.
*/
export function createColumnStaticRender<TValue, TFilterArtifact extends FilterArtifact>(
columnType: ColumnTypeDef<TValue, TFilterArtifact>,
config: CreateColumnConfig<TValue, TFilterArtifact>,
): ColumnComponent<TExtraProps> {
function Column(_props: ColumnComponentProps<unknown> & TExtraProps): null {
return null
}

Column.staticRender = (props: Record<string, unknown>): React.ReactNode => {
): (props: Record<string, unknown>) => React.ReactNode {
return (props: Record<string, unknown>): React.ReactNode => {
const fieldRef = props['field'] as FieldRef<unknown> | undefined
const fieldName = fieldRef ? extractFieldName(fieldRef) : null
const header = props['header'] as React.ReactNode | undefined
const sortable = (props['sortable'] as boolean | undefined) ?? columnType.defaultSortable
const filterEnabled = (props['filter'] as boolean | undefined) ?? false
const children = props['children'] as ((value: TValue | null, accessor: EntityAccessor<object>) => React.ReactNode) | undefined
const rawOptions = props['options'] as readonly string[] | Readonly<Record<string, React.ReactNode>> | undefined
const enumOptions = Array.isArray(rawOptions)
? Object.fromEntries(rawOptions.map(v => [v, v])) as Readonly<Record<string, React.ReactNode>>
: rawOptions
const enumOptions: Readonly<Record<string, React.ReactNode>> | undefined = Array.isArray(rawOptions)
? Object.fromEntries((rawOptions as readonly string[]).map(v => [v, v]))
: rawOptions as Readonly<Record<string, React.ReactNode>> | undefined
const enumName = extractEnumName(fieldRef)

const renderCell = children
Expand All @@ -95,6 +100,8 @@ export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TEx
accessor,
fieldRef: fieldRef ?? null,
fieldName,
enumOptions,
enumName,
})
}

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

return React.createElement(ColumnLeaf, leafProps as ColumnLeafProps)
}
}

export function createColumn<TValue, TFilterArtifact extends FilterArtifact, TExtraProps = object>(
columnType: ColumnTypeDef<TValue, TFilterArtifact>,
config: CreateColumnConfig<TValue, TFilterArtifact>,
): ColumnComponent<TExtraProps> {
function Column(_props: ColumnComponentProps<unknown> & TExtraProps): null {
return null
}
Column.staticRender = createColumnStaticRender(columnType, config)
return Column as ColumnComponent<TExtraProps>
}
1 change: 1 addition & 0 deletions packages/bindx-dataview/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export {
// createColumn factory (Layer 2: UI wrapping)
export {
createColumn,
createColumnStaticRender,
type ColumnRenderProps,
type FilterRenderProps,
type CreateColumnConfig,
Expand Down
1 change: 1 addition & 0 deletions packages/bindx-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export type {
export {
// Schema utilities
scalar,
enumScalar,
hasOne,
hasMany,
defineSchema,
Expand Down
89 changes: 59 additions & 30 deletions packages/bindx-ui/src/datagrid/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, { type ReactElement, type ReactNode } from 'react'
import type { EntityAccessor, EntityDef, EnumFilterArtifact, EnumListFilterArtifact, FieldRef } from '@contember/bindx'
import {
createColumn,
createColumnStaticRender,
createRelationColumn,
hasOneCellConfig,
hasManyCellConfig,
Expand Down Expand Up @@ -99,9 +100,33 @@ function renderDateTimeDefault({ value }: ColumnRenderProps<string | null>): Rea
return date.toLocaleString()
}

function renderEnumListDefault({ value }: ColumnRenderProps<readonly string[] | null>): React.ReactNode {
function EnumCellLabel({ value, enumOptions, enumName }: {
value: string
enumOptions: Readonly<Record<string, ReactNode>> | undefined
enumName: string | undefined
}): ReactNode {
const formatter = useEnumOptionsFormatter()
if (enumOptions?.[value] != null) return enumOptions[value]
if (enumName) {
const resolved = formatter(enumName)
if (resolved[value] != null) return resolved[value]
}
return value
}

function renderEnumDefault({ value, enumOptions, enumName }: ColumnRenderProps<string | null>): ReactNode {
if (value == null) return ''
return <EnumCellLabel value={value} enumOptions={enumOptions} enumName={enumName} />
}

function renderEnumListDefault({ value, enumOptions, enumName }: ColumnRenderProps<readonly string[] | null>): ReactNode {
if (!Array.isArray(value)) return ''
return value.join(', ')
return value.map((v, i) => (
<React.Fragment key={i}>
{i > 0 ? ', ' : null}
<EnumCellLabel value={v} enumOptions={enumOptions} enumName={enumName} />
</React.Fragment>
))
}

function ColumnEnumFilterControls(): ReactElement {
Expand Down Expand Up @@ -152,41 +177,45 @@ export const DataGridBooleanColumn = createColumn(booleanColumnDef, {
renderFilter: () => <DataGridBooleanFilterControls />,
})

const _DataGridEnumColumn = createColumn(enumColumnDef, {
renderCell: renderScalarDefault,
renderFilter: () => <ColumnEnumFilterControls />,
})

type ExtractEnum<F> = F extends FieldRef<infer T> ? Exclude<T, null | undefined> & string : string

export const DataGridEnumColumn = <F extends FieldRef<any>>(props: {
field: F
header?: ReactNode
sortable?: boolean
filter?: boolean
children?: (value: ExtractEnum<F> | null, accessor: EntityAccessor<object>) => ReactNode
options?: { [K in ExtractEnum<F>]?: ReactNode }
}): ReactNode => null
DataGridEnumColumn.staticRender = _DataGridEnumColumn.staticRender

const _DataGridEnumListColumn = createColumn(enumListColumnDef, {
renderCell: renderEnumListDefault,
renderFilter: () => <ColumnEnumFilterControls />,
})
export const DataGridEnumColumn = Object.assign(
<F extends FieldRef<any>>(_props: {
field: F
header?: ReactNode
sortable?: boolean
filter?: boolean
children?: (value: ExtractEnum<F> | null, accessor: EntityAccessor<object>) => ReactNode
options?: { [K in ExtractEnum<F>]?: ReactNode }
}): ReactNode => null,
{
staticRender: createColumnStaticRender(enumColumnDef, {
renderCell: renderEnumDefault,
renderFilter: () => <ColumnEnumFilterControls />,
}),
},
)

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

export const DataGridEnumListColumn = <F extends FieldRef<any>>(props: {
field: F
header?: ReactNode
sortable?: boolean
filter?: boolean
children?: (value: ExtractEnumList<F>[] | null, accessor: EntityAccessor<object>) => ReactNode
options?: { [K in ExtractEnumList<F>]?: ReactNode }
}): ReactNode => null
DataGridEnumListColumn.staticRender = _DataGridEnumListColumn.staticRender
export const DataGridEnumListColumn = Object.assign(
<F extends FieldRef<any>>(_props: {
field: F
header?: ReactNode
sortable?: boolean
filter?: boolean
children?: (value: ExtractEnumList<F>[] | null, accessor: EntityAccessor<object>) => ReactNode
options?: { [K in ExtractEnumList<F>]?: ReactNode }
}): ReactNode => null,
{
staticRender: createColumnStaticRender(enumListColumnDef, {
renderCell: renderEnumListDefault,
renderFilter: () => <ColumnEnumFilterControls />,
}),
},
)

export const DataGridUuidColumn = createColumn(uuidColumnDef, {
renderCell: renderScalarDefault,
Expand Down
2 changes: 1 addition & 1 deletion packages/bindx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export type {
// ============================================================================

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

// Role types
export type {
Expand Down
Loading