Skip to content

Commit

Permalink
Merge pull request #112 from fhlavac/cg
Browse files Browse the repository at this point in the history
Add conditional text filtering to DataView
  • Loading branch information
fhlavac authored Nov 6, 2024
2 parents e797073 + 7bfb9e5 commit 995b46b
Show file tree
Hide file tree
Showing 26 changed files with 1,189 additions and 37 deletions.
111 changes: 111 additions & 0 deletions cypress/component/DataViewFilters.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/esm/DataViewToolbar';
import { FilterIcon } from '@patternfly/react-icons';

const filtersProps = {
ouiaId: 'DataViewFilters',
toggleIcon: <FilterIcon />,
values: { name: '', branch: '' }
};

interface RepositoryFilters {
name: string,
branch: string
};

const DataViewToolbarWithState = (props: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '' } });

return (
<DataViewToolbar
ouiaId='FiltersExampleHeader'
clearAllFilters = {clearAllFilters}
filters={
<DataViewFilters {...filtersProps} onChange={(_e, values) => onSetFilters(values)} values={filters} {...props}>
<DataViewTextFilter filterId="name" title='Name' placeholder='Filter by name' />
<DataViewTextFilter filterId="branch" title='Branch' placeholder='Filter by branch' />
</DataViewFilters>
}
/>
);
};

describe('DataViewFilters', () => {
it('renders DataViewFilters with menu and filter items', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"]').should('exist');
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();

cy.contains('Name').should('exist');
cy.contains('Branch').should('exist');
});

it('can select a filter option', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"]').should('contain.text', 'Name');
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Branch').click();

cy.get('[data-ouia-component-id="DataViewFilters"]').should('contain.text', 'Branch');
});

it('responds to input and clears the filters', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Name').click();

cy.get('input[placeholder="Filter by name"]').type('Repository one');
cy.get('.pf-v5-c-chip__text').should('have.length', 1);
cy.get('input[placeholder="Filter by name"]').clear();
cy.get('.pf-v5-c-chip__text').should('have.length', 0);
});

it('displays chips for selected filters', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Name').click();
cy.get('input[placeholder="Filter by name"]').type('Repository one');

cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Branch').click();
cy.get('input[placeholder="Filter by branch"]').type('Main branch');

cy.get('.pf-v5-c-chip__text').should('have.length', 2);
cy.get('.pf-v5-c-chip__text').eq(0).should('contain.text', 'Repository one');
cy.get('.pf-v5-c-chip__text').eq(1).should('contain.text', 'Main branch');
});

it('removes filters by clicking individual chips', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Name').click();
cy.get('input[placeholder="Filter by name"]').type('Repository one');

cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Branch').click();
cy.get('input[placeholder="Filter by branch"]').type('Main branch');

cy.get('[data-ouia-component-id="close"]').should('have.length', 2);

cy.get('[data-ouia-component-id="close"]').first().click();
cy.get('[data-ouia-component-id="close"]').last().click();

cy.get('[data-ouia-component-id="close"]').should('have.length', 0);
});

it('clears all filters using the clear-all button', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Name').click();
cy.get('input[placeholder="Filter by name"]').type('Repository one');

cy.get('[data-ouia-component-id="DataViewFilters"] .pf-v5-c-menu-toggle').click();
cy.contains('Branch').click();
cy.get('input[placeholder="Filter by branch"]').type('Main branch');

cy.get('[data-ouia-component-id="FiltersExampleHeader-clear-all-filters"]').should('exist').click();
});
});
75 changes: 75 additions & 0 deletions cypress/component/DataViewTextFilter.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';

const defaultProps = {
filterId: 'name',
title: 'Name',
value: '',
ouiaId: 'DataViewTextFilter',
placeholder: 'Filter by name'
};

const DataViewToolbarWithState = (props: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
const [ value, setValue ] = useState('Repository one');

return (
<DataViewToolbar clearAllFilters={() => setValue('')}>
<DataViewTextFilter {...defaultProps} value={value} onChange={() => setValue('')} {...props} />
</DataViewToolbar>
);
};

