From c741fad0d0058fa655e17cf705e268f307d93e1a Mon Sep 17 00:00:00 2001 From: David Matejka Date: Tue, 29 Sep 2026 13:09:44 +0200 Subject: [PATCH 1/2] test(bindx-ui): failing repro for filter actions on an empty has-one cell Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013L98ArhvRJ1p7riCvTRXBf --- .../hasOneColumnEmptyCellFilter.test.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx diff --git a/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx b/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx new file mode 100644 index 0000000..41c48ac --- /dev/null +++ b/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx @@ -0,0 +1,85 @@ +// Regression test for +// +// A nullable has-one with no related entity still yields a handle whose `id` +// is a disconnected placeholder id. The cell must not offer include/exclude +// actions on it: a filter on that id cannot match anything, and a server +// rejects it as an invalid id. +import '../../setup' +import { afterEach, describe, expect, test } from 'bun:test' +import { act, cleanup, 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, 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(): HTMLElement { + const adapter = new MockAdapter({ + Article: { + 'article-1': { + id: 'article-1', + title: 'With author', + content: '', + author: { id: 'author-1', name: 'Alice', email: 'alice@example.com' }, + tags: [], + }, + 'article-2': { + id: 'article-2', + title: 'Without author', + content: '', + author: null, + tags: [], + }, + }, + Author: { + 'author-1': { id: 'author-1', name: 'Alice', email: 'alice@example.com' }, + }, + Tag: {}, + Location: {}, + }, { delay: 0 }) + + const { container } = render( + + + {it => ( + <> + + {author => author.name.value ?? '—'} + + + + )} + + , + ) + return container +} + +function authorCells(container: HTMLElement): HTMLElement[] { + return Array.from(container.querySelectorAll('[data-testid="datagrid-cell-author"]')) +} + +describe('has-one column cell filter affordance', () => { + test('should not offer include/exclude on a cell whose relation is empty', async () => { + const container = renderGrid() + + await waitFor(() => { + expect(authorCells(container)).toHaveLength(2) + }) + + const connectedCell = authorCells(container).find(cell => cell.textContent?.includes('Alice')) + const emptyCell = authorCells(container).find(cell => cell.textContent?.includes('—')) + expect(connectedCell).toBeDefined() + expect(emptyCell).toBeDefined() + + expect(connectedCell!.querySelector('[data-bindx-tooltip]')).not.toBeNull() + expect(emptyCell!.querySelector('[data-bindx-tooltip]')).toBeNull() + }) +}) From 05fcae830614f1823db072cebdf96ca4d543d6db Mon Sep 17 00:00:00 2001 From: David Matejka Date: Tue, 29 Sep 2026 13:16:54 +0200 Subject: [PATCH 2/2] fix(bindx-ui): offer no filter actions on an empty has-one cell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A disconnected has-one still exposes a placeholder id, so the cell wrapper's `if (!id)` guard never triggered on an empty relation. The cell offered include/exclude, and either action filtered on `__placeholder_…`, which the server rejects as an invalid id. Wrap the cell only when the related id is a persisted one. An empty value stays filterable through the column's null filter. Fixes #136 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013L98ArhvRJ1p7riCvTRXBf --- packages/bindx-ui/src/datagrid/columns/has-one-column.tsx | 6 ++++-- tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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..1bc2e8b 100644 --- a/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx +++ b/packages/bindx-ui/src/datagrid/columns/has-one-column.tsx @@ -1,5 +1,6 @@ import React, { type ReactElement, type ReactNode, useCallback } from 'react' import type { EntityAccessor, EntityDef, FieldRef } from '@contember/bindx' +import { isPersistedId } from '@contember/bindx' import { createRelationColumn, hasOneColumnDef, @@ -106,8 +107,9 @@ const relationUI = { ), renderCellWrapper: ({ content, item, fieldName, filterName, fieldRef }: RelationCellWrapperContext) => { - const id = getRelatedAccessor(item, fieldName)?.id ?? null - if (!id) return content + const id = getRelatedAccessor(item, fieldName)?.id + // An empty relation still has a placeholder id; filtering on it is rejected by the server. + if (!id || !isPersistedId(id)) return content return ( {content} diff --git a/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx b/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx index 41c48ac..e8f445e 100644 --- a/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx +++ b/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx @@ -1,4 +1,4 @@ -// Regression test for +// Regression test for https://github.com/contember/bindx/issues/136 // // A nullable has-one with no related entity still yields a handle whose `id` // is a disconnected placeholder id. The cell must not offer include/exclude