Skip to content

Commit 9310038

Browse files
committed
fix: use getFilteredRowModel when filtering is enabled
1 parent 8974f75 commit 9310038

9 files changed

Lines changed: 306 additions & 0 deletions

File tree

src/components/BaseTable/__stories__/BaseTable.stories.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ColumnPinningStory} from './stories/ColumnPinningStory';
66
import {CustomRowStory} from './stories/CustomRowStory';
77
import {DefaultStory} from './stories/DefaultStory';
88
import {EmptyContentStory} from './stories/EmptyContentStory';
9+
import {FilteringStory} from './stories/FilteringStory';
910
import {GroupingStory} from './stories/GroupingStory';
1011
import {GroupingStory2} from './stories/GroupingStory2';
1112
import {GroupingWithSelectionStory} from './stories/GroupingWithSelectionStory';
@@ -51,6 +52,10 @@ export const Sorting: StoryObj<typeof SortingStory> = {
5152
render: SortingStory,
5253
};
5354

55+
export const Filtering: StoryObj<typeof FilteringStory> = {
56+
render: FilteringStory,
57+
};
58+
5459
export const Grouping: StoryObj<typeof GroupingStory> = {
5560
render: GroupingStory,
5661
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type {ColumnDef} from '../../../../types/base';
2+
import type {Item} from '../types';
3+
4+
export const filterableColumns: ColumnDef<Item>[] = [
5+
{
6+
id: 'name',
7+
header: 'Name',
8+
accessorKey: 'name',
9+
filterFn: 'includesString',
10+
size: 200,
11+
},
12+
{
13+
id: 'age',
14+
header: 'Age',
15+
accessorKey: 'age',
16+
size: 100,
17+
},
18+
{
19+
id: 'status',
20+
header: 'Status',
21+
accessorKey: 'status',
22+
filterFn: (row, columnId, filterValue) => {
23+
if (!filterValue) return true;
24+
return row.getValue(columnId) === filterValue;
25+
},
26+
size: 150,
27+
},
28+
];
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as React from 'react';
2+
3+
import type {ColumnFiltersState} from '@tanstack/react-table';
4+
5+
import {useTable} from '../../../../hooks';
6+
import {BaseTable} from '../../BaseTable';
7+
import {data} from '../constants/data';
8+
import {filterableColumns} from '../constants/filtering';
9+
10+
export const FilteringStory = () => {
11+
const [globalFilter, setGlobalFilter] = React.useState('');
12+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
13+
14+
const table = useTable({
15+
data,
16+
columns: filterableColumns,
17+
enableGlobalFilter: true,
18+
enableColumnFilters: true,
19+
state: {
20+
globalFilter,
21+
columnFilters,
22+
},
23+
onGlobalFilterChange: setGlobalFilter,
24+
onColumnFiltersChange: setColumnFilters,
25+
getRowId: (row) => row.id,
26+
});
27+
28+
const handleClearFilters = () => {
29+
setGlobalFilter('');
30+
setColumnFilters([]);
31+
};
32+
33+
const handleNameFilterChange = (value: string) => {
34+
const newFilters = columnFilters.filter((f) => f.id !== 'name');
35+
if (value) {
36+
newFilters.push({id: 'name', value});
37+
}
38+
setColumnFilters(newFilters);
39+
};
40+
41+
const handleStatusFilterChange = (value: string) => {
42+
const newFilters = columnFilters.filter((f) => f.id !== 'status');
43+
if (value) {
44+
newFilters.push({id: 'status', value});
45+
}
46+
setColumnFilters(newFilters);
47+
};
48+
49+
const nameFilter = columnFilters.find((f) => f.id === 'name')?.value as string | undefined;
50+
const statusFilter = columnFilters.find((f) => f.id === 'status')?.value as string | undefined;
51+
52+
return (
53+
<div>
54+
<div style={{marginBottom: '16px'}}>
55+
<label style={{display: 'block', marginBottom: '8px'}}>
56+
Global Search:
57+
<input
58+
type="text"
59+
placeholder="Search all columns..."
60+
value={globalFilter}
61+
onChange={(e) => setGlobalFilter(e.target.value)}
62+
style={{marginLeft: '8px', padding: '4px'}}
63+
/>
64+
</label>
65+
<div style={{fontSize: '14px', color: '#666'}}>
66+
Showing {table.getFilteredRowModel().rows.length} of {data.length} rows
67+
</div>
68+
</div>
69+
70+
<div style={{marginBottom: '16px'}}>
71+
<label style={{display: 'block', marginBottom: '8px'}}>
72+
Filter by Name:
73+
<input
74+
type="text"
75+
placeholder="Filter by name..."
76+
value={nameFilter || ''}
77+
onChange={(e) => handleNameFilterChange(e.target.value)}
78+
style={{marginLeft: '8px', padding: '4px'}}
79+
/>
80+
</label>
81+
<label style={{display: 'block', marginBottom: '8px'}}>
82+
Filter by Status:
83+
<select
84+
value={statusFilter || ''}
85+
onChange={(e) => handleStatusFilterChange(e.target.value)}
86+
style={{marginLeft: '8px', padding: '4px'}}
87+
>
88+
<option value="">All</option>
89+
<option value="free">Free</option>
90+
<option value="busy">Busy</option>
91+
<option value="unknown">Unknown</option>
92+
</select>
93+
</label>
94+
</div>
95+
96+
<div style={{marginBottom: '16px'}}>
97+
<button onClick={handleClearFilters} style={{padding: '4px 8px'}}>
98+
Clear All Filters
99+
</button>
100+
</div>
101+
102+
<BaseTable table={table} />
103+
</div>
104+
);
105+
};

src/components/Table/__stories__/Table.stories.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {Meta, StoryObj} from '@storybook/react';
33
import {Table} from '../index';
44

55
import {DefaultStory} from './stories/DefaultStory';
6+
import {FilteringStory} from './stories/FilteringStory';
67
import {GroupingStory} from './stories/GroupingStory';
78
import {GroupingWithSelectionStory} from './stories/GroupingWithSelectionStory';
89
import {ReorderingStory} from './stories/ReorderingStory';
@@ -62,6 +63,10 @@ export const Sorting: StoryObj<typeof SortingStory> = {
6263
render: SortingStory,
6364
};
6465

66+
export const Filtering: StoryObj<typeof FilteringStory> = {
67+
render: FilteringStory,
68+
};
69+
6570
export const Reordering: StoryObj<typeof ReorderingStory> = {
6671
render: ReorderingStory,
6772
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Select, TextInput} from '@gravity-ui/uikit';
2+
import type {SelectOption} from '@gravity-ui/uikit';
3+
4+
import type {ColumnDef} from '../../../../types/base';
5+
import type {Item} from '../../../BaseTable/__stories__/types';
6+
import {cnFilteringStory} from '../stories/FilteringStory.classname';
7+
8+
export const statusOptions: SelectOption[] = [
9+
{value: '', content: 'All'},
10+
{value: 'free', content: 'Free'},
11+
{value: 'busy', content: 'Busy'},
12+
{value: 'unknown', content: 'Unknown'},
13+
];
14+
15+
export const filterableColumns: ColumnDef<Item>[] = [
16+
{
17+
id: 'name',
18+
header: ({column}) => (
19+
<div>
20+
<div className={cnFilteringStory('column-header')}>Name</div>
21+
<TextInput
22+
value={(column.getFilterValue() as string) || ''}
23+
onUpdate={(value) => column.setFilterValue(value)}
24+
placeholder="Filter by name..."
25+
size="s"
26+
/>
27+
</div>
28+
),
29+
accessorKey: 'name',
30+
filterFn: 'includesString',
31+
size: 200,
32+
},
33+
{
34+
id: 'age',
35+
header: 'Age',
36+
accessorKey: 'age',
37+
size: 100,
38+
},
39+
{
40+
id: 'status',
41+
header: ({column}) => (
42+
<div>
43+
<div className={cnFilteringStory('column-header')}>Status</div>
44+
<Select
45+
value={[(column.getFilterValue() as string) || '']}
46+
onUpdate={(values) => column.setFilterValue(values[0] || undefined)}
47+
options={statusOptions}
48+
size="s"
49+
width="max"
50+
/>
51+
</div>
52+
),
53+
accessorKey: 'status',
54+
filterFn: (row, columnId, filterValue) => {
55+
if (!filterValue) return true;
56+
return row.getValue(columnId) === filterValue;
57+
},
58+
size: 150,
59+
},
60+
];
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {cn} from '../../../../utils';
2+
3+
export const cnFilteringStory = cn('filtering-story');
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.filtering-story {
2+
&__controls {
3+
margin-block-end: 20px;
4+
}
5+
6+
&__global-search {
7+
margin-block-end: 20px;
8+
}
9+
10+
&__title {
11+
margin-block-end: 12px;
12+
font-weight: 500;
13+
}
14+
15+
&__input {
16+
max-inline-size: 400px;
17+
}
18+
19+
&__info {
20+
margin-block-start: 12px;
21+
font-size: 13px;
22+
opacity: 0.7;
23+
}
24+
25+
&__column-header {
26+
margin-block-end: 8px;
27+
font-weight: bold;
28+
}
29+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react';
2+
3+
import {Button, TextInput} from '@gravity-ui/uikit';
4+
import type {ColumnFiltersState} from '@tanstack/react-table';
5+
6+
import {useTable} from '../../../../hooks';
7+
import {data} from '../../../BaseTable/__stories__/constants/data';
8+
import {Table} from '../../Table';
9+
import {filterableColumns} from '../constants/filtering';
10+
11+
import {cnFilteringStory} from './FilteringStory.classname';
12+
13+
import './FilteringStory.scss';
14+
15+
export const FilteringStory = () => {
16+
const [globalFilter, setGlobalFilter] = React.useState('');
17+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
18+
19+
const table = useTable({
20+
data,
21+
columns: filterableColumns,
22+
enableGlobalFilter: true,
23+
enableColumnFilters: true,
24+
state: {
25+
globalFilter,
26+
columnFilters,
27+
},
28+
onGlobalFilterChange: setGlobalFilter,
29+
onColumnFiltersChange: setColumnFilters,
30+
getRowId: (row) => row.id,
31+
});
32+
33+
const handleClearFilters = React.useCallback(() => {
34+
setGlobalFilter('');
35+
setColumnFilters([]);
36+
}, []);
37+
38+
return (
39+
<div>
40+
<div className={cnFilteringStory('global-search')}>
41+
<h3 className={cnFilteringStory('title')}>Global Search</h3>
42+
<TextInput
43+
placeholder="Search all columns..."
44+
value={globalFilter}
45+
onUpdate={setGlobalFilter}
46+
size="m"
47+
className={cnFilteringStory('input')}
48+
/>
49+
<div className={cnFilteringStory('info')}>
50+
Showing {table.getFilteredRowModel().rows.length} of {data.length} rows
51+
</div>
52+
</div>
53+
54+
<div className={cnFilteringStory('controls')}>
55+
<Button onClick={handleClearFilters} view="outlined">
56+
Clear All Filters
57+
</Button>
58+
</div>
59+
60+
<Table table={table} />
61+
</div>
62+
);
63+
};

src/hooks/useTable.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {TableOptions} from '@tanstack/react-table';
22
import {
33
getCoreRowModel,
44
getExpandedRowModel,
5+
getFilteredRowModel,
56
getGroupedRowModel,
67
getSortedRowModel,
78
useReactTable,
@@ -19,6 +20,8 @@ export const useTable = <TData>(options: UseTableOptions<TData>) => {
1920
enableMultiRowSelection: options.enableMultiRowSelection ?? false,
2021
enableRowSelection: options.enableRowSelection ?? false,
2122
enableSorting: options.enableSorting ?? false,
23+
enableColumnFilters: options.enableColumnFilters ?? false,
24+
enableGlobalFilter: options.enableGlobalFilter ?? false,
2225
getCoreRowModel: options.getCoreRowModel ?? getCoreRowModel(),
2326
getExpandedRowModel: options.enableExpanding
2427
? (options.getExpandedRowModel ?? getExpandedRowModel())
@@ -29,8 +32,13 @@ export const useTable = <TData>(options: UseTableOptions<TData>) => {
2932
getSortedRowModel: options.enableSorting
3033
? (options.getSortedRowModel ?? getSortedRowModel())
3134
: undefined,
35+
getFilteredRowModel:
36+
options.enableColumnFilters || options.enableGlobalFilter
37+
? (options.getFilteredRowModel ?? getFilteredRowModel())
38+
: undefined,
3239
manualGrouping: options.manualGrouping ?? false,
3340
manualSorting: options.manualSorting ?? false,
41+
manualFiltering: options.manualFiltering ?? false,
3442
};
3543

3644
return useReactTable(tableOptions);

0 commit comments

Comments
 (0)