describe('DataViewTextFilter', () => {

it('renders DataViewTextFilter with correct initial values', () => {
cy.mount(<DataViewToolbarWithState value="" />);
cy.get('[data-ouia-component-id="DataViewTextFilter"]').should('exist');
cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input')
.should('have.attr', 'placeholder', 'Filter by name')
.and('have.value', '');
});

it('accepts input when passed', () => {
cy.mount(<DataViewToolbarWithState value="" />);
cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input')
.type('Repository one')
.should('have.value', 'Repository one');
});

it('displays a chip when value is present and removes it on delete', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input').should('have.value', 'Repository one');

cy.get('.pf-v5-c-chip__text').contains('Repository one');
cy.get('.pf-m-chip-group button.pf-v5-c-button.pf-m-plain').click();

cy.get('.pf-v5-c-chip__text').should('not.exist');
cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input').should('have.value', '');
});

it('clears input when the clear button is clicked', () => {
cy.mount(<DataViewToolbarWithState />);
cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input').should('have.value', 'Repository one');

cy.get('[data-ouia-component-id="DataViewToolbar-clear-all-filters"]').click();

cy.get('[data-ouia-component-id="DataViewTextFilter-input"] input').should('have.value', '');
});

it('hides or shows the toolbar item based on showToolbarItem prop', () => {
cy.mount(
<DataViewToolbar>
<DataViewTextFilter {...defaultProps} showToolbarItem={false} />
</DataViewToolbar>
);
cy.get('[data-ouia-component-id="DataViewTextFilter"]').should('not.exist');

cy.mount(
<DataViewToolbar>
<DataViewTextFilter {...defaultProps} showToolbarItem />
</DataViewToolbar>
);
cy.get('[data-ouia-component-id="DataViewTextFilter"]').should('exist');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
sortValue: 4
propComponents: ['DataViewToolbar', 'DataViewTableBasic', 'DataViewTableTree']
propComponents: ['DataViewToolbar', 'DataViewTableBasic', 'DataViewTableTree', 'DataViewTrTree', 'DataViewTrObject']
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md
---
import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon } from '@patternfly/react-core';
Expand All @@ -26,7 +26,7 @@ import { DataView, DataViewState } from '@patternfly/react-data-view/dist/dynami

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

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.
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.

### Basic toolbar example

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useMemo } from 'react';
import { Pagination } from '@patternfly/react-core';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';

const perPageOptions = [
{ title: '5', value: 5 },
{ title: '10', value: 10 }
];

interface Repository {
name: string;
branch: string | null;
prs: string | null;
workspaces: string;
lastCommit: string;
}

interface RepositoryFilters {
name: string,
branch: string
}

const repositories: Repository[] = [
{ name: 'Repository one', branch: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
{ name: 'Repository two', branch: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
{ name: 'Repository three', branch: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
{ name: 'Repository four', branch: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
{ name: 'Repository five', branch: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
{ name: 'Repository six', branch: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
];

const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspaces', 'Last commit' ];

const ouiaId = 'LayoutExample';

const MyTable: React.FunctionComponent = () => {
const [ searchParams, setSearchParams ] = useSearchParams();
const pagination = useDataViewPagination({ perPage: 5 });
const { page, perPage } = pagination;
const { filters, onSetFilters, clearAllFilters } = useDataViewFilters<RepositoryFilters>({ initialFilters: { name: '', branch: '' }, searchParams, setSearchParams });

const pageRows = useMemo(() => repositories
.filter(item => (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) && (!filters.branch || item.branch?.toLocaleLowerCase().includes(filters.branch?.toLocaleLowerCase())))
.slice((page - 1) * perPage, ((page - 1) * perPage) + perPage)
.map(item => Object.values(item)), [ page, perPage, filters ]);

return (
<DataView>
<DataViewToolbar
ouiaId='LayoutExampleHeader'
clearAllFilters = {clearAllFilters}
pagination={
<Pagination
perPageOptions={perPageOptions}
itemCount={repositories.length}
{...pagination}
/>
}
filters={
<DataViewFilters onChange={(_e, values) => onSetFilters(values)} values={filters}>
<DataViewTextFilter filterId="name" title='Name' placeholder='Filter by name' />
<DataViewTextFilter filterId="branch" title='Branch' placeholder='Filter by branch' />
</DataViewFilters>
}
/>
<DataViewTable aria-label='Repositories table' ouiaId={ouiaId} columns={columns} rows={pageRows} />
<DataViewToolbar
ouiaId='LayoutExampleFooter'
pagination={
<Pagination
isCompact
perPageOptions={perPageOptions}
itemCount={repositories.length}
{...pagination}
/>
}
/>
</DataView>
);
}

export const BasicExample: React.FunctionComponent = () => (
<BrowserRouter>
<MyTable/>
</BrowserRouter>
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
sortValue: 3
propComponents: ['DataViewFilters', 'DataViewTextFilter']
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
---
import { useMemo } from 'react';
import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';

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

Expand Down Expand Up @@ -84,3 +87,34 @@ The `useDataViewSelection` hook manages the selection state of the data view.
```js file="./SelectionExample.tsx"

```

# Filters
Enables filtering of data records in the data view and displays the applied filter chips.

### Toolbar usage
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. Props of these filter components are listed at the bottom of this page.

You can decide between passing `value` and `onChange` event to every filter separately or pass `values` and `onChange` to the `DataViewFilters` wrapper which make them available to its children. Props directly passed to child filters have a higher priority than the "inherited" ones.

### Filters state

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.

**Initial values:**
- `initialFilters` object with default filter values
- optional `searchParams` object for managing URL-based filter state
- optional `setSearchParams` function to update the URL when filters are modified

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.

**Return values:**
- `filters` object representing the current filter values
- `onSetFilters` function to update the filter state
- `clearAllFilters` function to reset all filters to their initial values

### Filtering example
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.

```js file="./FiltersExample.tsx"

```
21 changes: 21 additions & 0 deletions packages/module/src/DataViewFilters/DataViewFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { render } from '@testing-library/react';
import DataViewFilters from './DataViewFilters';
import DataViewToolbar from '../DataViewToolbar';
import DataViewTextFilter from '../DataViewTextFilter';

describe('DataViewFilters component', () => {
const mockOnChange = jest.fn();

it('should render correctly', () => {
const { container } = render(<DataViewToolbar
filters={
<DataViewFilters onChange={mockOnChange} values={{}}>
<DataViewTextFilter filterId="one" title="One" />
<DataViewTextFilter filterId="two" title="Two" />
</DataViewFilters>
}
/>);
expect(container).toMatchSnapshot();
});
});
Loading

0 comments on commit 995b46b

Please sign in to comment.