Skip to content

Commit b5bcf09

Browse files
committed
filters
1 parent 449f391 commit b5bcf09

File tree

18 files changed

+978
-34
lines changed

18 files changed

+978
-34
lines changed

packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ source: react
1111
# If you use typescript, the name of the interface to display props for
1212
# These are found through the sourceProps function provided in patternfly-docs.source.js
1313
sortValue: 4
14-
propComponents: ['DataViewToolbar', 'DataViewTableBasic', 'DataViewTableTree']
14+
propComponents: ['DataViewToolbar', 'DataViewTableBasic', 'DataViewTableTree', 'DataViewTrTree', 'DataViewTrObject']
1515
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md
1616
---
1717
import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon } from '@patternfly/react-core';
@@ -26,7 +26,7 @@ import { DataView, DataViewState } from '@patternfly/react-data-view/dist/dynami
2626

2727
The **data view toolbar** component renders a default opinionated data view toolbar above or below the data section.
2828

29-
Data view toolbar can contain a `pagination`, `bulkSelect`, `actions` or other children content passed. The preffered way of passing children toolbar items is using the [toolbar item](/components/toolbar#toolbar-items) component.
29+
Data view toolbar can contain a `pagination`, `bulkSelect`, `filters`, `actions` or other children content passed. The preffered way of passing children toolbar items is using the [toolbar item](/components/toolbar#toolbar-items) component.
3030

3131
### Basic toolbar example
3232

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { useMemo } from 'react';
2+
import { Pagination } from '@patternfly/react-core';
3+
import { BrowserRouter, useSearchParams } from 'react-router-dom';
4+
import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
5+
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
6+
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
7+
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
8+
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
9+
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
10+
11+
const perPageOptions = [
12+
{ title: '5', value: 5 },
13+
{ title: '10', value: 10 }
14+
];
15+
16+
interface Repository {
17+
name: string;
18+
branch: string | null;
19+
prs: string | null;
20+
workspaces: string;
21+
lastCommit: string;
22+
}
23+
24+
interface RepositoryFilters {
25+
name: string,
26+
branch: string
27+
}
28+
29+
const repositories: Repository[] = [
30+
{ name: 'Repository one', branch: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
31+
{ name: 'Repository two', branch: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
32+
{ name: 'Repository three', branch: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
33+
{ name: 'Repository four', branch: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
34+
{ name: 'Repository five', branch: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
35+
{ name: 'Repository six', branch: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
36+
];
37+
38+
const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspaces', 'Last commit' ];
39+
40+
const ouiaId = 'LayoutExample';
41+
42+
const MyTable: React.FunctionComponent = () => {
43+
const [ searchParams, setSearchParams ] = useSearchParams();
44+
const pagination = useDataViewPagination({ perPage: 5 });
45+
const { page, perPage } = pagination;
46+
const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '' }, searchParams, setSearchParams });
47+
48+
const pageRows = useMemo(() => repositories
49+
.filter(item => (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && (!filters.branch || item.branch?.toLocaleLowerCase().includes(filters.branch?.toLocaleLowerCase())))
50+
.slice((page - 1) * perPage, ((page - 1) * perPage) + perPage)
51+
.map(item => Object.values(item)), [ page, perPage, filters ]);
52+
53+
return (
54+
<DataView>
55+
<DataViewToolbar
56+
ouiaId='LayoutExampleHeader'
57+
clearAllFilters = {clearAllFilters}
58+
pagination={
59+
<Pagination
60+
perPageOptions={perPageOptions}
61+
itemCount={repositories.length}
62+
{...pagination}
63+
/>
64+
}
65+
filters={
66+
<DataViewFilters onChange={(_e, values) => onSetFilters(values)} values={filters}>
67+
<DataViewTextFilter filterId="name" title='Name' placeholder='Filter by name' />
68+
<DataViewTextFilter filterId="branch" title='Branch' placeholder='Filter by branch' />
69+
</DataViewFilters>
70+
}
71+
/>
72+
<DataViewTable aria-label='Repositories table' ouiaId={ouiaId} columns={columns} rows={pageRows} />
73+
<DataViewToolbar
74+
ouiaId='LayoutExampleFooter'
75+
pagination={
76+
<Pagination
77+
isCompact
78+
perPageOptions={perPageOptions}
79+
itemCount={repositories.length}
80+
{...pagination}
81+
/>
82+
}
83+
/>
84+
</DataView>
85+
);
86+
}
87+
88+
export const BasicExample: React.FunctionComponent = () => (
89+
<BrowserRouter>
90+
<MyTable/>
91+
</BrowserRouter>
92+
)

packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ sortValue: 3
1414
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
1515
---
1616
import { useMemo } from 'react';
17-
import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
17+
import { BrowserRouter, useSearchParams } from 'react-router-dom';
18+
import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
1819
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
1920
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
2021
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
2122
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
22-
import { BrowserRouter, useSearchParams } from 'react-router-dom';
23+
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
24+
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
2325

2426
This is a list of functionality you can use to manage data displayed in the **data view**.
2527

@@ -84,3 +86,32 @@ The `useDataViewSelection` hook manages the selection state of the data view.
8486
```js file="./SelectionExample.tsx"
8587

8688
```
89+
90+
# Filters
91+
Enables filtering of data records in the data view and displays the applied filter chips.
92+
93+
### Toolbar usage
94+
The data view toolbar can include a set of filters by passing a React node to the `filters` property. You can use predefined components `DataViewFilters` and `DataViewTextFilter` to customize and handle filtering directly in the toolbar. The `DataViewFilters` is a wrapper allowing conditional filtering using multiple attributes. If you need just a single filter, you can use `DataViewTextFilter` or a different filter component alone.
95+
96+
### Filters state
97+
98+
The `useDataViewFilters` hook manages the filter state of the data view. It allows you to define default filter values, synchronize filter state with URL parameters, and handle filter changes efficiently.
99+
100+
**Initial values:**
101+
- `initialFilters` object with default filter values
102+
- optional `searchParams` object for managing URL-based filter state
103+
- optional `setSearchParams` function to update the URL when filters are modified
104+
105+
The `useDataViewFilters` hook works well with the React Router library to support URL-based filtering. Alternatively, you can manage filter state in the URL using `URLSearchParams` and `window.history.pushState` APIs, or other routing libraries. If no URL parameters are provided, the filter state is managed internally.
106+
107+
**Return values:**
108+
- `filters` object representing the current filter values
109+
- `onSetFilters` function to update the filter state
110+
- `clearAllFilters` function to reset all filters to their initial values
111+
112+
### Filtering example
113+
This example demonstrates the setup and usage of filters within the data view. It includes text filters for different attributes, the ability to clear all filters, and persistence of filter state in the URL.
114+
115+
```js file="./FiltersExample.tsx"
116+
117+
```

packages/module/src/DataView/DataView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Stack, StackItem } from '@patternfly/react-core';
2+
import { Stack, StackItem, StackProps } from '@patternfly/react-core';
33
import { DataViewSelection, InternalContextProvider } from '../InternalContext';
44

55
export const DataViewState = {
@@ -10,7 +10,8 @@ export const DataViewState = {
1010

1111
export type DataViewState = typeof DataViewState[keyof typeof DataViewState];
1212

13-
export interface DataViewProps {
13+
/** extends StackProps */
14+
export interface DataViewProps extends StackProps {
1415
/** Content rendered inside the data view */
1516
children: React.ReactNode;
1617
/** Custom OUIA ID */
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import DataViewFilters from './DataViewFilters';
4+
import DataViewToolbar from '../DataViewToolbar';
5+
import DataViewTextFilter from '../DataViewTextFilter';
6+
7+
describe('DataViewFilters component', () => {
8+
const mockOnChange = jest.fn();
9+
10+
it('should render correctly', () => {
11+
const { container } = render(<DataViewToolbar
12+
filters={
13+
<DataViewFilters onChange={mockOnChange} values={{}}>
14+
<DataViewTextFilter filterId="one" title="One" />
15+
<DataViewTextFilter filterId="two" title="Two" />
16+
</DataViewFilters>
17+
}
18+
/>);
19+
expect(container).toMatchSnapshot();
20+
});
21+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, { useMemo, useState, useRef, useEffect, ReactElement } from 'react';
2+
import {
3+
Menu, MenuContent, MenuItem, MenuList, MenuToggle, Popper, ToolbarGroup, ToolbarToggleGroup, ToolbarToggleGroupProps,
4+
} from '@patternfly/react-core';
5+
import { FilterIcon } from '@patternfly/react-icons';
6+
7+
// helper interface to generate attribute menu
8+
interface DataViewFilterIdentifiers {
9+
filterId: string;
10+
title: string;
11+
}
12+
13+
/** extends ToolbarToggleGroupProps */
14+
export interface DataViewFiltersProps<T extends object> extends Omit<ToolbarToggleGroupProps, 'toggleIcon' | 'breakpoint' | 'onChange'> {
15+
/** Content rendered inside the data view */
16+
children: React.ReactNode;
17+
/** Optional onChange callback shared across filters */
18+
onChange?: (key: string, newValues: Partial<T>) => void;
19+
/** Optional values shared across filters */
20+
values?: T;
21+
/** Icon for the toolbar toggle group */
22+
toggleIcon?: ToolbarToggleGroupProps['toggleIcon'];
23+
/** Breakpoint for the toolbar toggle group */
24+
breakpoint?: ToolbarToggleGroupProps['breakpoint'];
25+
/** Custom OUIA ID */
26+
ouiaId?: string;
27+
};
28+
29+
30+
export const DataViewFilters = <T extends object>({
31+
children,
32+
ouiaId = 'DataViewFilters',
33+
toggleIcon = <FilterIcon />,
34+
breakpoint = 'xl',
35+
onChange,
36+
values,
37+
...props
38+
}: DataViewFiltersProps<T>) => {
39+
const [ activeAttributeMenu, setActiveAttributeMenu ] = useState<string>('');
40+
const [ isAttributeMenuOpen, setIsAttributeMenuOpen ] = useState(false);
41+
const attributeToggleRef = useRef<HTMLButtonElement>(null);
42+
const attributeMenuRef = useRef<HTMLDivElement>(null);
43+
const attributeContainerRef = useRef<HTMLDivElement>(null);
44+
45+
const filterItems: DataViewFilterIdentifiers[] = useMemo(() => React.Children.toArray(children)
46+
.map(child =>
47+
React.isValidElement(child) ? { filterId: String(child.props.filterId), title: String(child.props.title) } : undefined
48+
).filter((item): item is DataViewFilterIdentifiers => !!item), []); // eslint-disable-line react-hooks/exhaustive-deps
49+
50+
useEffect(() => {
51+
filterItems.length > 0 && setActiveAttributeMenu(filterItems[0].title);
52+
}, [ filterItems ]);
53+
54+
const attributeToggle = (
55+
<MenuToggle
56+
ref={attributeToggleRef}
57+
onClick={() => setIsAttributeMenuOpen(!isAttributeMenuOpen)}
58+
isExpanded={isAttributeMenuOpen}
59+
icon={toggleIcon}
60+
>
61+
{activeAttributeMenu}
62+
</MenuToggle>
63+
);
64+
65+
const attributeMenu = (
66+
<Menu
67+
ref={attributeMenuRef}
68+
onSelect={(_ev, itemId) => {
69+
const selectedItem = filterItems.find(item => item.filterId === itemId);
70+
selectedItem && setActiveAttributeMenu(selectedItem.title);
71+
setIsAttributeMenuOpen(false);
72+
}}
73+
>
74+
<MenuContent>
75+
<MenuList>
76+
{filterItems.map(item => (
77+
<MenuItem key={item.filterId} itemId={item.filterId}>
78+
{item.title}
79+
</MenuItem>
80+
))}
81+
</MenuList>
82+
</MenuContent>
83+
</Menu>
84+
);
85+
86+
return (
87+
<ToolbarToggleGroup data-ouia-component-id={ouiaId} toggleIcon={toggleIcon} breakpoint={breakpoint} {...props}>
88+
<ToolbarGroup variant="filter-group">
89+
<div ref={attributeContainerRef}>
90+
<Popper
91+
trigger={attributeToggle}
92+
triggerRef={attributeToggleRef}
93+
popper={attributeMenu}
94+
popperRef={attributeMenuRef}
95+
appendTo={attributeContainerRef.current || undefined}
96+
isVisible={isAttributeMenuOpen}
97+
/>
98+
</div>
99+
{React.Children.map(children, (child) => (
100+
React.isValidElement(child) ? (
101+
React.cloneElement(child as ReactElement<{
102+
showToolbarItem: boolean;
103+
onChange: (_e: unknown, values: unknown) => void;
104+
value: unknown;
105+
}>, {
106+
showToolbarItem: activeAttributeMenu === child.props.title,
107+
onChange: (event, value) => onChange?.(event, { [child.props.filterId]: value } as Partial<T>),
108+
value: values?.[child.props.filterId],
109+
...child.props
110+
})
111+
) : child
112+
))}
113+
114+
</ToolbarGroup>
115+
</ToolbarToggleGroup>
116+
);
117+
};
118+
119+
export default DataViewFilters;

0 commit comments

Comments
 (0)