Skip to content

Commit

Permalink
DataViews: implement multiple selection for filters (#59610)
Browse files Browse the repository at this point in the history
Co-authored-by: oandregal <[email protected]>
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: jorgefilipecosta <[email protected]>
Co-authored-by: andrewhayward <[email protected]>
  • Loading branch information
6 people authored Mar 11, 2024
1 parent 423c71f commit 8c7e6be
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 90 deletions.
4 changes: 4 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancement

- DataViews now supports multi-selection. A new set of filter operators has been introduced: `is`, `isNot`, `isAny`, `isNone`. Single-selection operators are `is` and `isNot`, and multi-selection operators are `isAny` and `isNone`. If no operators are declared for a filter, it will support multi-selection. Additionally, the old filter operators `in` and `notIn` operators have been deprecated and will work as `is` and `isNot` respectively. Please, migrate to the new operators as they'll be removed soon.

## 0.7.0 (2024-03-06)

## 0.6.0 (2024-02-21)
Expand Down
54 changes: 43 additions & 11 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ The fields describe the visible items for each record in the dataset.
Example:

```js
const STATUSES = [
{ value: 'draft', label: __( 'Draft' ) },
{ value: 'future', label: __( 'Scheduled' ) },
{ value: 'pending', label: __( 'Pending Review' ) },
{ value: 'private', label: __( 'Private' ) },
{ value: 'publish', label: __( 'Published' ) },
{ value: 'trash', label: __( 'Trash' ) },
];
const fields = [
{
id: 'title',
Expand Down Expand Up @@ -89,9 +97,25 @@ const fields = [
elements: [
{ value: 1, label: 'Admin' }
{ value: 2, label: 'User' }
]
],
filterBy: {
operators: [ 'is', 'isNot' ]
},
enableSorting: false
}
},
{
header: __( 'Status' ),
id: 'status',
getValue: ( { item } ) =>
STATUSES.find( ( { value } ) => value === item.status )
?.label ?? item.status,
type: 'enumeration',
elements: STATUSES,
filterBy: {
operators: [ 'isAny' ],
},
enableSorting: false,
},
]
```

Expand Down Expand Up @@ -120,8 +144,8 @@ const view = {
type: 'table',
search: '',
filters: [
{ field: 'author', operator: 'in', value: 2 },
{ field: 'status', operator: 'in', value: 'publish,draft' }
{ field: 'author', operator: 'is', value: 2 },
{ field: 'status', operator: 'isAny', value: [ 'publish', 'draft'] }
],
page: 1,
perPage: 5,
Expand All @@ -140,7 +164,7 @@ Properties:
- `search`: the text search applied to the dataset.
- `filters`: the filters applied to the dataset. Each item describes:
- `field`: which field this filter is bound to.
- `operator`: which type of filter it is. One of `in`, `notIn`. See "Operator types".
- `operator`: which type of filter it is. See "Operator types".
- `value`: the actual value selected by the user.
- `perPage`: number of records to show per page.
- `page`: the page that is visible.
Expand Down Expand Up @@ -172,8 +196,8 @@ function MyCustomPageTable() {
},
search: '',
filters: [
{ field: 'author', operator: 'in', value: 2 },
{ field: 'status', operator: 'in', value: 'publish,draft' }
{ field: 'author', operator: 'is', value: 2 },
{ field: 'status', operator: 'isAny', value: [ 'publish', 'draft' ] }
],
hiddenFields: [ 'date', 'featured-image' ],
layout: {},
Expand All @@ -182,10 +206,10 @@ function MyCustomPageTable() {
const queryArgs = useMemo( () => {
const filters = {};
view.filters.forEach( ( filter ) => {
if ( filter.field === 'status' && filter.operator === 'in' ) {
if ( filter.field === 'status' && filter.operator === 'isAny' ) {
filters.status = filter.value;
}
if ( filter.field === 'author' && filter.operator === 'in' ) {
if ( filter.field === 'author' && filter.operator === 'is' ) {
filters.author = filter.value;
}
} );
Expand Down Expand Up @@ -282,8 +306,16 @@ Callback that signals the user triggered the details for one of more items, and

### Operators

- `in`: operator to be used in filters for fields of type `enumeration`.
- `notIn`: operator to be used in filters for fields of type `enumeration`.
Allowed operators for fields of type `enumeration`:

- `is`: whether the item is equal to a single value.
- `isNot`: whether the item is not equal to a single value.
- `isAny`: whether the item is present in a list of values.
- `isNone`: whether the item is not present in a list of values.

`is` and `isNot` are single-selection operators, while `isAny` and `isNone` are multi-selection. By default, a filter with no operators declared will support multi-selection. A filter cannot mix single-selection & multi-selection operators; if a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded and the filter won't allow selecting more than one item.

> The legacy operators `in` and `notIn` have been deprecated and will be removed soon. In the meantime, they work as `is` and `isNot` operators, respectively.
## Contributing to this package

Expand Down
28 changes: 22 additions & 6 deletions packages/dataviews/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,33 @@ import ViewList from './view-list';
export const ENUMERATION_TYPE = 'enumeration';

// Filter operators.
export const OPERATOR_IN = 'in';
export const OPERATOR_NOT_IN = 'notIn';
export const OPERATOR_IS = 'is';
export const OPERATOR_IS_NOT = 'isNot';
export const OPERATOR_IS_ANY = 'isAny';
export const OPERATOR_IS_NONE = 'isNone';
export const ALL_OPERATORS = [
OPERATOR_IS,
OPERATOR_IS_NOT,
OPERATOR_IS_ANY,
OPERATOR_IS_NONE,
];
export const OPERATORS = {
[ OPERATOR_IN ]: {
key: 'in-filter',
[ OPERATOR_IS ]: {
key: 'is-filter',
label: __( 'Is' ),
},
[ OPERATOR_NOT_IN ]: {
key: 'not-in-filter',
[ OPERATOR_IS_NOT ]: {
key: 'is-not-filter',
label: __( 'Is not' ),
},
[ OPERATOR_IS_ANY ]: {
key: 'is-any-filter',
label: __( 'Is any' ),
},
[ OPERATOR_IS_NONE ]: {
key: 'is-none-filter',
label: __( 'Is none' ),
},
};

// Sorting
Expand Down
73 changes: 50 additions & 23 deletions packages/dataviews/src/filter-summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,67 @@ import { ENTER, SPACE } from '@wordpress/keycodes';
* Internal dependencies
*/
import SearchWidget from './search-widget';
import { OPERATOR_IN, OPERATOR_NOT_IN, OPERATORS } from './constants';
import {
OPERATORS,
OPERATOR_IS,
OPERATOR_IS_NOT,
OPERATOR_IS_ANY,
OPERATOR_IS_NONE,
} from './constants';

const FilterText = ( { activeElement, filterInView, filter } ) => {
if ( activeElement === undefined ) {
const FilterText = ( { activeElements, filterInView, filter } ) => {
if ( activeElements === undefined || activeElements.length === 0 ) {
return filter.name;
}

const filterTextWrappers = {
Span1: <span className="dataviews-filter-summary__filter-text-name" />,
Span2: <span className="dataviews-filter-summary__filter-text-value" />,
Name: <span className="dataviews-filter-summary__filter-text-name" />,
Value: <span className="dataviews-filter-summary__filter-text-value" />,
};

if (
activeElement !== undefined &&
filterInView?.operator === OPERATOR_IN
) {
if ( filterInView?.operator === OPERATOR_IS_ANY ) {
return createInterpolateElement(
sprintf(
/* translators: 1: Filter name. 3: Filter value. e.g.: "Author is any: Admin, Editor". */
__( '<Name>%1$s is any: </Name><Value>%2$s</Value>' ),
filter.name,
activeElements.map( ( element ) => element.label ).join( ', ' )
),
filterTextWrappers
);
}

if ( filterInView?.operator === OPERATOR_IS_NONE ) {
return createInterpolateElement(
sprintf(
/* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */
__( '<Span1>%1$s </Span1><Span2>is %2$s</Span2>' ),
/* translators: 1: Filter name. 3: Filter value. e.g.: "Author is none: Admin, Editor". */
__( '<Name>%1$s is none: </Name><Value>%2$s</Value>' ),
filter.name,
activeElement.label
activeElements.map( ( element ) => element.label ).join( ', ' )
),
filterTextWrappers
);
}

if (
activeElement !== undefined &&
filterInView?.operator === OPERATOR_NOT_IN
) {
if ( filterInView?.operator === OPERATOR_IS ) {
return createInterpolateElement(
sprintf(
/* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */
__( '<Span1>%1$s </Span1><Span2>is not %2$s</Span2>' ),
/* translators: 1: Filter name. 3: Filter value. e.g.: "Author is: Admin". */
__( '<Name>%1$s is: </Name><Value>%2$s</Value>' ),
filter.name,
activeElement.label
activeElements[ 0 ].label
),
filterTextWrappers
);
}

if ( filterInView?.operator === OPERATOR_IS_NOT ) {
return createInterpolateElement(
sprintf(
/* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not: Admin". */
__( '<Name>%1$s is not: </Name><Value>%2$s</Value>' ),
filter.name,
activeElements[ 0 ].label
),
filterTextWrappers
);
Expand Down Expand Up @@ -140,9 +164,12 @@ export default function FilterSummary( {
const toggleRef = useRef();
const { filter, view, onChangeView } = commonProps;
const filterInView = view.filters.find( ( f ) => f.field === filter.field );
const activeElement = filter.elements.find(
( element ) => element.value === filterInView?.value
);
const activeElements = filter.elements.filter( ( element ) => {
if ( filter.singleSelection ) {
return element.value === filterInView?.value;
}
return filterInView?.value?.includes( element.value );
} );
const isPrimary = filter.isPrimary;
const hasValues = filterInView?.value !== undefined;
const canResetOrRemove = ! isPrimary || hasValues;
Expand Down Expand Up @@ -188,7 +215,7 @@ export default function FilterSummary( {
ref={ toggleRef }
>
<FilterText
activeElement={ activeElement }
activeElements={ activeElements }
filterInView={ filterInView }
filter={ filter }
/>
Expand Down
14 changes: 10 additions & 4 deletions packages/dataviews/src/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import FilterSummary from './filter-summary';
import AddFilter from './add-filter';
import ResetFilters from './reset-filters';
import { sanitizeOperators } from './utils';
import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants';
import {
ENUMERATION_TYPE,
ALL_OPERATORS,
OPERATOR_IS,
OPERATOR_IS_NOT,
} from './constants';
import { __experimentalHStack as HStack } from '@wordpress/components';

const Filters = memo( function Filters( {
Expand Down Expand Up @@ -43,15 +48,16 @@ const Filters = memo( function Filters( {
field: field.id,
name: field.header,
elements: field.elements,
singleSelection: operators.some( ( op ) =>
[ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op )
),
operators,
isVisible:
isPrimary ||
view.filters.some(
( f ) =>
f.field === field.id &&
[ OPERATOR_IN, OPERATOR_NOT_IN ].includes(
f.operator
)
ALL_OPERATORS.includes( f.operator )
),
isPrimary,
} );
Expand Down
Loading

0 comments on commit 8c7e6be

Please sign in to comment.