diff --git a/packages/source/src/index.ts b/packages/source/src/index.ts index 7d7ef5100..31fe23526 100644 --- a/packages/source/src/index.ts +++ b/packages/source/src/index.ts @@ -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"; diff --git a/packages/source/src/stories/use-data-source.stories.tsx b/packages/source/src/stories/use-data-source.stories.tsx index 64e1e3e6f..b86ea63c1 100644 --- a/packages/source/src/stories/use-data-source.stories.tsx +++ b/packages/source/src/stories/use-data-source.stories.tsx @@ -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); @@ -204,6 +205,7 @@ const cols: GridColumn[] = [ title: "A", width: 200, group: "Group 1", + hasMenu: true, }, { title: "B", @@ -260,6 +262,11 @@ export const UseDataSource: React.VFC = () => { }); const [sort, setSort] = React.useState(); + const [filterValue, setFilterValue] = React.useState(undefined); + + const filterHandler = (value: number) => { + return filterValue > value; + } const sortArgs = useColumnSort({ columns: moveArgs.columns, @@ -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, @@ -287,12 +304,13 @@ export const UseDataSource: React.VFC = () => { return ( Fixme.}> + setFilterValue(_.parseInt(e.target.value))} placeholder="Filter value for column A"/> diff --git a/packages/source/src/use-column-filter.ts b/packages/source/src/use-column-filter.ts new file mode 100644 index 000000000..05da3496e --- /dev/null +++ b/packages/source/src/use-column-filter.ts @@ -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; +}; + +type Props = Pick & { + filter?: ColumnFilter | ColumnFilter[]; +}; +type Result = Pick & { + 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(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)) { + 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( + ([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, + }; +} diff --git a/packages/source/test/use-column-filter.test.tsx b/packages/source/test/use-column-filter.test.tsx new file mode 100644 index 000000000..5dc23c596 --- /dev/null +++ b/packages/source/test/use-column-filter.test.tsx @@ -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" }, + ], + }) + ); + + const order = Array.from({ length: data.length }, (_, i) => result.current.getOriginalIndex(i)); + expect(order).toEqual([3, 4, undefined, undefined, undefined]); + }); +});