Skip to content

Commit 5d771ad

Browse files
LWS49adi-herwana-nus
authored andcommitted
fix(search): fix bug where external ID not included in search when first entry has external ID being null
1 parent 710bb4d commit 5d771ad

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ const useTanStackTableBuilder = <D extends object>(
175175
props.pagination.onPaginationChange(newValue, pagination);
176176
}
177177
},
178+
// TanStack's default getColumnCanGlobalFilter sniffs the first row's value
179+
// type (string|number) to decide whether a column participates in global
180+
// filter. When the first row has a nullable column (e.g. externalId=null),
181+
// typeof null === 'object' → false, silently excluding that column from
182+
// search even when the column has searchable:true / enableGlobalFilter:true.
183+
// We already express intent via enableGlobalFilter, so bypass the sniff.
184+
// See: https://github.com/TanStack/table/pull/6252
185+
getColumnCanGlobalFilter: () => true,
178186
autoResetPageIndex: false,
179187
state: {
180188
rowSelection,

client/app/lib/components/table/__tests__/useTanStackTableBuilder.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,58 @@ describe('localStorage persistence', () => {
768768
});
769769
});
770770

771+
// ---------- global search — nullable searchable column (regression) ----------
772+
//
773+
// Root cause: TanStack's default getColumnCanGlobalFilter sniffs the first row's
774+
// value type (string|number). When the first row has externalId=null, typeof null
775+
// === 'object', so TanStack silently excludes the column from global filter for
776+
// the entire table — even rows with a real string value are never matched.
777+
// Fix: override getColumnCanGlobalFilter in useReactTable to return true always,
778+
// relying on enableGlobalFilter (set by searchable:true/false) instead of sniffing.
779+
780+
describe('global search — searchable column whose first row is null', () => {
781+
interface StudentRow {
782+
id: number;
783+
name: string;
784+
externalId: string | null;
785+
}
786+
787+
const nullFirstData: StudentRow[] = [
788+
{ id: 1, name: 'Alice', externalId: null },
789+
{ id: 2, name: 'Bob', externalId: 'EXT001' },
790+
];
791+
792+
const searchColumns: ColumnTemplate<StudentRow>[] = [
793+
{ of: 'name', title: 'Name', cell: (r) => r.name, searchable: true },
794+
{
795+
of: 'externalId',
796+
title: 'External ID',
797+
cell: (r) => r.externalId ?? '',
798+
searchable: true,
799+
},
800+
];
801+
802+
it('finds rows by a searchable column value even when the first row has null for that column', () => {
803+
const { result } = renderHook(
804+
() =>
805+
useTanStackTableBuilder<StudentRow>({
806+
data: nullFirstData,
807+
columns: searchColumns,
808+
getRowId: (r) => r.id.toString(),
809+
search: { searchPlaceholder: 'Search' },
810+
}),
811+
{ wrapper: withStore() },
812+
);
813+
814+
act(() => result.current.toolbar!.onSearchKeywordChange?.('EXT001'));
815+
816+
expect(result.current.body.rows).toHaveLength(1);
817+
expect(
818+
(result.current.body.rows[0] as { original: StudentRow }).original.name,
819+
).toBe('Bob');
820+
});
821+
});
822+
771823
describe('useTanStackTableBuilder onDirectExport', () => {
772824
beforeEach(() => {
773825
mockedDownloadFile.mockClear();

0 commit comments

Comments
 (0)