Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataViews: add filters to table columns #55508

Merged
merged 20 commits into from Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
175 changes: 174 additions & 1 deletion packages/edit-site/src/components/dataviews/view-list.js
Expand Up @@ -21,6 +21,8 @@ import {
check,
arrowUp,
arrowDown,
chevronRightSmall,
funnel,
} from '@wordpress/icons';
import {
Button,
Expand All @@ -41,6 +43,8 @@ const {
DropdownMenuGroupV2,
DropdownMenuItemV2,
DropdownMenuSeparatorV2,
DropdownSubMenuV2,
DropdownSubMenuTriggerV2,
} = unlock( componentsPrivateApis );

const EMPTY_OBJECT = {};
Expand All @@ -63,6 +67,29 @@ function HeaderMenu( { dataView, header } ) {
return text;
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a small non blocking remark: This component HeaderMenu is built as a tan stack component (gets dataView and header) and renders a dropdown. I wonder if it could be similar to built it as a DataView component instead. (get the view and field as props).

Copy link
Member Author

Choose a reason for hiding this comment

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

I had the same thought #55508 (comment)

}
const sortedDirection = header.column.getIsSorted();

let filter;
if (
header.column.columnDef.filters?.length > 0 &&
header.column.columnDef.filters.some(
( f ) =>
( 'string' === typeof f && f === 'enumeration' ) ||
( 'object' === typeof f && f.type === 'enumeration' )
)
) {
filter = {
id: header.column.columnDef.id,
elements: [
{
value: '',
label: __( 'All' ),
},
...( header.column.columnDef.elements || [] ),
],
};
}
const isFilterable = !! filter;

return (
<DropdownMenuV2
align="start"
Expand Down Expand Up @@ -119,6 +146,92 @@ function HeaderMenu( { dataView, header } ) {
{ __( 'Hide' ) }
</DropdownMenuItemV2>
) }
{ isFilterable && (
<DropdownMenuGroupV2>
<DropdownSubMenuV2
key={ filter.id }
trigger={
<DropdownSubMenuTriggerV2
prefix={ <Icon icon={ funnel } /> }
suffix={
<Icon icon={ chevronRightSmall } />
}
>
{ __( 'Filter by' ) }
</DropdownSubMenuTriggerV2>
}
>
{ filter.elements.map( ( element ) => {
let isActive = false;
const columnFilters =
dataView.getState().columnFilters;
const columnFilter = columnFilters.find(
( f ) =>
Object.keys( f )[ 0 ].split(
':'
)[ 0 ] === filter.id
);

// Set the empty item as active if the filter is not set.
if ( ! columnFilter && element.value === '' ) {
isActive = true;
}

if ( columnFilter ) {
const value =
Object.values( columnFilter )[ 0 ];
// Intentionally use loose comparison, so it does type conversion.
// This covers the case where a top-level filter for the same field converts a number into a string.
isActive = element.value == value; // eslint-disable-line eqeqeq
}

return (
<DropdownMenuItemV2
key={ element.value }
suffix={
isActive && <Icon icon={ check } />
}
onSelect={ () => {
const otherFilters =
columnFilters?.filter(
( f ) => {
const [
field,
operator,
] =
Object.keys(
f
)[ 0 ].split( ':' );
return (
field !==
filter.id ||
operator !== 'in'
);
}
);

if ( element.value === '' ) {
dataView.setColumnFilters(
otherFilters
);
} else {
dataView.setColumnFilters( [
...otherFilters,
{
[ filter.id + ':in' ]:
element.value,
},
] );
}
} }
>
{ element.label }
</DropdownMenuItemV2>
);
} ) }
</DropdownSubMenuV2>
</DropdownMenuGroupV2>
) }
</WithSeparators>
</DropdownMenuV2>
);
Expand Down Expand Up @@ -186,6 +299,58 @@ function ViewList( {
);
}, [ view.hiddenFields ] );

/**
* Transform the filters from the view format into the tanstack columns filter format.
*
* Input:
*
* view.filters = [
* { field: 'date', operator: 'before', value: '2020-01-01' },
* { field: 'date', operator: 'after', value: '2020-01-01' },
* ]
*
* Output:
*
* columnFilters = [
* { "date:before": '2020-01-01' },
* { "date:after": '2020-01-01' }
* ]
*
* @param {Array} filters The view filters to transform.
* @return {Array} The transformed TanStack column filters.
*/
const toTanStackColumnFilters = ( filters ) =>
filters.map( ( filter ) => ( {
[ filter.field + ':' + filter.operator ]: filter.value,
} ) );

/**
* Transform the filters from the view format into the tanstack columns filter format.
*
* Input:
*
* columnFilters = [
* { "date:before": '2020-01-01'},
* { "date:after": '2020-01-01' }
* ]
*
* Output:
*
* view.filters = [
* { field: 'date', operator: 'before', value: '2020-01-01' },
* { field: 'date', operator: 'after', value: '2020-01-01' },
* ]
*
* @param {Array} filters The TanStack column filters to transform.
* @return {Array} The transformed view filters.
*/
const fromTanStackColumnFilters = ( filters ) =>
filters.map( ( filter ) => {
const [ key, value ] = Object.entries( filter )[ 0 ];
const [ field, operator ] = key.split( ':' );
return { field, operator, value };
} );

const dataView = useReactTable( {
data,
columns,
Expand All @@ -203,6 +368,7 @@ function ViewList( {
]
: [],
globalFilter: view.search,
columnFilters: toTanStackColumnFilters( view.filters ),
pagination: {
pageIndex: view.page,
pageSize: view.perPage,
Expand Down Expand Up @@ -261,7 +427,14 @@ function ViewList( {
} );
},
onGlobalFilterChange: ( value ) => {
onChangeView( { ...view, search: value, page: 0 } );
onChangeView( { ...view, search: value, page: 1 } );
Copy link
Member Author

Choose a reason for hiding this comment

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

Apparently, this value is not being used – or is ignored once the own Search component updates it to 1 here. I thought I'd change it anyway, though this needs a separate further investigation.

},
onColumnFiltersChange: ( columnFiltersUpdater ) => {
onChangeView( {
...view,
filters: fromTanStackColumnFilters( columnFiltersUpdater() ),
page: 1,
} );
},
onPaginationChange: ( paginationUpdater ) => {
onChangeView( ( currentView ) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/icons/CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New features

- Add new `funnel` icon.

## 9.36.0 (2023-11-02)

## 9.35.0 (2023-10-18)
Expand Down
1 change: 1 addition & 0 deletions packages/icons/src/index.js
Expand Up @@ -95,6 +95,7 @@ export { default as formatStrikethrough } from './library/format-strikethrough';
export { default as formatUnderline } from './library/format-underline';
export { default as formatUppercase } from './library/format-uppercase';
export { default as fullscreen } from './library/fullscreen';
export { default as funnel } from './library/funnel';
export { default as gallery } from './library/gallery';
export { default as globe } from './library/globe';
export { default as grid } from './library/grid';
Expand Down
12 changes: 12 additions & 0 deletions packages/icons/src/library/funnel.js
@@ -0,0 +1,12 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/primitives';

const funnel = (
<SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path d="M10 17.5H14V16H10V17.5ZM6 6V7.5H18V6H6ZM8 12.5H16V11H8V12.5Z" />
</SVG>
);

export default funnel;