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
new file mode 100644
index 0000000..e8f445e
--- /dev/null
+++ b/tests/react/dataview/hasOneColumnEmptyCellFilter.test.tsx
@@ -0,0 +1,85 @@
+// 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
+// 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()
+ })
+})