Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/source/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { useCollapsingGroups } from "./use-collapsing-groups.js";
export { useMoveableColumns } from "./use-movable-columns.js";
export { useColumnSort } from "./use-column-sort.js";
export { useColumnFilter } from "./use-column-filter.js";
export { useAsyncDataSource } from "./use-async-data-source.js";
export { useUndoRedo } from "./use-undo-redo.js";
22 changes: 20 additions & 2 deletions packages/source/src/stories/use-data-source.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
type Theme,
} from "@glideapps/glide-data-grid";
import { faker } from "@faker-js/faker";
import { useCollapsingGroups, useColumnSort, useMoveableColumns } from "../index.js";
import { useCollapsingGroups, useColumnFilter, useColumnSort, useMoveableColumns } from "../index.js";
import { useUndoRedo } from "../use-undo-redo.js";
import { useMockDataGenerator } from "./utils.js";
import _ from "lodash";

faker.seed(1337);

Expand Down Expand Up @@ -204,6 +205,7 @@ const cols: GridColumn[] = [
title: "A",
width: 200,
group: "Group 1",
hasMenu: true,
},
{
title: "B",
Expand Down Expand Up @@ -260,6 +262,11 @@ export const UseDataSource: React.VFC = () => {
});

const [sort, setSort] = React.useState<number>();
const [filterValue, setFilterValue] = React.useState<number | undefined>(undefined);

