Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/bindx/src/dataview/filterHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,16 +393,16 @@ export function createRelationFilterHandler(fieldPath: string): FilterHandler<Re
export function createIsDefinedFilterHandler(fieldPath: string): FilterHandler<IsDefinedFilterArtifact> {
return {
defaultArtifact(): IsDefinedFilterArtifact {
return { defined: null }
return {}
},

isActive(artifact: IsDefinedFilterArtifact): boolean {
return artifact.defined !== null
return artifact.nullCondition !== undefined
},

toWhere(artifact: IsDefinedFilterArtifact): Record<string, unknown> | undefined {
if (artifact.defined === null) return undefined
return buildNestedWhere(fieldPath, { isNull: !artifact.defined })
if (artifact.nullCondition === undefined) return undefined
return buildNestedWhere(fieldPath, { isNull: artifact.nullCondition })
},
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/bindx/src/dataview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ export interface EnumListFilterArtifact {
* IsDefined filter artifact
*/
export interface IsDefinedFilterArtifact {
readonly defined: boolean | null
/**
* Follows the shared `nullCondition` convention used by every other filter handler.
* `false` = exclude nulls (is-defined); `true` = include-nulls-only (not-defined);
* `undefined` = filter inactive.
*/
readonly nullCondition?: boolean
}

/**
Expand Down
16 changes: 8 additions & 8 deletions tests/unit/dataview/filterHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,19 +274,19 @@ describe('filter handlers', () => {
describe('isDefined filter', () => {
const handler = createIsDefinedFilterHandler('publishedAt')

test('defined = true → isNull: false', () => {
const where = handler.toWhere({ defined: true })
test('nullCondition = false (is-defined) → isNull: false', () => {
const where = handler.toWhere({ nullCondition: false })
expect(where).toEqual({ publishedAt: { isNull: false } })
})

test('defined = false → isNull: true', () => {
const where = handler.toWhere({ defined: false })
test('nullCondition = true (not-defined) → isNull: true', () => {
const where = handler.toWhere({ nullCondition: true })
expect(where).toEqual({ publishedAt: { isNull: true } })
})

test('defined = null is inactive', () => {
expect(handler.isActive({ defined: null })).toBe(false)
expect(handler.toWhere({ defined: null })).toBeUndefined()
test('nullCondition undefined is inactive', () => {
expect(handler.isActive({})).toBe(false)
expect(handler.toWhere({})).toBeUndefined()
})
})

Expand All @@ -299,7 +299,7 @@ describe('filter handlers', () => {

test('isDefined filter on nested path', () => {
const handler = createIsDefinedFilterHandler('author.email')
const where = handler.toWhere({ defined: true })
const where = handler.toWhere({ nullCondition: false })
expect(where).toEqual({ author: { email: { isNull: false } } })
})
})
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/dataview/isDefinedHandlerNullConditionCompat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Regression test for <issue-url — filled in after Step 7>
//
// `DataGridIsDefinedFilterControls` (bindx-ui) drives `DataViewNullFilterTrigger`,
// which writes a `nullCondition: boolean` field onto the filter artifact via
// `useDataViewNullFilter`. That field is the convention used by every other
// filter handler (text/number/date/enum/relation/boolean) — see e.g.
// `filterHandlers.test.ts > text filter > with null condition`.
//
// `createIsDefinedFilterHandler` is the outlier: its `IsDefinedFilterArtifact`
// shape is `{ defined: boolean | null }`. The handler's `isActive` and `toWhere`
// only inspect `defined` — `nullCondition` is completely ignored. So when the
// bindx-ui filter UI writes `nullCondition`, the handler never wakes up and
// the filter has no effect.
//
// `DataGridIsDefinedColumn` (in bindx-ui) wires these two together, but the
// combination is non-functional today and there are no existing usages to
// have caught it.
import '../../setup'
import { describe, expect, test } from 'bun:test'
import { createIsDefinedFilterHandler } from '@contember/bindx'

describe('createIsDefinedFilterHandler integration with DataViewNullFilterTrigger', () => {
const handler = createIsDefinedFilterHandler('email')

test('should treat artifact as active when DataGridIsDefinedFilterControls writes nullCondition: false (✓ button)', () => {
// `DataGridIsDefinedFilterControls`'s ✓ button uses
// `DataViewNullFilterTrigger action="toggleExclude"`, which calls
// `useDataViewNullFilter`'s `toggleExclude` branch:
//
// setFilter(it => ({ ...it, nullCondition: it?.nullCondition === false ? undefined : false }))
//
// Starting from the default artifact, this writes `nullCondition: false`.
const afterExcludeClick = {
...handler.defaultArtifact(),
nullCondition: false,
} as never

expect(handler.isActive(afterExcludeClick)).toBe(true)
expect(handler.toWhere(afterExcludeClick)).toEqual({ email: { isNull: false } })
})

test('should treat artifact as active when DataGridIsDefinedFilterControls writes nullCondition: true (✗ button)', () => {
// `DataGridIsDefinedFilterControls`'s ✗ button uses
// `DataViewNullFilterTrigger action="toggleInclude"`, which sets
// `nullCondition: true`.
const afterIncludeClick = {
...handler.defaultArtifact(),
nullCondition: true,
} as never

expect(handler.isActive(afterIncludeClick)).toBe(true)
expect(handler.toWhere(afterIncludeClick)).toEqual({ email: { isNull: true } })
})
})