Summary
DataGridIsDefinedFilterControls and createIsDefinedFilterHandler are wired together by DataGridIsDefinedColumn, but they read and write different fields on the filter artifact — the controls write nullCondition (via DataViewNullFilterTrigger → useDataViewNullFilter), while the handler reads defined. Clicking the ✓/✗ buttons in the popover therefore never activates the handler and the filter has no effect on the data.
Environment
Reproduction
import { createIsDefinedFilterHandler } from '@contember/bindx'
const handler = createIsDefinedFilterHandler('email')
// What `DataGridIsDefinedFilterControls`'s ✓ button actually writes onto the
// artifact (via `useDataViewNullFilter` action="toggleExclude"):
const afterExcludeClick = { ...handler.defaultArtifact(), nullCondition: false }
handler.isActive(afterExcludeClick) // → false (expected: true)
handler.toWhere(afterExcludeClick) // → undefined (expected: { email: { isNull: false } })
The same mismatch occurs end-to-end inside a grid:
<DataGrid entity={User}>
{it => (
<>
<DataGridIsDefinedColumn field={it.tenantPersonId} header="Has identity" />
</>
)}
</DataGrid>
Clicking ✓ or ✗ inside the column header popover updates the artifact's nullCondition field but leaves defined: null. useFilteringState.hasActiveFilters stays false, resolvedWhere stays undefined, and the table doesn't filter.
Expected behavior
Either the controls or the handler should change to agree on the field. Every other filter type in filterHandlers.test.ts uses nullCondition as the canonical "include nullable bucket" toggle — text, number, date, enum, relation all support { ..., nullCondition: boolean } artifacts (see existing text filter > with null condition test).
createIsDefinedFilterHandler is the outlier with its { defined: boolean | null } shape.
Actual behavior
Default artifact: { defined: null } — isActive returns false, toWhere returns undefined.
After the ✓ button click (useDataViewNullFilter toggleExclude branch in filterHooks.ts):
setFilter(it => ({ ...it, nullCondition: it?.nullCondition === false ? undefined : false }))
Artifact becomes { defined: null, nullCondition: false }. Handler's isActive and toWhere both look at artifact.defined only (see filterHandlers.ts) — they ignore nullCondition entirely. isActive returns false, toWhere returns undefined, and the where clause for the GraphQL query has no contribution from this filter.
Suspected root cause
Two changes were probably made on different days without cross-checking:
IsDefinedFilterArtifact was introduced as { defined: boolean | null } — see packages/bindx/src/dataview/filterArtifacts.ts.
DataGridIsDefinedFilterControls was built on top of DataViewNullFilterTrigger, which had already standardized on nullCondition.
Neither side was wrong on its own. The integration just wasn't covered by a test — DataGridIsDefinedColumn has no usage in the repo, so the broken combination went unnoticed.
Suggested fix
Pick one of the two conventions and align both sides. The least-disruptive option is to make createIsDefinedFilterHandler read nullCondition, since every other filter handler already supports that field and DataViewNullFilterTrigger is generic infrastructure used by many components:
// packages/bindx/src/dataview/filterHandlers.ts
export interface IsDefinedFilterArtifact {
readonly nullCondition?: boolean
}
export function createIsDefinedFilterHandler(fieldPath: string): FilterHandler<IsDefinedFilterArtifact> {
return {
defaultArtifact: () => ({}),
isActive: artifact => artifact.nullCondition !== undefined,
toWhere: artifact => artifact.nullCondition === undefined
? undefined
: buildNestedWhere(fieldPath, { isNull: artifact.nullCondition }),
}
}
Semantics map: nullCondition: false ≡ "exclude nulls" = is-defined; nullCondition: true ≡ "include nulls only" = not-defined; undefined ≡ off. That matches the existing ✓/✗ button copy in DataGridIsDefinedFilterControls.
If keeping defined is preferred, the controls / useDataViewNullFilter need a parallel toggleDefined action and the IsDefined column needs its own dedicated trigger.
Either fix should come with a DataGridIsDefinedColumn integration test under tests/react/dataview/ that asserts the click actually flips hasActiveFilters.
Workaround shipped downstream
We applied a temporary workaround in our project, marked
TODO [BindX] (<this-issue-url>): <description>. The workaround is a tiny two-button filter UI in our admin (IsDefinedFilterButtons) that writes { defined: boolean | null } directly via the column's setArtifact, bypassing DataGridIsDefinedFilterControls/DataViewNullFilterTrigger. We will remove this once the controls and handler agree.
Summary
DataGridIsDefinedFilterControlsandcreateIsDefinedFilterHandlerare wired together byDataGridIsDefinedColumn, but they read and write different fields on the filter artifact — the controls writenullCondition(viaDataViewNullFilterTrigger→useDataViewNullFilter), while the handler readsdefined. Clicking the ✓/✗ buttons in the popover therefore never activates the handler and the filter has no effect on the data.Environment
@contember/bindx@0.1.37(version installed in the reporting project)contember/bindx@mainas ofbd9da13tests/unit/dataview/isDefinedHandlerNullConditionCompat.test.tsbug/isdefined-filter-controls-handler-field-mismatchReproduction
The same mismatch occurs end-to-end inside a grid:
Clicking ✓ or ✗ inside the column header popover updates the artifact's
nullConditionfield but leavesdefined: null.useFilteringState.hasActiveFiltersstaysfalse,resolvedWherestaysundefined, and the table doesn't filter.Expected behavior
Either the controls or the handler should change to agree on the field. Every other filter type in
filterHandlers.test.tsusesnullConditionas the canonical "include nullable bucket" toggle — text, number, date, enum, relation all support{ ..., nullCondition: boolean }artifacts (see existingtext filter > with null conditiontest).createIsDefinedFilterHandleris the outlier with its{ defined: boolean | null }shape.Actual behavior
Default artifact:
{ defined: null }—isActivereturnsfalse,toWherereturnsundefined.After the ✓ button click (
useDataViewNullFiltertoggleExcludebranch infilterHooks.ts):Artifact becomes
{ defined: null, nullCondition: false }. Handler'sisActiveandtoWhereboth look atartifact.definedonly (seefilterHandlers.ts) — they ignorenullConditionentirely.isActivereturnsfalse,toWherereturnsundefined, and the where clause for the GraphQL query has no contribution from this filter.Suspected root cause
Two changes were probably made on different days without cross-checking:
IsDefinedFilterArtifactwas introduced as{ defined: boolean | null }— seepackages/bindx/src/dataview/filterArtifacts.ts.DataGridIsDefinedFilterControlswas built on top ofDataViewNullFilterTrigger, which had already standardized onnullCondition.Neither side was wrong on its own. The integration just wasn't covered by a test —
DataGridIsDefinedColumnhas no usage in the repo, so the broken combination went unnoticed.Suggested fix
Pick one of the two conventions and align both sides. The least-disruptive option is to make
createIsDefinedFilterHandlerreadnullCondition, since every other filter handler already supports that field andDataViewNullFilterTriggeris generic infrastructure used by many components:Semantics map:
nullCondition: false≡ "exclude nulls" = is-defined;nullCondition: true≡ "include nulls only" = not-defined;undefined≡ off. That matches the existing ✓/✗ button copy inDataGridIsDefinedFilterControls.If keeping
definedis preferred, the controls /useDataViewNullFilterneed a paralleltoggleDefinedaction and the IsDefined column needs its own dedicated trigger.Either fix should come with a
DataGridIsDefinedColumnintegration test undertests/react/dataview/that asserts the click actually flipshasActiveFilters.Workaround shipped downstream
We applied a temporary workaround in our project, marked
TODO [BindX] (<this-issue-url>): <description>. The workaround is a tiny two-button filter UI in our admin (IsDefinedFilterButtons) that writes{ defined: boolean | null }directly via the column'ssetArtifact, bypassingDataGridIsDefinedFilterControls/DataViewNullFilterTrigger. We will remove this once the controls and handler agree.