const filterHandler = (value: number) => {
return filterValue > value;
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter callback expects a GridCell object but treats the parameter as a number. This will cause a runtime error since filterValue > value will be comparing a number to a GridCell object. Should be return filterValue > Number(value.data).

Suggested change
return filterValue > value;
const filterHandler = (value) => {
return filterValue > Number(value.data);

Copilot uses AI. Check for mistakes.

}

const sortArgs = useColumnSort({
columns: moveArgs.columns,
Expand All @@ -275,6 +282,16 @@ export const UseDataSource: React.VFC = () => {
},
});

const filterArgs = useColumnFilter({
columns: moveArgs.columns,
getCellContent: moveArgs.getCellContent,
rows,
filter: filterValue === undefined ? undefined : {
column: moveArgs.columns[0],
filterCallback: filterHandler
}
})

const collapseArgs = useCollapsingGroups({
columns: moveArgs.columns,
theme: testTheme,
Expand All @@ -287,12 +304,13 @@ export const UseDataSource: React.VFC = () => {

return (
<BeautifulWrapper title="Custom source extensions" description={<Description>Fixme.</Description>}>
<input value={filterValue} type="number" onChange={(e) => setFilterValue(_.parseInt(e.target.value))} placeholder="Filter value for column A"/>
<DataEditor
{...defaultProps}
{...moveArgs}
{...sortArgs}
{...collapseArgs}
rows={rows}
{...filterArgs}
onColumnMoved={moveArgs.onColumnMoved}
onHeaderClicked={onHeaderClick}
/>
Expand Down
88 changes: 88 additions & 0 deletions packages/source/src/use-column-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { type DataEditorProps, type GridCell, type GridColumn } from "@glideapps/glide-data-grid";
import range from "lodash/range.js";
import { useMemo, useCallback } from "react";

export type ColumnFilter = {
column: GridColumn;
filterCallback: (cellValue: any) => boolean;
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cellValue parameter should be typed as GridCell instead of any since the hook passes the entire cell object (line 53) to the filter callback, not just the cell's data value.

Suggested change
filterCallback: (cellValue: any) => boolean;
filterCallback: (cellValue: GridCell) => boolean;

Copilot uses AI. Check for mistakes.

};

type Props = Pick<DataEditorProps, "getCellContent" | "rows" | "columns"> & {
filter?: ColumnFilter | ColumnFilter[];
};
type Result = Pick<DataEditorProps, "getCellContent" | "rows"> & {
getOriginalIndex: (index: number) => number;
};

export function useColumnFilter(p: Props): Result {
const { filter, rows, getCellContent: getCellContentIn } = p;

const filters = useMemo(() => {
if (filter === undefined) return [] as ColumnFilter[];
return Array.isArray(filter) ? filter : [filter];
}, [filter]);

const filterCols = useMemo(() =>
filters.map(s => {
const c = p.columns.findIndex(col => s.column === col || (col.id !== undefined && s.column.id === col.id));
return c === -1 ? undefined : c;
}),
[filters, p.columns]
);

const filterMap = useMemo(() => {
const activeFilters = filters
.map((s, i) => ({ filter: s, col: filterCols[i] }))
.filter((v): v is { filter: ColumnFilter; col: number } => v.col !== undefined);

if (activeFilters.length === 0) return undefined;

const values = activeFilters.map(() => new Array<GridCell>(rows));
for (let sIndex = 0; sIndex < activeFilters.length; sIndex++) {
const { col } = activeFilters[sIndex];
const index: [number, number] = [col, 0];
for (let i = 0; i < rows; i++) {
index[1] = i;
values[sIndex][i] = getCellContentIn(index);
}
}

return range(rows).filter((r) => {
for (let sIndex = 0; sIndex < activeFilters.length; sIndex++) {
const { filter: colFilter } = activeFilters[sIndex];
const v = values[sIndex][r];
if (!colFilter.filterCallback(v)) {
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter callback receives the entire GridCell object v, but the type definition suggests it should receive cellValue: any. This inconsistency could cause runtime errors if consumers expect just the cell data rather than the full cell object.

Suggested change
if (!colFilter.filterCallback(v)) {
if (!colFilter.filterCallback(v.data)) {

Copilot uses AI. Check for mistakes.

return false;
}
}
return true;
})
}, [getCellContentIn, rows, filters, filterCols]);

const getOriginalIndex = useCallback(
(index: number): number => {
if (filterMap === undefined) return index;
return filterMap[index];
},
[filterMap]
);

const getCellContent = useCallback<typeof getCellContentIn>(
([col, row]) => {
if (filterMap === undefined) return getCellContentIn([col, row]);
row = filterMap[row];
return getCellContentIn([col, row]);
},
[getCellContentIn, filterMap]
);

if (filterMap === undefined) {
return { getCellContent: p.getCellContent, getOriginalIndex, rows: p.rows };
}

return {
getOriginalIndex,
getCellContent,
rows: filterMap.length,
};
}
42 changes: 42 additions & 0 deletions packages/source/test/use-column-filter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useColumnFilter } from "../src/use-column-filter.js";
import { renderHook } from "@testing-library/react-hooks";
import { GridCellKind, type GridCell } from "@glideapps/glide-data-grid";
import { expect, describe, test } from "vitest";

describe("use-column-filter", () => {
test("multi column filter", () => {
const columns = [
{ title: "A", id: "A" },
{ title: "B", id: "B" },
];
const data = [
["2", "a"],
["1", "b"],
["2", "b"],
["1", "a"],
["3", "c"],
];

const getCellContent = ([col, row]: readonly [number, number]): GridCell => ({
kind: GridCellKind.Text,
allowOverlay: false,
data: data[row][col],
displayData: data[row][col],
});

const { result } = renderHook(() =>
useColumnFilter({
columns,
rows: data.length,
getCellContent,
filter: [
{ column: columns[0], filterCallback: (v) => v !== "2" },
{ column: columns[1], filterCallback: (v) => v !== "b" },
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test filter callback expects a string but will receive a GridCell object. This should be (v) => v.data !== "2" to access the cell's data property.

Suggested change
{ column: columns[1], filterCallback: (v) => v !== "b" },
{ column: columns[0], filterCallback: (v) => v.data !== "2" },
{ column: columns[1], filterCallback: (v) => v.data !== "b" },

Copilot uses AI. Check for mistakes.

Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test filter callback expects a string but will receive a GridCell object. This should be (v) => v.data !== "b" to access the cell's data property.

Suggested change
{ column: columns[1], filterCallback: (v) => v !== "b" },
{ column: columns[1], filterCallback: (v) => v.data !== "b" },

Copilot uses AI. Check for mistakes.

],
})
);

const order = Array.from({ length: data.length }, (_, i) => result.current.getOriginalIndex(i));
expect(order).toEqual([3, 4, undefined, undefined, undefined]);
});
});
Loading