diff --git a/packages/bindx-dataview/src/columnLeaf.ts b/packages/bindx-dataview/src/columnLeaf.ts index f454396f..0f9894b5 100644 --- a/packages/bindx-dataview/src/columnLeaf.ts +++ b/packages/bindx-dataview/src/columnLeaf.ts @@ -35,6 +35,11 @@ export interface ColumnLeafProps { readonly sortingField: string | null readonly filterName: string | null readonly filterHandler: FilterHandler | undefined + /** + * Where the filter starts when nothing is stored for it. Defaults to the + * handler's (inactive) default artifact. + */ + readonly filterInitialArtifact?: FilterArtifact readonly isTextSearchable: boolean /** * The leaf exists only to register its field — for full-text search, or to diff --git a/packages/bindx-dataview/src/columns.tsx b/packages/bindx-dataview/src/columns.tsx index f198d1e2..31da7f79 100644 --- a/packages/bindx-dataview/src/columns.tsx +++ b/packages/bindx-dataview/src/columns.tsx @@ -99,9 +99,15 @@ interface EnumExtraProps { options: readonly T[] | Record } -export interface DataGridEnumColumnProps extends DataGridScalarColumnPropsBase, EnumExtraProps> {} +export interface DataGridEnumColumnProps extends DataGridScalarColumnPropsBase, EnumExtraProps> { + /** Where the column's filter starts when nothing is stored for it. Defaults to no value picked. */ + filterInitialArtifact?: EnumFilterArtifact> +} -export interface DataGridEnumListColumnProps extends DataGridScalarColumnPropsBase, EnumExtraProps> {} +export interface DataGridEnumListColumnProps extends DataGridScalarColumnPropsBase, EnumExtraProps> { + /** Where the column's filter starts when nothing is stored for it. Defaults to no value picked. */ + filterInitialArtifact?: EnumListFilterArtifact> +} // ============================================================================ // Scalar Columns via createColumn() @@ -233,6 +239,8 @@ export interface DataGridColumnProps { sortable?: boolean filter?: boolean filterHandler?: FilterHandler + /** Where the column's filter starts when nothing is stored for it. Defaults to the handler's inactive artifact. */ + filterInitialArtifact?: FilterArtifact /** Register the field without showing a column (e.g. to make it full-text searchable). */ virtual?: boolean children?: (value: T | null, accessor: EntityAccessor) => React.ReactNode @@ -262,6 +270,7 @@ export const DataGridColumn = Object.assign( filterHandler: filterEnabled && fieldName ? (customHandler ?? textColumnDef.createFilterHandler(fieldName) as FilterHandler) : undefined, + filterInitialArtifact: props['filterInitialArtifact'] as FilterArtifact | undefined, isTextSearchable: false, header, renderCell: (accessor: EntityAccessor) => { diff --git a/packages/bindx-dataview/src/createColumn.ts b/packages/bindx-dataview/src/createColumn.ts index fc834491..91983d39 100644 --- a/packages/bindx-dataview/src/createColumn.ts +++ b/packages/bindx-dataview/src/createColumn.ts @@ -43,13 +43,15 @@ export interface CreateColumnConfig { +export interface ColumnComponentProps { field: FieldRef /** Column identity. Defaults to the bound field name — pass it when two columns bind one field. */ name?: string header?: React.ReactNode sortable?: boolean filter?: boolean + /** Where the column's filter starts when nothing is stored for it. Defaults to the handler's inactive artifact. */ + filterInitialArtifact?: TFilterArtifact /** Register the field without showing a column (e.g. to make it full-text searchable). */ virtual?: boolean children?: (value: TValue | null, accessor: EntityAccessor) => React.ReactNode @@ -59,8 +61,12 @@ export interface ColumnComponentProps { // Factory // ============================================================================ -export interface ColumnComponent { - (props: ColumnComponentProps & TExtraProps): null +/** + * `TFilterArtifact` defaults to `never`: a column whose artifact type is not known takes no + * `filterInitialArtifact`, and every column `createColumn` builds is assignable to it. + */ +export interface ColumnComponent { + (props: ColumnComponentProps & TExtraProps): null staticRender: (props: Record) => React.ReactNode } @@ -119,6 +125,7 @@ export function createColumnStaticRender : undefined, + filterInitialArtifact: props['filterInitialArtifact'] as FilterArtifact | undefined, isTextSearchable: columnType.isTextSearchable, columnType: columnType.name as ColumnLeafProps['columnType'], enumName, @@ -140,10 +147,10 @@ export function createColumnStaticRender( columnType: ColumnTypeDef, config: CreateColumnConfig, -): ColumnComponent { - function Column(_props: ColumnComponentProps & TExtraProps): null { +): ColumnComponent { + function Column(_props: ColumnComponentProps & TExtraProps): null { return null } Column.staticRender = createColumnStaticRender(columnType, config) - return Column as ColumnComponent + return Column as ColumnComponent } diff --git a/packages/bindx-dataview/src/createRelationColumn.tsx b/packages/bindx-dataview/src/createRelationColumn.tsx index 0c03b6a4..bacb80b5 100644 --- a/packages/bindx-dataview/src/createRelationColumn.tsx +++ b/packages/bindx-dataview/src/createRelationColumn.tsx @@ -11,7 +11,7 @@ */ import React from 'react' -import type { FieldRef, FilterArtifact, FilterHandler, EntityAccessor, SelectionMeta } from '@contember/bindx' +import type { FieldRef, FilterArtifact, FilterHandler, EntityAccessor, RelationFilterArtifact, SelectionMeta } from '@contember/bindx' import { SelectionScope } from '@contember/bindx' import { createCollectorProxy, collectSelection as collectJsxSelection, SCOPE_REF } from '@contember/bindx-react' import type { ColumnTypeDef } from './columnTypes.js' @@ -64,6 +64,8 @@ export interface RelationColumnProps { name?: string header?: React.ReactNode filter?: boolean + /** Where the column's filter starts when nothing is stored for it. Defaults to no related row picked. */ + filterInitialArtifact?: RelationFilterArtifact /** Register the relation without showing a column. */ virtual?: boolean renderCellWrapper?: (content: React.ReactNode, item: EntityAccessor) => React.ReactNode @@ -162,6 +164,7 @@ export function createRelationColumn( filterHandler: filterName ? columnType.createFilterHandler(fieldName!) as FilterHandler : undefined, + filterInitialArtifact: props['filterInitialArtifact'] as FilterArtifact | undefined, isTextSearchable: false, columnType: columnType.name as ColumnLeafProps['columnType'], relatedEntityName, diff --git a/packages/bindx-dataview/src/useDataGridSetup.ts b/packages/bindx-dataview/src/useDataGridSetup.ts index e956003b..dc5da1ba 100644 --- a/packages/bindx-dataview/src/useDataGridSetup.ts +++ b/packages/bindx-dataview/src/useDataGridSetup.ts @@ -199,12 +199,12 @@ export function useDataGridSetup({ }, [entityType, schemaRegistry, children, layouts]) // ---- Phase 2: State management ---- - const filterDefs = useMemo((): ReadonlyMap }> => { - const map = new Map }>() + const filterDefs = useMemo((): ReadonlyMap; initialArtifact?: FilterArtifact }> => { + const map = new Map; initialArtifact?: FilterArtifact }>() const textFieldPaths: string[] = [] for (const col of columns) { if (col.filterName && col.filterHandler) { - map.set(col.filterName, { handler: col.filterHandler }) + map.set(col.filterName, { handler: col.filterHandler, initialArtifact: col.filterInitialArtifact }) } if (col.isTextSearchable && col.fieldName) { textFieldPaths.push(col.fieldName) diff --git a/packages/bindx-dataview/src/useDataViewState.ts b/packages/bindx-dataview/src/useDataViewState.ts index 10a0b16d..8d5da907 100644 --- a/packages/bindx-dataview/src/useDataViewState.ts +++ b/packages/bindx-dataview/src/useDataViewState.ts @@ -40,12 +40,18 @@ export type FilterArtifactUpdate = export interface FilteringState { readonly filters: ReadonlyMap + /** The filter's current artifact: the stored one, else its initial artifact. `undefined` only for an unregistered filter. */ getArtifact(name: string): FilterArtifact | undefined /** An updater sees the live artifact, not a render snapshot; returning `undefined` resets the filter to its default. */ setArtifact(name: string, artifact: FilterArtifactUpdate): void - /** Replaces the whole artifact record at once — for restoring a saved filter preset. */ + /** + * Replaces the whole artifact record at once — for restoring a saved filter preset. + * A registered filter the record does not name reads as its initial artifact. + */ setAllArtifacts(artifacts: Record): void + /** Clears one filter: sets it to its handler's default (inactive) artifact. */ resetFilter(name: string): void + /** Puts every filter back where it starts: its initial artifact, else the handler default. */ resetAll(): void readonly hasActiveFilters: boolean readonly resolvedWhere: Record | undefined @@ -62,7 +68,7 @@ export interface UseFilteringOptions { export function useFilteringState(options: UseFilteringOptions): FilteringState { const { filters: filterDefs, stateStorage = 'null', storageKey = 'dataview' } = options - const defaultArtifacts = useMemo((): Record => { + const initialArtifacts = useMemo((): Record => { const result: Record = {} for (const [name, def] of filterDefs) { result[name] = def.initialArtifact ?? def.handler.defaultArtifact() @@ -70,10 +76,18 @@ export function useFilteringState(options: UseFilteringOptions): FilteringState return result }, [filterDefs]) - const [artifacts, setArtifacts] = useStoredState>( + const [storedArtifacts, setArtifacts] = useStoredState>( stateStorage, [storageKey, 'filters'], - (stored) => stored ?? defaultArtifacts, + (stored) => stored ?? initialArtifacts, + ) + + // A registered filter the record does not name (a record stored before the filter + // existed, or a restored preset that omits it) is where it would start with nothing + // stored. Every view below reads this one record, so the UI and the query agree. + const artifacts = useMemo( + (): Record => ({ ...initialArtifacts, ...storedArtifacts }), + [initialArtifacts, storedArtifacts], ) const getArtifact = useCallback( @@ -85,12 +99,12 @@ export function useFilteringState(options: UseFilteringOptions): FilteringState const setArtifact = useCallback( (name: string, artifact: FilterArtifactUpdate): void => { setArtifacts(current => { - const next = typeof artifact === 'function' ? artifact(current[name]) : artifact + const next = typeof artifact === 'function' ? artifact(current[name] ?? initialArtifacts[name]) : artifact const resolved = next ?? filterDefs.get(name)?.handler.defaultArtifact() return resolved === undefined ? current : { ...current, [name]: resolved } }) }, - [filterDefs, setArtifacts], + [filterDefs, initialArtifacts, setArtifacts], ) const resetFilter = useCallback( @@ -103,8 +117,8 @@ export function useFilteringState(options: UseFilteringOptions): FilteringState ) const resetAll = useCallback((): void => { - setArtifacts(defaultArtifacts) - }, [defaultArtifacts, setArtifacts]) + setArtifacts(initialArtifacts) + }, [initialArtifacts, setArtifacts]) const hasActiveFilters = useMemo((): boolean => { for (const [name, def] of filterDefs) { @@ -135,6 +149,7 @@ export function useFilteringState(options: UseFilteringOptions): FilteringState map.set(name, { name, handler: def.handler, + // Stored JSON is not validated, so a `null` entry falls back to the handler default. artifact: artifacts[name] ?? def.handler.defaultArtifact(), }) } diff --git a/packages/bindx-ui/src/datagrid/columns/enum-column.tsx b/packages/bindx-ui/src/datagrid/columns/enum-column.tsx index d4457477..91b1bc38 100644 --- a/packages/bindx-ui/src/datagrid/columns/enum-column.tsx +++ b/packages/bindx-ui/src/datagrid/columns/enum-column.tsx @@ -1,5 +1,5 @@ import React, { type ReactElement, type ReactNode } from 'react' -import type { EntityAccessor, FieldRef } from '@contember/bindx' +import type { EntityAccessor, EnumFilterArtifact, EnumListFilterArtifact, FieldRef } from '@contember/bindx' import { createColumn, createColumnStaticRender, @@ -67,6 +67,8 @@ export const DataGridEnumColumn = Object.assign( header?: ReactNode sortable?: boolean filter?: boolean + /** Where the column's filter starts when nothing is stored for it. Defaults to no value picked. */ + filterInitialArtifact?: EnumFilterArtifact> children?: (value: ExtractEnum | null, accessor: EntityAccessor) => ReactNode options?: { [K in ExtractEnum]?: ReactNode } }): ReactNode => null, @@ -88,6 +90,8 @@ export const DataGridEnumListColumn = Object.assign( header?: ReactNode sortable?: boolean filter?: boolean + /** Where the column's filter starts when nothing is stored for it. Defaults to no value picked. */ + filterInitialArtifact?: EnumListFilterArtifact> children?: (value: ExtractEnumList[] | null, accessor: EntityAccessor) => ReactNode options?: { [K in ExtractEnumList]?: ReactNode } }): ReactNode => null, diff --git a/packages/bindx/src/dataview/types.ts b/packages/bindx/src/dataview/types.ts index 7eacc42d..ec7305b6 100644 --- a/packages/bindx/src/dataview/types.ts +++ b/packages/bindx/src/dataview/types.ts @@ -60,9 +60,9 @@ export interface BooleanFilterArtifact { /** * Enum filter artifact */ -export interface EnumFilterArtifact { - readonly values?: readonly string[] - readonly notValues?: readonly string[] +export interface EnumFilterArtifact { + readonly values?: readonly TValue[] + readonly notValues?: readonly TValue[] readonly nullCondition?: boolean } @@ -78,9 +78,9 @@ export interface RelationFilterArtifact { /** * Enum list filter artifact - for array enum fields (uses 'includes' condition) */ -export interface EnumListFilterArtifact { - readonly values?: readonly string[] - readonly notValues?: readonly string[] +export interface EnumListFilterArtifact { + readonly values?: readonly TValue[] + readonly notValues?: readonly TValue[] readonly nullCondition?: boolean } diff --git a/tests/react/dataview/dataViewStateBatching.test.tsx b/tests/react/dataview/dataViewStateBatching.test.tsx index a1546dc8..e20ae81a 100644 --- a/tests/react/dataview/dataViewStateBatching.test.tsx +++ b/tests/react/dataview/dataViewStateBatching.test.tsx @@ -257,7 +257,7 @@ describe('bulk state setters', () => { }) expect(result.current.hasActiveFilters).toBe(true) - // A saved preset arrives as a whole record; a filter it omits must end up unset. + // A saved preset arrives as a whole record; a filter it omits drops its previous value and reads where it starts. act(() => { result.current.setAllArtifacts({ title: { mode: 'startsWith', query: 'beta' } satisfies TextFilterArtifact, @@ -265,7 +265,7 @@ describe('bulk state setters', () => { }) expect(result.current.getArtifact('title')).toEqual({ mode: 'startsWith', query: 'beta' }) - expect(result.current.getArtifact('status')).toBeUndefined() + expect(result.current.getArtifact('status')).toEqual({}) expect(result.current.resolvedWhere).toEqual({ title: { startsWithCI: 'beta' } }) }) }) diff --git a/tests/react/dataview/filterMissingFromStoredState.test.tsx b/tests/react/dataview/filterMissingFromStoredState.test.tsx new file mode 100644 index 00000000..95bfd4eb --- /dev/null +++ b/tests/react/dataview/filterMissingFromStoredState.test.tsx @@ -0,0 +1,253 @@ +// Regression test for https://github.com/contember/bindx/issues/125: a +// persisted filter record that predates a registered filter (it was written +// before the filter existed) leaves that filter out of `resolvedWhere`, while +// `filters` reports an artifact for it. The filter's `initialArtifact` is not +// applied either, so a filter meant to start constrained starts unconstrained. +import '../../setup' +import { describe, test, expect, afterEach } from 'bun:test' +import { renderHook, render, cleanup, act, waitFor } from '@testing-library/react' +import React, { type ReactElement, type ReactNode } from 'react' +import { BindxProvider, MockAdapter, defineSchema, hasOne, scalar } from '@contember/bindx-react' +import { createEnumFilterHandler, createTextFilterHandler, entityDef } from '@contember/bindx' +import type { + EntityAccessor, + EnumFilterArtifact, + FilterArtifact, + FilterHandler, + RelationFilterArtifact, + TextFilterArtifact, +} from '@contember/bindx' +import { + DataGrid, + DataGridEnumColumn, + DataGridHasOneColumn, + DataGridTextColumn, + useDataViewContext, + useFilteringState, + type FilteringState, + type StateStorage, +} from '@contember/bindx-dataview' +import { TestTable, getRowCount, queryByTestId } from './helpers.js' + +afterEach(() => { + cleanup() +}) + +/** A backend holding a record saved while the grid had only the `title` filter. */ +function storageWithRecordWithoutStatus(): StateStorage { + return { + get: (key: string): T | undefined => + key === 'grid:filters' ? ({ title: { mode: 'contains', query: '' } } as T) : undefined, + set: (): void => {}, + remove: (): void => {}, + } +} + +const publishedOnly: EnumFilterArtifact = { values: ['published'] } + +const filterDefs = new Map; initialArtifact?: FilterArtifact }>([ + ['title', { handler: createTextFilterHandler('title') }], + ['status', { handler: createEnumFilterHandler('status'), initialArtifact: publishedOnly }], +]) + +describe('useFilteringState — stored record that predates a filter', () => { + test('should start the missing filter at its initial artifact', () => { + const { result } = renderHook(() => useFilteringState({ + filters: filterDefs, + stateStorage: storageWithRecordWithoutStatus(), + storageKey: 'grid', + })) + + expect(result.current.getArtifact('status')).toEqual(publishedOnly) + expect(result.current.resolvedWhere).toEqual({ status: { in: ['published'] } }) + expect(result.current.hasActiveFilters).toBe(true) + }) + + test('should report the same artifact through filters, getArtifact and resolvedWhere', () => { + const { result } = renderHook(() => useFilteringState({ + filters: filterDefs, + stateStorage: storageWithRecordWithoutStatus(), + storageKey: 'grid', + })) + + const shown = result.current.filters.get('status')?.artifact + const handler = filterDefs.get('status')?.handler + expect(result.current.getArtifact('status')).toEqual(shown) + expect(result.current.resolvedWhere).toEqual(shown === undefined ? undefined : handler?.toWhere(shown)) + }) +}) + +describe('useFilteringState — a record that omits a registered filter', () => { + test('should read a filter omitted by setAllArtifacts at its initial artifact everywhere', () => { + const { result } = renderHook(() => useFilteringState({ filters: filterDefs })) + + act(() => result.current.setAllArtifacts({ title: { mode: 'contains', query: 'x' } })) + + expect(result.current.getArtifact('status')).toEqual(publishedOnly) + expect(result.current.filters.get('status')?.artifact).toEqual(publishedOnly) + expect(result.current.resolvedWhere).toEqual({ + and: [{ title: { containsCI: 'x' } }, { status: { in: ['published'] } }], + }) + }) + + test('should hand an updater the initial artifact of a filter the record omits', () => { + const { result } = renderHook(() => useFilteringState({ + filters: filterDefs, + stateStorage: storageWithRecordWithoutStatus(), + storageKey: 'grid', + })) + let seen: FilterArtifact | undefined + + act(() => result.current.setArtifact('status', current => { + seen = current + return current + })) + + expect(seen).toEqual(publishedOnly) + }) + + test('should clear one filter to the handler default and reset all to the initial artifacts', () => { + const { result } = renderHook(() => useFilteringState({ filters: filterDefs })) + + act(() => result.current.resetFilter('status')) + expect(result.current.getArtifact('status')).toEqual({}) + expect(result.current.resolvedWhere).toBeUndefined() + + act(() => result.current.resetAll()) + expect(result.current.getArtifact('status')).toEqual(publishedOnly) + }) +}) + +// ============================================================================ +// DataGrid: a column declares where its filter starts +// ============================================================================ + +interface Author { + id: string + name: string +} + +interface Article { + id: string + title: string + status: string + author: Author | null +} + +const gridSchema = defineSchema<{ Article: Article; Author: Author }>({ + entities: { + Article: { + fields: { id: scalar(), title: scalar(), status: scalar(), author: hasOne('Author', { nullable: true }) }, + }, + Author: { + fields: { id: scalar(), name: scalar() }, + }, + }, +}) + +const ArticleDef = entityDef
('Article') + +function createArticles(): Record>> { + return { + Article: { + a1: { id: 'a1', title: 'Alpha', status: 'published', author: { id: 'au1', name: 'John' } }, + a2: { id: 'a2', title: 'Beta', status: 'draft', author: { id: 'au2', name: 'Jane' } }, + a3: { id: 'a3', title: 'Gamma', status: 'published', author: { id: 'au1', name: 'John' } }, + }, + } +} + +function FilteringProbe({ onFiltering }: { onFiltering: (filtering: FilteringState) => void }): null { + onFiltering(useDataViewContext().filtering) + return null +} + +type ArticleColumns = (it: EntityAccessor
) => ReactNode + +const statusColumnStartingPublished: ArticleColumns = it => ( + <> + + + +) + +function renderGrid( + columns: ArticleColumns, + stateStorage?: StateStorage, +): { container: HTMLElement; filtering: () => FilteringState } { + let latest: FilteringState | undefined + const adapter = new MockAdapter(createArticles(), { delay: 0 }) + const grid = (): ReactElement => ( + + + {it => ( + <> + {columns(it)} + { latest = filtering }} /> + + + )} + + + ) + const { container } = render(grid()) + return { + container, + filtering: () => { + if (!latest) throw new Error('DataGrid has not rendered its context yet') + return latest + }, + } +} + +describe('DataGrid — a column with an initial filter artifact', () => { + test('should start the grid filtered by the column initial artifact', async () => { + const { container, filtering } = renderGrid(statusColumnStartingPublished) + + await waitFor(() => { + expect(queryByTestId(container, 'datagrid-loading')).toBeNull() + expect(getRowCount(container)).toBe(2) + }) + expect(filtering().getArtifact('status')).toEqual(publishedOnly) + expect(filtering().hasActiveFilters).toBe(true) + }) + + test('should apply the initial artifact when the stored record predates the filter', async () => { + const { container, filtering } = renderGrid(statusColumnStartingPublished, storageWithRecordWithoutStatus()) + + await waitFor(() => { + expect(queryByTestId(container, 'datagrid-loading')).toBeNull() + expect(getRowCount(container)).toBe(2) + }) + expect(filtering().filters.get('status')?.artifact).toEqual(publishedOnly) + }) + + test('should pass the initial artifact of a createColumn column to its filter', async () => { + const alphaOnly: TextFilterArtifact = { mode: 'contains', query: 'Alpha' } + const { container, filtering } = renderGrid(it => ( + + )) + + await waitFor(() => { + expect(queryByTestId(container, 'datagrid-loading')).toBeNull() + expect(getRowCount(container)).toBe(1) + }) + expect(filtering().getArtifact('title')).toEqual(alphaOnly) + }) + + test('should pass the initial artifact of a relation column to its filter', async () => { + const janeOnly: RelationFilterArtifact = { id: ['au2'] } + const { container, filtering } = renderGrid(it => ( + + {author => author.name.value} + + )) + + // MockAdapter does not evaluate conditions on a relation, so this asserts the query, not the rows. + await waitFor(() => { + expect(queryByTestId(container, 'datagrid-loading')).toBeNull() + }) + expect(filtering().getArtifact('author')).toEqual(janeOnly) + expect(filtering().resolvedWhere).toEqual({ author: { id: { eq: 'au2' } } }) + }) +}) diff --git a/tests/unit/types/columnFilterInitialArtifact.test.ts b/tests/unit/types/columnFilterInitialArtifact.test.ts new file mode 100644 index 00000000..a90b8b94 --- /dev/null +++ b/tests/unit/types/columnFilterInitialArtifact.test.ts @@ -0,0 +1,72 @@ +/** + * Type-level tests for the `filterInitialArtifact` column prop. + * + * A column built by `createColumn` carries its filter artifact type + * (`filterInitialArtifact` accepts only that type), and the prop sits in a + * parameter, so it is contravariant. The unparameterized `ColumnComponent` must + * still accept every specific column, or annotating such a column with it stops + * compiling. Enum columns narrow the artifact's values to the enum. + * + * Positive cases are real assignments, so they exercise the compiler's own + * assignability check. Negative cases use `assertFalse>()` + * (see `selectionErasure.test.ts` for why not `@ts-expect-error`). + */ + +import { describe, expect, test } from 'bun:test' +import type { EnumFilterArtifact, EnumListFilterArtifact, FieldRef, TextFilterArtifact } from '@contember/bindx' +import { + createColumn, + DataGridTextColumn, + enumColumnDef, + textColumnDef, + type ColumnComponent, + type ColumnComponentProps, + type DataGridEnumColumnProps, + type DataGridEnumListColumnProps, +} from '@contember/bindx-dataview' + +type IsAssignable = [TSource] extends [TTarget] ? true : false + +function assertTrue(): void {} +function assertFalse(): void {} + +const TextColumn: ColumnComponent = createColumn(textColumnDef, { renderCell: () => null }) +const EnumColumn: ColumnComponent = createColumn(enumColumnDef, { renderCell: () => null }) +const BuiltInTextColumn: ColumnComponent = DataGridTextColumn + +type TextColumnProps = Parameters>[0] + +describe('ColumnComponent — filter artifact type', () => { + test('a column built by createColumn is assignable to the unparameterized ColumnComponent', () => { + expect(typeof TextColumn).toBe('function') + expect(typeof EnumColumn).toBe('function') + expect(typeof BuiltInTextColumn).toBe('function') + }) + + test('a column takes an initial artifact of its own filter type only', () => { + assertTrue; filterInitialArtifact: TextFilterArtifact }, TextColumnProps>>() + assertFalse; filterInitialArtifact: EnumFilterArtifact }, TextColumnProps>>() + expect(true).toBe(true) + }) + + test('the unparameterized ColumnComponent takes no initial artifact', () => { + assertFalse; filterInitialArtifact: TextFilterArtifact }, ColumnComponentProps>>() + expect(true).toBe(true) + }) +}) + +type Status = 'draft' | 'published' + +describe('enum columns — initial artifact values', () => { + test('an enum column takes only values of its enum', () => { + assertTrue, NonNullable['filterInitialArtifact']>>>() + assertFalse, NonNullable['filterInitialArtifact']>>>() + expect(true).toBe(true) + }) + + test('an enum list column takes only values of its enum', () => { + assertTrue, NonNullable['filterInitialArtifact']>>>() + assertFalse, NonNullable['filterInitialArtifact']>>>() + expect(true).toBe(true) + }) +})