From 9e792ad7fd98265a860ed1f30070df9beedead77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:17:05 +0100 Subject: [PATCH 01/24] Implement multiple selection in filters --- packages/dataviews/src/filter-summary.js | 29 ++++---- packages/dataviews/src/filters.js | 1 + packages/dataviews/src/search-widget.js | 71 +++++++++++++++---- .../src/components/page-pages/index.js | 3 + 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 6e1e3e9e2620b..67571d7dd9535 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -26,8 +26,8 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; import SearchWidget from './search-widget'; import { OPERATOR_IN, OPERATOR_NOT_IN, OPERATORS } from './constants'; -const FilterText = ( { activeElement, filterInView, filter } ) => { - if ( activeElement === undefined ) { +const FilterText = ( { activeElements, filterInView, filter } ) => { + if ( activeElements === undefined || activeElements.length === 0 ) { return filter.name; } @@ -36,31 +36,25 @@ const FilterText = ( { activeElement, filterInView, filter } ) => { Span2: , }; - if ( - activeElement !== undefined && - filterInView?.operator === OPERATOR_IN - ) { + if ( filterInView?.operator === OPERATOR_IN ) { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */ __( '%1$s is %2$s' ), filter.name, - activeElement.label + activeElements.map( ( element ) => element.label ).join( ', ' ) ), filterTextWrappers ); } - if ( - activeElement !== undefined && - filterInView?.operator === OPERATOR_NOT_IN - ) { + if ( filterInView?.operator === OPERATOR_NOT_IN ) { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */ __( '%1$s is not %2$s' ), filter.name, - activeElement.label + activeElements.map( ( element ) => element.label ).join( ', ' ) ), filterTextWrappers ); @@ -140,9 +134,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; @@ -188,7 +185,7 @@ export default function FilterSummary( { ref={ toggleRef } > diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index eb1bce3a42dc4..38eafd2e57675 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -43,6 +43,7 @@ const Filters = memo( function Filters( { field: field.id, name: field.header, elements: field.elements, + singleSelection: !! field.filterBy?.singleSelection, operators, isVisible: isPrimary || diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js index f8b3e84fd8ba3..7ff0688dab471 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.js @@ -15,7 +15,7 @@ import { Icon, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { search } from '@wordpress/icons'; +import { search, check } from '@wordpress/icons'; import { SVG, Circle } from '@wordpress/primitives'; /** @@ -48,10 +48,25 @@ function ListBox( { view, filter, onChangeView } ) { // so the first item is not selected, since the focus is on the operators control. defaultActiveId: filter.operators?.length === 1 ? undefined : null, } ); - const selectedFilter = view.filters.find( - ( _filter ) => _filter.field === filter.field + const currentFilter = view.filters.find( + ( f ) => f.field === filter.field ); - const selectedValues = selectedFilter?.value; + const getSelectedValues = ( current ) => { + if ( filter.singleSelection ) { + return current?.value; + } + + if ( Array.isArray( current?.value ) ) { + return current.value; + } + + if ( ! Array.isArray( current?.value ) && !! current?.value ) { + return [ current.value ]; + } + + return []; + }; + const selectedValues = getSelectedValues( currentFilter ); return ( } onClick={ () => { - const currentFilter = view.filters.find( - ( _filter ) => - _filter.field === filter.field - ); + const getNewValue = ( + filterDefinition, + current, + value + ) => { + if ( filterDefinition.singleSelection ) { + return value; + } + + if ( Array.isArray( current?.value ) ) { + return current.value.includes( + element.value + ) + ? current.value.filter( + ( v ) => v !== value + ) + : [ ...current.value, value ]; + } + + return [ value ]; + }; const newFilters = currentFilter ? [ ...view.filters.map( @@ -101,7 +133,11 @@ function ListBox( { view, filter, onChangeView } ) { currentFilter.operator || filter .operators[ 0 ], - value: element.value, + value: getNewValue( + filter, + currentFilter, + element.value + ), }; } return _filter; @@ -113,7 +149,11 @@ function ListBox( { view, filter, onChangeView } ) { { field: filter.field, operator: filter.operators[ 0 ], - value: element.value, + value: getNewValue( + filter, + currentFilter, + element.value + ), }, ]; onChangeView( { @@ -126,9 +166,14 @@ function ListBox( { view, filter, onChangeView } ) { } > - { selectedValues === element.value && ( - - ) } + { filter.singleSelection && + selectedValues === element.value && ( + + ) } + { ! filter.singleSelection && + selectedValues.includes( element.value ) && ( + + ) } { element.label } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 73dd87eeab5a5..0a5be7bfe5683 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -320,6 +320,9 @@ export default function PagePages() { value: id, label: name, } ) ) || [], + filterBy: { + singleSelection: true, + }, }, { header: __( 'Status' ), From 0de9f370bed91957d69cf652a4061d09de6d4447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:23:11 +0100 Subject: [PATCH 02/24] Make Sync filter in pattern single selection and remove test from Pages --- packages/edit-site/src/components/page-pages/index.js | 3 --- packages/edit-site/src/components/page-patterns/index.js | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 0a5be7bfe5683..73dd87eeab5a5 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -320,9 +320,6 @@ export default function PagePages() { value: id, label: name, } ) ) || [], - filterBy: { - singleSelection: true, - }, }, { header: __( 'Status' ), diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 8ca10d2357e55..0ad237b0a10dc 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -325,6 +325,7 @@ export default function DataviewsPatterns() { filterBy: { operators: [ OPERATOR_IN ], isPrimary: true, + singleSelection: true, }, enableSorting: false, } ); From 44f305217fcce506087c1a9a6d80669be29cb17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:56:39 +0100 Subject: [PATCH 03/24] Document singleSelection --- packages/dataviews/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index bbc2271db6573..a774fec7f7138 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -108,6 +108,7 @@ Each field is an object with the following properties: - `filterBy`: configuration for the filters. - `operators`: the list of operators supported by the field. - `isPrimary`: whether it is a primary filter. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. + - `singleSelection`: whether the filter should only allow single selection. `false` by default. ### `view`: `object` From e6cc4b0a3e1a3fff8c01abec0daf410b9155f831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:18:28 +0100 Subject: [PATCH 04/24] Implement multiple selection for combobox --- packages/dataviews/src/search-widget.js | 102 ++++++++++++------------ 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js index 7ff0688dab471..ab80bded5bb90 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.js @@ -39,6 +39,36 @@ function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } +const getCurrentValue = ( filterDefinition, currentFilter ) => { + if ( filterDefinition.singleSelection ) { + return currentFilter?.value; + } + + if ( Array.isArray( currentFilter?.value ) ) { + return currentFilter.value; + } + + if ( ! Array.isArray( currentFilter?.value ) && !! currentFilter?.value ) { + return [ currentFilter.value ]; + } + + return []; +}; + +const getNewValue = ( filterDefinition, currentFilter, value ) => { + if ( filterDefinition.singleSelection ) { + return value; + } + + if ( Array.isArray( currentFilter?.value ) ) { + return currentFilter.value.includes( value ) + ? currentFilter.value.filter( ( v ) => v !== value ) + : [ ...currentFilter.value, value ]; + } + + return [ value ]; +}; + function ListBox( { view, filter, onChangeView } ) { const compositeStore = useCompositeStore( { virtualFocus: true, @@ -51,22 +81,7 @@ function ListBox( { view, filter, onChangeView } ) { const currentFilter = view.filters.find( ( f ) => f.field === filter.field ); - const getSelectedValues = ( current ) => { - if ( filter.singleSelection ) { - return current?.value; - } - - if ( Array.isArray( current?.value ) ) { - return current.value; - } - - if ( ! Array.isArray( current?.value ) && !! current?.value ) { - return [ current.value ]; - } - - return []; - }; - const selectedValues = getSelectedValues( currentFilter ); + const currentValue = getCurrentValue( filter, currentFilter ); return ( } onClick={ () => { - const getNewValue = ( - filterDefinition, - current, - value - ) => { - if ( filterDefinition.singleSelection ) { - return value; - } - - if ( Array.isArray( current?.value ) ) { - return current.value.includes( - element.value - ) - ? current.value.filter( - ( v ) => v !== value - ) - : [ ...current.value, value ]; - } - - return [ value ]; - }; const newFilters = currentFilter ? [ ...view.filters.map( @@ -167,11 +161,11 @@ function ListBox( { view, filter, onChangeView } ) { > { filter.singleSelection && - selectedValues === element.value && ( + currentValue === element.value && ( ) } { ! filter.singleSelection && - selectedValues.includes( element.value ) && ( + currentValue.includes( element.value ) && ( ) } @@ -192,10 +186,10 @@ function ListBox( { view, filter, onChangeView } ) { function ComboboxList( { view, filter, onChangeView } ) { const [ searchValue, setSearchValue ] = useState( '' ); const deferredSearchValue = useDeferredValue( searchValue ); - const selectedFilter = view.filters.find( + const currentFilter = view.filters.find( ( _filter ) => _filter.field === filter.field ); - const selectedValues = selectedFilter?.value; + const currentValue = getCurrentValue( filter, currentFilter ); const matches = useMemo( () => { const normalizedSearch = normalizeSearchInput( deferredSearchValue ); return filter.elements.filter( ( item ) => @@ -206,9 +200,6 @@ function ComboboxList( { view, filter, onChangeView } ) { { - const currentFilter = view.filters.find( - ( _filter ) => _filter.field === filter.field - ); const newFilters = currentFilter ? [ ...view.filters.map( ( _filter ) => { @@ -218,7 +209,11 @@ function ComboboxList( { view, filter, onChangeView } ) { operator: currentFilter.operator || filter.operators[ 0 ], - value, + value: getNewValue( + filter, + currentFilter, + value + ), }; } return _filter; @@ -229,7 +224,11 @@ function ComboboxList( { view, filter, onChangeView } ) { { field: filter.field, operator: filter.operators[ 0 ], - value, + value: getNewValue( + filter, + currentFilter, + value + ), }, ]; onChangeView( { @@ -268,9 +267,14 @@ function ComboboxList( { view, filter, onChangeView } ) { focusOnHover > - { selectedValues === element.value && ( - - ) } + { filter.singleSelection && + currentValue === element.value && ( + + ) } + { ! filter.singleSelection && + currentValue.includes( element.value ) && ( + + ) } Date: Wed, 6 Mar 2024 10:38:56 +0100 Subject: [PATCH 05/24] Fix Combobox multiselection --- packages/dataviews/src/search-widget.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.js index ab80bded5bb90..d6fcddf969b60 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.js @@ -199,6 +199,7 @@ function ComboboxList( { view, filter, onChangeView } ) { return ( { const newFilters = currentFilter ? [ @@ -209,11 +210,7 @@ function ComboboxList( { view, filter, onChangeView } ) { operator: currentFilter.operator || filter.operators[ 0 ], - value: getNewValue( - filter, - currentFilter, - value - ), + value, }; } return _filter; @@ -224,11 +221,7 @@ function ComboboxList( { view, filter, onChangeView } ) { { field: filter.field, operator: filter.operators[ 0 ], - value: getNewValue( - filter, - currentFilter, - value - ), + value, }, ]; onChangeView( { From 7ced562d274a8f4c5d2b39e441cec157590e48da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:35:15 +0100 Subject: [PATCH 06/24] Add OPERATOR_EQUAL and OPERATOR_NOT_EQUAL --- packages/dataviews/README.md | 11 +++++++--- packages/dataviews/src/constants.js | 18 +++++++++++++++- packages/dataviews/src/filter-summary.js | 21 +++++-------------- packages/dataviews/src/filters.js | 15 ++++++++----- packages/dataviews/src/utils.js | 6 +++--- .../src/components/page-patterns/index.js | 5 ++--- packages/edit-site/src/utils/constants.js | 2 ++ 7 files changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index a774fec7f7138..2f17802176017 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -108,7 +108,6 @@ Each field is an object with the following properties: - `filterBy`: configuration for the filters. - `operators`: the list of operators supported by the field. - `isPrimary`: whether it is a primary filter. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. - - `singleSelection`: whether the filter should only allow single selection. `false` by default. ### `view`: `object` @@ -283,8 +282,14 @@ 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`: + +- `equal`: whether an item is equal to a single value. +- `notEqual`: whether an item is not equal to a single value. +- `in`: whether an item is in a list of values. +- `notIn`: whether an item is not in a list of values. + +By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. ## Contributing to this package diff --git a/packages/dataviews/src/constants.js b/packages/dataviews/src/constants.js index b3d17d7fd1145..3d250d784d7e0 100644 --- a/packages/dataviews/src/constants.js +++ b/packages/dataviews/src/constants.js @@ -22,13 +22,29 @@ export const ENUMERATION_TYPE = 'enumeration'; // Filter operators. export const OPERATOR_IN = 'in'; export const OPERATOR_NOT_IN = 'notIn'; +export const OPERATOR_EQUAL = 'equal'; +export const OPERATOR_NOT_EQUAL = 'notEqual'; +export const ALL_OPERATORS = [ + OPERATOR_IN, + OPERATOR_NOT_IN, + OPERATOR_EQUAL, + OPERATOR_NOT_EQUAL, +]; export const OPERATORS = { [ OPERATOR_IN ]: { key: 'in-filter', - label: __( 'Is' ), + label: __( 'In' ), }, [ OPERATOR_NOT_IN ]: { key: 'not-in-filter', + label: __( 'Not in' ), + }, + [ OPERATOR_EQUAL ]: { + key: 'equal-filter', + label: __( 'Is' ), + }, + [ OPERATOR_NOT_EQUAL ]: { + key: 'not-equal-filter', label: __( 'Is not' ), }, }; diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 67571d7dd9535..b49794598c2b9 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -24,7 +24,7 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; * Internal dependencies */ import SearchWidget from './search-widget'; -import { OPERATOR_IN, OPERATOR_NOT_IN, OPERATORS } from './constants'; +import { ALL_OPERATORS, OPERATORS } from './constants'; const FilterText = ( { activeElements, filterInView, filter } ) => { if ( activeElements === undefined || activeElements.length === 0 ) { @@ -36,24 +36,13 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { Span2: , }; - if ( filterInView?.operator === OPERATOR_IN ) { + if ( ALL_OPERATORS.includes( filterInView?.operator ) ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */ - __( '%1$s is %2$s' ), - filter.name, - activeElements.map( ( element ) => element.label ).join( ', ' ) - ), - filterTextWrappers - ); - } - - if ( filterInView?.operator === OPERATOR_NOT_IN ) { - return createInterpolateElement( - sprintf( - /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */ - __( '%1$s is not %2$s' ), + /* translators: 1: Filter name. 2: Filter operator. 3: Filter value. e.g.: "Author is Admin", "Author not in Admin, Editor". */ + __( '%1$s %2$s %3$s' ), filter.name, + OPERATORS[ filterInView.operator ].label, activeElements.map( ( element ) => element.label ).join( ', ' ) ), filterTextWrappers diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 38eafd2e57675..df70568333db1 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -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_EQUAL, + OPERATOR_NOT_EQUAL, +} from './constants'; import { __experimentalHStack as HStack } from '@wordpress/components'; const Filters = memo( function Filters( { @@ -43,16 +48,16 @@ const Filters = memo( function Filters( { field: field.id, name: field.header, elements: field.elements, - singleSelection: !! field.filterBy?.singleSelection, + singleSelection: operators.some( ( op ) => + [ OPERATOR_EQUAL, OPERATOR_NOT_EQUAL ].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, } ); diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index 99fd56acc4280..f6c987630d87d 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -7,7 +7,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ -import { OPERATORS } from './constants'; +import { ALL_OPERATORS, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; import { unlock } from './lock-unlock'; const { DropdownMenuSeparatorV2: DropdownMenuSeparator } = unlock( @@ -69,10 +69,10 @@ export function getPaginationResults( { data, view } ) { export const sanitizeOperators = ( field ) => { let operators = field.filterBy?.operators; if ( ! operators || ! Array.isArray( operators ) ) { - operators = Object.keys( OPERATORS ); + operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; // Default values. } return operators.filter( ( operator ) => - Object.keys( OPERATORS ).includes( operator ) + ALL_OPERATORS.includes( operator ) ); }; diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 0ad237b0a10dc..e7de0cc2af68f 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -46,7 +46,7 @@ import { PATTERN_SYNC_TYPES, PATTERN_DEFAULT_CATEGORY, ENUMERATION_TYPE, - OPERATOR_IN, + OPERATOR_EQUAL, } from '../../utils/constants'; import { exportJSONaction, @@ -323,9 +323,8 @@ export default function DataviewsPatterns() { type: ENUMERATION_TYPE, elements: SYNC_FILTERS, filterBy: { - operators: [ OPERATOR_IN ], + operators: [ OPERATOR_EQUAL ], isPrimary: true, - singleSelection: true, }, enableSorting: false, } ); diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index f5ca89b9fb62c..7aa0d1dd129ee 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -52,3 +52,5 @@ export const LAYOUT_LIST = 'list'; export const ENUMERATION_TYPE = 'enumeration'; export const OPERATOR_IN = 'in'; export const OPERATOR_NOT_IN = 'notIn'; +export const OPERATOR_EQUAL = 'equal'; +export const OPERATOR_NOT_EQUAL = 'notEqual'; From e9ae3628d34339b64b30545baed0885305685acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:42:45 +0100 Subject: [PATCH 07/24] Sanitize operators: do not allow mixing single&multi selection --- packages/dataviews/src/utils.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index f6c987630d87d..445fd1960b43f 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -68,12 +68,28 @@ export function getPaginationResults( { data, view } ) { export const sanitizeOperators = ( field ) => { let operators = field.filterBy?.operators; + + // Assign default values. if ( ! operators || ! Array.isArray( operators ) ) { - operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; // Default values. + operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; } - return operators.filter( ( operator ) => + + // Make sure only valid operators are used. + operators = operators.filter( ( operator ) => ALL_OPERATORS.includes( operator ) ); + + // Do not allow mixing single & multiselection operators. + if ( + operators.includes( OPERATOR_IN ) || + operators.includes( OPERATOR_NOT_IN ) + ) { + operators = operators.filter( ( operator ) => + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + ); + } + + return operators; }; export function WithDropDownMenuSeparators( { children } ) { From 5608a772190c5ef5d9efd7f3d87fd172ee8ec027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:46:03 +0100 Subject: [PATCH 08/24] Update docs --- packages/dataviews/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 2f17802176017..4dd2391601715 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -289,7 +289,7 @@ Allowed operators for fields of type `enumeration`: - `in`: whether an item is in a list of values. - `notIn`: whether an item is not in a list of values. -By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. +By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. A filter cannot mix single-selection (`equal`, `notEqual`) & multi-selection (`in`, `notIn`) operators. If a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded. ## Contributing to this package From 46da75df80ca656d0f33c26f8c8167a3a68a7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:47:28 +0100 Subject: [PATCH 09/24] Update docs --- packages/dataviews/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 4dd2391601715..6ce5451cd91dc 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -289,7 +289,7 @@ Allowed operators for fields of type `enumeration`: - `in`: whether an item is in a list of values. - `notIn`: whether an item is not in a list of values. -By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. A filter cannot mix single-selection (`equal`, `notEqual`) & multi-selection (`in`, `notIn`) operators. If a single-selection operator is present in the list of valid operators, the multi-selection ones will be discarded. +By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. A filter cannot mix single-selection (`equal`, `notEqual`) & multi-selection (`in`, `notIn`) 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. ## Contributing to this package From b9237bdbae88eef23dfaec4d02f7b40284c38ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:15:46 +0100 Subject: [PATCH 10/24] Fix translations --- packages/dataviews/src/filter-summary.js | 51 +++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index b49794598c2b9..1c9a289bce5c9 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -24,7 +24,13 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; * Internal dependencies */ import SearchWidget from './search-widget'; -import { ALL_OPERATORS, OPERATORS } from './constants'; +import { + OPERATORS, + OPERATOR_IN, + OPERATOR_NOT_IN, + OPERATOR_EQUAL, + OPERATOR_NOT_EQUAL, +} from './constants'; const FilterText = ( { activeElements, filterInView, filter } ) => { if ( activeElements === undefined || activeElements.length === 0 ) { @@ -36,19 +42,54 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { Span2: , }; - if ( ALL_OPERATORS.includes( filterInView?.operator ) ) { + if ( filterInView?.operator === OPERATOR_IN ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 2: Filter operator. 3: Filter value. e.g.: "Author is Admin", "Author not in Admin, Editor". */ - __( '%1$s %2$s %3$s' ), + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author in Admin, Editor". */ + __( '%1$s in %2$s' ), filter.name, - OPERATORS[ filterInView.operator ].label, activeElements.map( ( element ) => element.label ).join( ', ' ) ), filterTextWrappers ); } + if ( filterInView?.operator === OPERATOR_NOT_IN ) { + return createInterpolateElement( + sprintf( + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author not in Admin, Editor". */ + __( '%1$s not in %2$s' ), + filter.name, + activeElements.map( ( element ) => element.label ).join( ', ' ) + ), + filterTextWrappers + ); + } + + if ( filterInView?.operator === OPERATOR_EQUAL ) { + return createInterpolateElement( + sprintf( + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is Admin". */ + __( '%1$s is %2$s' ), + filter.name, + activeElements[ 0 ].label + ), + filterTextWrappers + ); + } + + if ( filterInView?.operator === OPERATOR_NOT_EQUAL ) { + return createInterpolateElement( + sprintf( + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not Admin". */ + __( '%1$s is not %2$s' ), + filter.name, + activeElements[ 0 ].label + ), + filterTextWrappers + ); + } + return sprintf( /* translators: 1: Filter name e.g.: "Unknown status for Author". */ __( 'Unknown status for %1$s' ), From c432000b754f02c3650218eb64ad4499a5eacc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:35:50 +0100 Subject: [PATCH 11/24] Add changelog note --- packages/dataviews/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 80579710332ba..d0b5cdb14ead6 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `is` and `isNot` for single-selection filters. + ## 0.7.0 (2024-03-06) ## 0.6.0 (2024-02-21) From 91b285ac356c0a3c83c41c049850b544685d04d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:51:10 +0100 Subject: [PATCH 12/24] Truncate text --- packages/dataviews/src/filter-summary.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 1c9a289bce5c9..f81a1bf5964d7 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -14,6 +14,7 @@ import { SelectControl, Tooltip, Icon, + __experimentalTruncate as Truncate, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useRef, createInterpolateElement } from '@wordpress/element'; @@ -38,15 +39,21 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { } const filterTextWrappers = { - Span1: , - Span2: , + Name: , + Value: ( + + ), }; if ( filterInView?.operator === OPERATOR_IN ) { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 3: Filter value. e.g.: "Author in Admin, Editor". */ - __( '%1$s in %2$s' ), + __( '%1$s in %2$s' ), filter.name, activeElements.map( ( element ) => element.label ).join( ', ' ) ), @@ -58,7 +65,7 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 3: Filter value. e.g.: "Author not in Admin, Editor". */ - __( '%1$s not in %2$s' ), + __( '%1$s not in %2$s' ), filter.name, activeElements.map( ( element ) => element.label ).join( ', ' ) ), @@ -70,7 +77,7 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is Admin". */ - __( '%1$s is %2$s' ), + __( '%1$s is %2$s' ), filter.name, activeElements[ 0 ].label ), @@ -82,7 +89,7 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not Admin". */ - __( '%1$s is not %2$s' ), + __( '%1$s is not %2$s' ), filter.name, activeElements[ 0 ].label ), From d5a4d86f150ed387e1de8d1f6b0d4688653cdc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:26:56 +0100 Subject: [PATCH 13/24] Fix templates & template parts custom filters --- .../src/components/page-templates-template-parts/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index 1462c5143a71d..6410f3498283f 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -382,7 +382,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { !! filter.value ) { filteredData = filteredData.filter( ( item ) => { - return item.author_text === filter.value; + return filter.value.includes( item.author_text ); } ); } else if ( filter.field === 'author' && @@ -390,7 +390,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { !! filter.value ) { filteredData = filteredData.filter( ( item ) => { - return item.author_text !== filter.value; + return ! filter.value.includes( item.author_text ); } ); } } ); From c273beac33da46c9d81fd20e36618d03641c70d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:27:57 +0100 Subject: [PATCH 14/24] Update changelog --- packages/dataviews/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index d0b5cdb14ead6..fc685fa28ce37 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `is` and `isNot` for single-selection filters. +- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `is` and `isNot` for single-selection filters. Note that the value in the `filters.view` would be an array for multi-selection as well. ## 0.7.0 (2024-03-06) From 54ef1653170c454a9a87ddb99f29c40511b22924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:49:49 +0100 Subject: [PATCH 15/24] Update terms --- packages/dataviews/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index fc685fa28ce37..498fdbbd00017 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `is` and `isNot` for single-selection filters. Note that the value in the `filters.view` would be an array for multi-selection as well. +- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `equal` and `notEqual` for single-selection filters. Note that the value in the `filters.view` would be an array for multi-selection as well. ## 0.7.0 (2024-03-06) From 532d4eeaab197715bca90ac1f5d405c4ddfbe692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:09:28 +0100 Subject: [PATCH 16/24] Fix summary tag --- packages/dataviews/src/filter-summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index f81a1bf5964d7..9f6289bb6c4f1 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -89,7 +89,7 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { return createInterpolateElement( sprintf( /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not Admin". */ - __( '%1$s is not %2$s' ), + __( '%1$s is not %2$s' ), filter.name, activeElements[ 0 ].label ), From 2ad3269c649e1638ead78a23b3512033137bceaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:14:34 +0100 Subject: [PATCH 17/24] Sanitize operators: remove multiselection ops if any single op is present --- packages/dataviews/src/utils.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index 445fd1960b43f..7440cc46bff5f 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -7,7 +7,13 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ -import { ALL_OPERATORS, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; +import { + ALL_OPERATORS, + OPERATOR_EQUAL, + OPERATOR_IN, + OPERATOR_NOT_EQUAL, + OPERATOR_NOT_IN, +} from './constants'; import { unlock } from './lock-unlock'; const { DropdownMenuSeparatorV2: DropdownMenuSeparator } = unlock( @@ -80,12 +86,13 @@ export const sanitizeOperators = ( field ) => { ); // Do not allow mixing single & multiselection operators. + // Remove multiselction operators if any of the single selection ones is present. if ( - operators.includes( OPERATOR_IN ) || - operators.includes( OPERATOR_NOT_IN ) + operators.includes( OPERATOR_EQUAL ) || + operators.includes( OPERATOR_NOT_EQUAL ) ) { operators = operators.filter( ( operator ) => - [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + [ OPERATOR_EQUAL, OPERATOR_NOT_EQUAL ].includes( operator ) ); } From 8f36ce37d8164486bfe24d561136e894dc39f5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:46:14 +0100 Subject: [PATCH 18/24] Remove truncation --- packages/dataviews/src/filter-summary.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 9f6289bb6c4f1..07dcdf9cf873e 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -14,7 +14,6 @@ import { SelectControl, Tooltip, Icon, - __experimentalTruncate as Truncate, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useRef, createInterpolateElement } from '@wordpress/element'; @@ -40,13 +39,7 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { const filterTextWrappers = { Name: , - Value: ( - - ), + Value: , }; if ( filterInView?.operator === OPERATOR_IN ) { From 5f3450c0bca3883154fba4f0b2cdaced20c27e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:49:40 +0100 Subject: [PATCH 19/24] Fix template & parts custom filtering --- .../src/components/page-templates-template-parts/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index 6410f3498283f..08742bd724c3d 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -379,7 +379,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { if ( filter.field === 'author' && filter.operator === OPERATOR_IN && - !! filter.value + filter?.value?.length > 0 ) { filteredData = filteredData.filter( ( item ) => { return filter.value.includes( item.author_text ); @@ -387,7 +387,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { } else if ( filter.field === 'author' && filter.operator === OPERATOR_NOT_IN && - !! filter.value + filter?.value?.length > 0 ) { filteredData = filteredData.filter( ( item ) => { return ! filter.value.includes( item.author_text ); From 374ad5aeed4f549cded8b298a2d781666061e244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:16:23 +0100 Subject: [PATCH 20/24] Tag the changes as Enhancements --- packages/dataviews/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 498fdbbd00017..3451617a069ea 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +### Enhancement + - The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `equal` and `notEqual` for single-selection filters. Note that the value in the `filters.view` would be an array for multi-selection as well. ## 0.7.0 (2024-03-06) From bee646ac019425bf1935564fc1cbd3376cf68f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:32:38 +0100 Subject: [PATCH 21/24] DataViews: use is/isNot/isAny/isNone operators --- packages/dataviews/src/constants.js | 40 ++++++++++++------------ packages/dataviews/src/filter-summary.js | 32 +++++++++---------- packages/dataviews/src/filters.js | 6 ++-- packages/dataviews/src/utils.js | 29 +++++++++++------ 4 files changed, 59 insertions(+), 48 deletions(-) diff --git a/packages/dataviews/src/constants.js b/packages/dataviews/src/constants.js index 3d250d784d7e0..bae974ff4eb5f 100644 --- a/packages/dataviews/src/constants.js +++ b/packages/dataviews/src/constants.js @@ -20,33 +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_EQUAL = 'equal'; -export const OPERATOR_NOT_EQUAL = 'notEqual'; +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_IN, - OPERATOR_NOT_IN, - OPERATOR_EQUAL, - OPERATOR_NOT_EQUAL, + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, ]; export const OPERATORS = { - [ OPERATOR_IN ]: { - key: 'in-filter', - label: __( 'In' ), - }, - [ OPERATOR_NOT_IN ]: { - key: 'not-in-filter', - label: __( 'Not in' ), - }, - [ OPERATOR_EQUAL ]: { - key: 'equal-filter', + [ OPERATOR_IS ]: { + key: 'is-filter', label: __( 'Is' ), }, - [ OPERATOR_NOT_EQUAL ]: { - key: 'not-equal-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 diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 07dcdf9cf873e..f382efb32be80 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -26,10 +26,10 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; import SearchWidget from './search-widget'; import { OPERATORS, - OPERATOR_IN, - OPERATOR_NOT_IN, - OPERATOR_EQUAL, - OPERATOR_NOT_EQUAL, + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, } from './constants'; const FilterText = ( { activeElements, filterInView, filter } ) => { @@ -42,11 +42,11 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { Value: , }; - if ( filterInView?.operator === OPERATOR_IN ) { + if ( filterInView?.operator === OPERATOR_IS_ANY ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 3: Filter value. e.g.: "Author in Admin, Editor". */ - __( '%1$s in %2$s' ), + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is any: Admin, Editor". */ + __( '%1$s is any: %2$s' ), filter.name, activeElements.map( ( element ) => element.label ).join( ', ' ) ), @@ -54,11 +54,11 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { ); } - if ( filterInView?.operator === OPERATOR_NOT_IN ) { + if ( filterInView?.operator === OPERATOR_IS_NONE ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 3: Filter value. e.g.: "Author not in Admin, Editor". */ - __( '%1$s not in %2$s' ), + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is none: Admin, Editor". */ + __( '%1$s is none: %2$s' ), filter.name, activeElements.map( ( element ) => element.label ).join( ', ' ) ), @@ -66,11 +66,11 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { ); } - if ( filterInView?.operator === OPERATOR_EQUAL ) { + if ( filterInView?.operator === OPERATOR_IS ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is Admin". */ - __( '%1$s is %2$s' ), + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is: Admin". */ + __( '%1$s is: %2$s' ), filter.name, activeElements[ 0 ].label ), @@ -78,11 +78,11 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { ); } - if ( filterInView?.operator === OPERATOR_NOT_EQUAL ) { + if ( filterInView?.operator === OPERATOR_IS_NOT ) { return createInterpolateElement( sprintf( - /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not Admin". */ - __( '%1$s is not %2$s' ), + /* translators: 1: Filter name. 3: Filter value. e.g.: "Author is not: Admin". */ + __( '%1$s is not: %2$s' ), filter.name, activeElements[ 0 ].label ), diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index df70568333db1..9f5cb0aedf7d8 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -13,8 +13,8 @@ import { sanitizeOperators } from './utils'; import { ENUMERATION_TYPE, ALL_OPERATORS, - OPERATOR_EQUAL, - OPERATOR_NOT_EQUAL, + OPERATOR_IS, + OPERATOR_IS_NOT, } from './constants'; import { __experimentalHStack as HStack } from '@wordpress/components'; @@ -49,7 +49,7 @@ const Filters = memo( function Filters( { name: field.header, elements: field.elements, singleSelection: operators.some( ( op ) => - [ OPERATOR_EQUAL, OPERATOR_NOT_EQUAL ].includes( op ) + [ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op ) ), operators, isVisible: diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js index 7440cc46bff5f..3b7a193f3d1e0 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.js @@ -9,10 +9,10 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; */ import { ALL_OPERATORS, - OPERATOR_EQUAL, - OPERATOR_IN, - OPERATOR_NOT_EQUAL, - OPERATOR_NOT_IN, + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, } from './constants'; import { unlock } from './lock-unlock'; @@ -77,7 +77,18 @@ export const sanitizeOperators = ( field ) => { // Assign default values. if ( ! operators || ! Array.isArray( operators ) ) { - operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; + operators = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; + } + + // Transform legacy in, notIn operators to is, isNot. + // To be removed in the future. + if ( operators.includes( 'in' ) ) { + operators = operators.filter( ( operator ) => operator !== 'is' ); + operators.push( 'is' ); + } + if ( operators.includes( 'notIn' ) ) { + operators = operators.filter( ( operator ) => operator !== 'notIn' ); + operators.push( 'isNot' ); } // Make sure only valid operators are used. @@ -86,13 +97,13 @@ export const sanitizeOperators = ( field ) => { ); // Do not allow mixing single & multiselection operators. - // Remove multiselction operators if any of the single selection ones is present. + // Remove multiselection operators if any of the single selection ones is present. if ( - operators.includes( OPERATOR_EQUAL ) || - operators.includes( OPERATOR_NOT_EQUAL ) + operators.includes( OPERATOR_IS ) || + operators.includes( OPERATOR_IS_NOT ) ) { operators = operators.filter( ( operator ) => - [ OPERATOR_EQUAL, OPERATOR_NOT_EQUAL ].includes( operator ) + [ OPERATOR_IS, OPERATOR_IS_NOT ].includes( operator ) ); } From aa6aec083b70ae9aaeabe7e194874b337ffb2440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:36:12 +0100 Subject: [PATCH 22/24] Site editor: use is/isNot/isAny/isNone operators --- .../edit-site/src/components/page-pages/index.js | 12 ++++++------ .../page-templates-template-parts/index.js | 8 ++++---- .../components/sidebar-dataviews/default-views.js | 14 +++++++++++--- packages/edit-site/src/utils/constants.js | 8 ++++---- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 73dd87eeab5a5..b8b735c4bfcf6 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -30,8 +30,8 @@ import { LAYOUT_GRID, LAYOUT_TABLE, LAYOUT_LIST, - OPERATOR_IN, - OPERATOR_NOT_IN, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, } from '../../utils/constants'; import { @@ -222,18 +222,18 @@ export default function PagePages() { view.filters.forEach( ( filter ) => { if ( filter.field === 'status' && - filter.operator === OPERATOR_IN + filter.operator === OPERATOR_IS_ANY ) { filters.status = filter.value; } if ( filter.field === 'author' && - filter.operator === OPERATOR_IN + filter.operator === OPERATOR_IS_ANY ) { filters.author = filter.value; } else if ( filter.field === 'author' && - filter.operator === OPERATOR_NOT_IN + filter.operator === OPERATOR_IS_NONE ) { filters.author_exclude = filter.value; } @@ -331,7 +331,7 @@ export default function PagePages() { elements: STATUSES, enableSorting: false, filterBy: { - operators: [ OPERATOR_IN ], + operators: [ OPERATOR_IS_ANY ], }, }, { diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index 08742bd724c3d..4ada7f98391d6 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -40,8 +40,8 @@ import { TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE, ENUMERATION_TYPE, - OPERATOR_IN, - OPERATOR_NOT_IN, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, LAYOUT_GRID, LAYOUT_TABLE, LAYOUT_LIST, @@ -378,7 +378,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { view.filters.forEach( ( filter ) => { if ( filter.field === 'author' && - filter.operator === OPERATOR_IN && + filter.operator === OPERATOR_IS_ANY && filter?.value?.length > 0 ) { filteredData = filteredData.filter( ( item ) => { @@ -386,7 +386,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { } ); } else if ( filter.field === 'author' && - filter.operator === OPERATOR_NOT_IN && + filter.operator === OPERATOR_IS_NONE && filter?.value?.length > 0 ) { filteredData = filteredData.filter( ( item ) => { diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index 1320564d0d9af..c02869dbc7fdb 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -11,7 +11,7 @@ import { LAYOUT_LIST, LAYOUT_TABLE, LAYOUT_GRID, - OPERATOR_IN, + OPERATOR_IS_ANY, } from '../../utils/constants'; export const DEFAULT_CONFIG_PER_VIEW_TYPE = { @@ -61,7 +61,11 @@ export const DEFAULT_VIEWS = { view: { ...DEFAULT_PAGE_BASE, filters: [ - { field: 'status', operator: OPERATOR_IN, value: 'draft' }, + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'draft', + }, ], }, }, @@ -72,7 +76,11 @@ export const DEFAULT_VIEWS = { view: { ...DEFAULT_PAGE_BASE, filters: [ - { field: 'status', operator: OPERATOR_IN, value: 'trash' }, + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'trash', + }, ], }, }, diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index 7aa0d1dd129ee..dfae1102df921 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -50,7 +50,7 @@ export const LAYOUT_GRID = 'grid'; export const LAYOUT_TABLE = 'table'; export const LAYOUT_LIST = 'list'; export const ENUMERATION_TYPE = 'enumeration'; -export const OPERATOR_IN = 'in'; -export const OPERATOR_NOT_IN = 'notIn'; -export const OPERATOR_EQUAL = 'equal'; -export const OPERATOR_NOT_EQUAL = 'notEqual'; +export const OPERATOR_IS = 'is'; +export const OPERATOR_IS_NOT = 'isNot'; +export const OPERATOR_IS_ANY = 'isAny'; +export const OPERATOR_IS_NONE = 'isNone'; From 7e109e1c24435ec079304af6ed8efc43c42c982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:41:26 +0100 Subject: [PATCH 23/24] fixup --- packages/edit-site/src/components/page-patterns/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index e7de0cc2af68f..d4cedaf27dee5 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -46,7 +46,7 @@ import { PATTERN_SYNC_TYPES, PATTERN_DEFAULT_CATEGORY, ENUMERATION_TYPE, - OPERATOR_EQUAL, + OPERATOR_IS, } from '../../utils/constants'; import { exportJSONaction, @@ -323,7 +323,7 @@ export default function DataviewsPatterns() { type: ENUMERATION_TYPE, elements: SYNC_FILTERS, filterBy: { - operators: [ OPERATOR_EQUAL ], + operators: [ OPERATOR_IS ], isPrimary: true, }, enableSorting: false, From 70e40ed995c72c2416f915a6f507086caba628cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:36:24 +0100 Subject: [PATCH 24/24] Update changelog and docs --- packages/dataviews/CHANGELOG.md | 2 +- packages/dataviews/README.md | 54 ++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 3451617a069ea..7c499b16c7501 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -4,7 +4,7 @@ ### Enhancement -- The filters that use `in` and `notIn` operators (the values by default) support now multi-selection. Use `equal` and `notEqual` for single-selection filters. Note that the value in the `filters.view` would be an array for multi-selection as well. +- 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) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 6ce5451cd91dc..32135bc65eba2 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -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', @@ -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, + }, ] ``` @@ -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, @@ -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. @@ -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: {}, @@ -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; } } ); @@ -284,12 +308,14 @@ Callback that signals the user triggered the details for one of more items, and Allowed operators for fields of type `enumeration`: -- `equal`: whether an item is equal to a single value. -- `notEqual`: whether an item is not equal to a single value. -- `in`: whether an item is in a list of values. -- `notIn`: whether an item is not in a list of values. +- `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. -By default, a field of type `enumeration` supports `in` and `notIn` operators — this is, it supports multiselection and negation. A filter cannot mix single-selection (`equal`, `notEqual`) & multi-selection (`in`, `notIn`) 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