From c5ecb3e381c25c8017e1115a3c39e068d465fdda Mon Sep 17 00:00:00 2001 From: David Matejka Date: Fri, 2 Oct 2026 15:25:34 +0200 Subject: [PATCH 1/2] test: failing repro for relation header filter search querying the id column Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013L98ArhvRJ1p7riCvTRXBf --- .../relationFilterSearchFields.test.tsx | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/react/dataview/relationFilterSearchFields.test.tsx diff --git a/tests/react/dataview/relationFilterSearchFields.test.tsx b/tests/react/dataview/relationFilterSearchFields.test.tsx new file mode 100644 index 0000000..7bb9f75 --- /dev/null +++ b/tests/react/dataview/relationFilterSearchFields.test.tsx @@ -0,0 +1,108 @@ +// Regression test for +// +// The search box in a relation column's header filter must query only the +// text fields the cell renders. The column's related selection also holds the +// relation's `id`, and a `containsCI` on an id column is not a valid Contember +// condition, so the engine rejects the options query and the list goes empty. +import '../../setup' +import { afterEach, describe, expect, test } from 'bun:test' +import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react' +import React from 'react' +import { BindxProvider, MockAdapter } from '@contember/bindx-react' +import { DataGrid } from '@contember/bindx-dataview' +import { DataGridAutoTable, DataGridHasManyColumn, DataGridHasOneColumn } from '@contember/bindx-ui' +import { schema, testSchema } from '../../shared/index.js' + +afterEach(async () => { + await act(async () => { + cleanup() + await new Promise(resolve => setTimeout(resolve, 0)) + }) +}) + +function renderGrid(): { optionFilters: (entityType: string) => unknown[] } { + const adapter = new MockAdapter({ + Article: { + 'article-1': { + id: 'article-1', + title: 'First', + content: '', + author: { id: 'author-1', name: 'Alice', email: 'alice@example.com' }, + tags: [{ id: 'tag-1', name: 'News', color: 'red' }], + }, + }, + Author: { + 'author-1': { id: 'author-1', name: 'Alice', email: 'alice@example.com' }, + }, + Tag: { + 'tag-1': { id: 'tag-1', name: 'News', color: 'red' }, + }, + Location: {}, + }, { delay: 0 }) + + const listFilters: Array<{ entityType: string; filter: unknown }> = [] + const query = adapter.query.bind(adapter) + adapter.query = (queries, options) => { + for (const q of queries) { + if (q.type === 'list') listFilters.push({ entityType: q.entityType, filter: q.filter }) + } + return query(queries, options) + } + + render( + + + {it => ( + <> + + {author => author.name.value} + + + {tag => tag.name.value} + + + + )} + + , + ) + + return { + optionFilters: entityType => listFilters.filter(entry => entry.entityType === entityType).map(entry => entry.filter), + } +} + +async function searchInHeaderFilter(header: string, text: string): Promise { + const trigger = await waitFor(() => { + const button = Array.from(document.querySelectorAll('button')).find(candidate => candidate.textContent?.trim() === header) + if (!button) throw new Error(`no header button ${header}`) + return button + }) + fireEvent.click(trigger) + const search = await waitFor(() => { + const input = document.querySelector('input[placeholder="Search…"]') + if (!input) throw new Error('no search input') + return input + }) + fireEvent.change(search, { target: { value: text } }) +} + +describe('relation column header filter search', () => { + test('should search a has-one column by the rendered text field only', async () => { + const { optionFilters } = renderGrid() + await searchInHeaderFilter('Author', 'Al') + + await waitFor(() => { + expect(optionFilters('Author').at(-1)).toEqual({ name: { containsCI: 'Al' } }) + }) + }) + + test('should search a has-many column by the rendered text field only', async () => { + const { optionFilters } = renderGrid() + await searchInHeaderFilter('Tags', 'Ne') + + await waitFor(() => { + expect(optionFilters('Tag').at(-1)).toEqual({ name: { containsCI: 'Ne' } }) + }) + }) +}) From 06ecc5d65a0def08c032f879e86067a7e65e44fe Mon Sep 17 00:00:00 2001 From: David Matejka Date: Fri, 2 Oct 2026 17:12:55 +0200 Subject: [PATCH 2/2] fix(bindx-ui): search a relation column's filter by its text fields, not its id The related selection of a has-one / has-many column always holds the relation's id, and the header filter's search put containsCI on it. The engine offers containsCI on String columns only, so it rejected every search and the panel showed no rows. Fixes #138 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013L98ArhvRJ1p7riCvTRXBf --- packages/bindx-ui/src/datagrid/columns/has-many-column.tsx | 3 ++- packages/bindx-ui/src/datagrid/columns/has-one-column.tsx | 3 ++- tests/react/dataview/relationFilterSearchFields.test.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/bindx-ui/src/datagrid/columns/has-many-column.tsx b/packages/bindx-ui/src/datagrid/columns/has-many-column.tsx index 9c3b030..b9047c0 100644 --- a/packages/bindx-ui/src/datagrid/columns/has-many-column.tsx +++ b/packages/bindx-ui/src/datagrid/columns/has-many-column.tsx @@ -31,7 +31,8 @@ function extractScalarFieldNames(selection: unknown): string[] { if (!meta.fields) return [] const names: string[] = [] for (const [name, field] of meta.fields) { - if (!field.nested) names.push(name) + // The relation's `id` is always selected, and the engine offers no text search on it. + if (!field.nested && name !== 'id') names.push(name) } return names } diff --git a/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx b/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx index a6999ee..eaf8a79 100644 --- a/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx +++ b/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx @@ -31,7 +31,8 @@ function extractScalarFieldNames(selection: unknown): string[] { if (!meta.fields) return [] const names: string[] = [] for (const [name, field] of meta.fields) { - if (!field.nested) names.push(name) + // The relation's `id` is always selected, and the engine offers no text search on it. + if (!field.nested && name !== 'id') names.push(name) } return names } diff --git a/tests/react/dataview/relationFilterSearchFields.test.tsx b/tests/react/dataview/relationFilterSearchFields.test.tsx index 7bb9f75..2246273 100644 --- a/tests/react/dataview/relationFilterSearchFields.test.tsx +++ b/tests/react/dataview/relationFilterSearchFields.test.tsx @@ -1,4 +1,4 @@ -// Regression test for +// Regression test for https://github.com/contember/bindx/issues/138 // // The search box in a relation column's header filter must query only the // text fields the cell renders. The column's related selection also holds the